zhou zhou
6 小时以前 fec285d150b377d004e47f0973d298b92fe4c711
#前端
321个文件已添加
42074 ■■■■■ 已修改文件
rsf-design/.auto-import.json 325 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/.env 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/.env.development 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/.env.production 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/.gitattributes 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/.gitignore 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/.prettierignore 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/.prettierrc 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/.stylelintignore 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/.stylelintrc.cjs 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/LICENSE 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/commitlint.config.cjs 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/eslint.config.mjs 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/index.html 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/package.json 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/pnpm-lock.yaml 5961 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/public/favicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/scripts/build-local-iconify-collections.mjs 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/scripts/clean-dev.helpers.mjs 265 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/scripts/clean-dev.js 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/SKILL.md 750 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/data/components.csv 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/00-index.md 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/components/art-form.md 632 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/components/art-search-bar.md 577 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/core-concepts/permission.md 199 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/core-concepts/project-structure.md 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/core-concepts/routing.md 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/examples/forms/search-bar.md 302 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/examples/index.md 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/examples/permission/button-permission.md 306 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/examples/tables/basic-table.md 180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/examples/templates/crud-page.md 350 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/generator-guide.md 378 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/getting-started/01-introduce.md 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/getting-started/02-quick-start.md 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/getting-started/03-must-read.md 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/getting-started/04-standard.md 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/getting-started/components-basics.md 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/getting-started/configuration-guide.md 286 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/getting-started/style-guide.md 355 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/docs/hooks/use-table.md 551 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/scripts/generate.py 816 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/scripts/init.py 423 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/scripts/list.py 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/scripts/search.py 176 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/scripts/verify.py 267 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/templates/env/.env.example 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/templates/package.json.template 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/templates/tailwind.config.js.template 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/skill/art-design-pro/templates/vite.config.js.template 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/App.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/auth.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/system-manage.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/avatar/avatar.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/avatar/avatar1.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/avatar/avatar10.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/avatar/avatar2.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/avatar/avatar3.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/avatar/avatar4.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/avatar/avatar5.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/avatar/avatar6.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/avatar/avatar7.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/avatar/avatar8.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/avatar/avatar9.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/ceremony/hb.png 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/ceremony/sd.png 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/ceremony/xc.png 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/ceremony/yd.png 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/common/logo.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/draw/draw1.png 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/favicon.ico 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/lock/bg_dark.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/lock/bg_light.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/login/lf_icon2.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/settings/menu_layouts/dual_column.png 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/settings/menu_layouts/horizontal.png 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/settings/menu_layouts/mixed.png 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/settings/menu_layouts/vertical.png 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/settings/menu_styles/dark.png 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/settings/menu_styles/design.png 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/settings/menu_styles/light.png 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/settings/theme_styles/dark.png 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/settings/theme_styles/light.png 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/settings/theme_styles/system.png 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/svg/403.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/svg/404.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/svg/500.svg 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/svg/login_icon.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/user/avatar.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/images/user/bg.webp 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/styles/core/app.scss 292 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/styles/core/dark.scss 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/styles/core/el-dark.scss 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/styles/core/el-light.scss 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/styles/core/el-ui.scss 519 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/styles/core/md.scss 1036 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/styles/core/mixin.scss 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/styles/core/reset.scss 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/styles/core/router-transition.scss 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/styles/core/tailwind.css 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/styles/core/theme-animation.scss 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/styles/core/theme-change.scss 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/styles/custom/one-dark-pro.scss 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/styles/index.scss 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/assets/svg/loading.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/banners/art-basic-banner/index.vue 261 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/banners/art-card-banner/index.vue 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/base/art-back-to-top/index.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/base/art-logo/index.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/base/art-svg-icon/index.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/cards/art-bar-chart-card/index.vue 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/cards/art-data-list-card/index.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/cards/art-donut-chart-card/index.vue 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/cards/art-image-card/index.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/cards/art-line-chart-card/index.vue 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/cards/art-progress-card/index.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/cards/art-stats-card/index.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/cards/art-timeline-list-card/index.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/charts/art-bar-chart/index.vue 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/charts/art-dual-bar-compare-chart/index.vue 159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/charts/art-h-bar-chart/index.vue 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/charts/art-k-line-chart/index.vue 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/charts/art-line-chart/index.vue 288 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/charts/art-radar-chart/index.vue 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/charts/art-ring-chart/index.vue 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/charts/art-scatter-chart/index.vue 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/forms/art-button-more/index.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/forms/art-button-table/index.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/forms/art-drag-verify/index.vue 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/forms/art-excel-export/index.vue 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/forms/art-excel-import/index.vue 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/forms/art-form/index.vue 367 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/forms/art-search-bar/index.vue 432 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/forms/art-wang-editor/index.vue 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/forms/art-wang-editor/style.scss 273 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-breadcrumb/index.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-chat-window/index.vue 228 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-fast-enter/index.vue 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-fireworks-effect/index.vue 427 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-global-component/index.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-global-search/index.vue 377 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-header-bar/index.vue 416 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-menus/art-mixed-menu/index.vue 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue 283 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss 251 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss 258 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-notification/index.vue 357 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-page-content/index.vue 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-screen-lock/index.vue 437 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.js 231 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.js 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.js 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsState.js 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/index.vue 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/style.scss 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/layouts/art-work-tab/index.vue 494 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/media/art-cutter-img/index.vue 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/media/art-video-player/index.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/others/art-menu-right/index.vue 303 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/others/art-watermark/index.vue 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/tables/art-table-header/index.vue 247 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/tables/art-table/index.vue 257 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/tables/art-table/style.scss 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/text-effect/art-count-to/index.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/text-effect/art-festival-text-scroll/index.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/text-effect/art-text-scroll/index.vue 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/theme/theme-svg/index.vue 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/views/exception/ArtException.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/views/login/AuthTopBar.vue 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/views/login/LoginLeftView.vue 600 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/views/result/ArtResultPage.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/components/core/widget/art-icon-button/index.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/config/assets/images.js 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/config/index.js 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/config/modules/component.js 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/config/modules/fastEnter.js 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/config/modules/festival.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/config/modules/headerBar.js 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/config/setting.js 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/directives/business/highlight.js 148 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/directives/business/ripple.js 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/directives/core/auth.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/directives/core/roles.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/directives/index.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/enums/appEnum.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/enums/formEnum.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/core/useAppMode.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/core/useAuth.js 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/core/useCeremony.js 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/core/useChart.js 534 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/core/useCommon.js 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/core/useFastEnter.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/core/useHeaderBar.js 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/core/useLayoutHeight.js 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/core/useTable.js 457 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/core/useTableColumns.js 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/core/useTableHeight.js 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/core/useTheme.js 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/hooks/index.js 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/locales/index.js 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/locales/langs/en.json 296 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/locales/langs/zh.json 296 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/main.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/mock/temp/formData.js 250 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/mock/upgrade/changeLog.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/plugins/echarts.js 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/plugins/iconify.collections.js 552 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/plugins/iconify.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/plugins/index.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/core/ComponentLoader.js 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/core/IframeRouteManager.js 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/core/MenuProcessor.js 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/core/RoutePermissionValidator.js 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/core/RouteRegistry.js 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/core/RouteTransformer.js 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/core/RouteValidator.js 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/core/index.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/guards/afterEach.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/guards/beforeEach.js 223 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/index.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/modules/dashboard.js 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/modules/exception.js 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/modules/index.js 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/modules/result.js 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/modules/system.js 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/routes/asyncRoutes.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/routes/staticRoutes.js 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/router/routesAlias.js 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/store/index.js 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/store/modules/menu.js 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/store/modules/setting.js 225 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/store/modules/table.js 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/store/modules/user.js 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/store/modules/worktab.js 343 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/constants/index.js 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/constants/links.js 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/form/index.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/form/responsive.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/form/validator.js 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/http/error.js 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/http/index.js 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/http/status.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/index.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/navigation/index.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/navigation/jump.js 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/navigation/route.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/navigation/worktab.js 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/router.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/socket/index.js 331 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/storage/index.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/storage/storage-config.js 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/storage/storage-key-manager.js 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/storage/storage.js 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/sys/console.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/sys/error-handle.js 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/sys/index.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/sys/mittBus.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/sys/upgrade.js 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/table/tableCache.js 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/table/tableConfig.js 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/table/tableUtils.js 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/ui/animation.js 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/ui/colors.js 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/ui/emojo.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/ui/iconify-loader.js 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/ui/index.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/ui/loading.js 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/utils/ui/tabs.js 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/auth/forget-password/index.vue 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/auth/login/index.vue 233 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/auth/login/style.css 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/auth/register/index.vue 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/dashboard/console/index.vue 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/dashboard/console/modules/about-project.vue 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/dashboard/console/modules/active-user.vue 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/dashboard/console/modules/card-list.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/dashboard/console/modules/dynamic-stats.vue 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/dashboard/console/modules/new-user.vue 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/dashboard/console/modules/sales-overview.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/dashboard/console/modules/todo-list.vue 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/exception/403/index.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/exception/404/index.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/exception/500/index.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/index/index.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/index/style.scss 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/outside/Iframe.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/result/fail/index.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/result/success/index.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/menu/index.vue 352 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/menu/modules/menu-dialog.vue 287 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/role/index.vue 213 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/role/modules/role-edit-dialog.vue 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/role/modules/role-permission-dialog.vue 153 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/role/modules/role-search.vue 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/user-center/index.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/user/index.vue 222 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/user/modules/user-dialog.vue 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/user/modules/user-search.vue 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/tests/clean-dev-helpers.test.mjs 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/tests/iconify-local-minimal.test.mjs 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/tests/iconify-local-prefixes.test.mjs 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/tests/manual-chunks.test.mjs 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/tests/repo-hygiene.test.mjs 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/vite.config.js 152 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/.auto-import.json
New file
@@ -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
  }
}
rsf-design/.env
New file
@@ -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
rsf-design/.env.development
New file
@@ -0,0 +1,13 @@
# 【开发】环境变量
# 应用部署基础路径(如部署在子目录 /admin,则设置为 /admin/)
VITE_BASE_URL = /
# API 请求基础路径(开发环境设置为 / 使用代理,生产环境设置为完整后端地址)
VITE_API_URL = /
# 代理目标地址(开发环境通过 Vite 代理转发请求到此地址,解决跨域问题)
VITE_API_PROXY_URL = https://m1.apifoxmock.com/m1/6400575-6097373-default
# Delete console
VITE_DROP_CONSOLE = false
rsf-design/.env.production
New file
@@ -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
rsf-design/.gitattributes
New file
@@ -0,0 +1,2 @@
*.html linguist-detectable=false
*.vue linguist-detectable=true
rsf-design/.gitignore
New file
@@ -0,0 +1,14 @@
node_modules/
dist/
dist-ssr/
.vite/
.playwright-cli/
output/
coverage/
*.log
__pycache__/
*.pyc
*.local
.DS_Store
Thumbs.db
.cursorrules
rsf-design/.prettierignore
New file
@@ -0,0 +1,3 @@
/node_modules/*
/dist/*
/src/main.ts
rsf-design/.prettierrc
New file
@@ -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
}
rsf-design/.stylelintignore
New file
@@ -0,0 +1,9 @@
dist
node_modules
public
.husky
.vscode
src/components/Layout/MenuLeft/index.vue
src/assets
stats.html
rsf-design/.stylelintrc.cjs
New file
@@ -0,0 +1,82 @@
module.exports = {
  // 继承推荐规范配置
  extends: [
    'stylelint-config-standard',
    'stylelint-config-recommended-scss',
    'stylelint-config-recommended-vue/scss',
    'stylelint-config-html/vue',
    'stylelint-config-recess-order'
  ],
  // 指定不同文件对应的解析器
  overrides: [
    {
      files: ['**/*.{vue,html}'],
      customSyntax: 'postcss-html'
    },
    {
      files: ['**/*.{css,scss}'],
      customSyntax: 'postcss-scss'
    }
  ],
  // 自定义规则
  rules: {
    'import-notation': 'string', // 指定导入CSS文件的方式("string"|"url")
    'selector-class-pattern': null, // 选择器类名命名规则
    'custom-property-pattern': null, // 自定义属性命名规则
    'keyframes-name-pattern': null, // 动画帧节点样式命名规则
    'no-descending-specificity': null, // 允许无降序特异性
    'no-empty-source': null, // 允许空样式
    'property-no-vendor-prefix': null, // 允许属性前缀
    // 允许 global 、export 、deep伪类
    'selector-pseudo-class-no-unknown': [
      true,
      {
        ignorePseudoClasses: ['global', 'export', 'deep']
      }
    ],
    // 允许未知属性
    'property-no-unknown': [
      true,
      {
        ignoreProperties: []
      }
    ],
    // 允许未知规则
    'at-rule-no-unknown': [
      true,
      {
        ignoreAtRules: [
          'apply',
          'use',
          'mixin',
          'include',
          'extend',
          'each',
          'if',
          'else',
          'for',
          'while',
          'reference'
        ]
      }
    ],
    'scss/at-rule-no-unknown': [
      true,
      {
        ignoreAtRules: [
          'apply',
          'use',
          'mixin',
          'include',
          'extend',
          'each',
          'if',
          'else',
          'for',
          'while',
          'reference'
        ]
      }
    ]
  }
}
rsf-design/LICENSE
New file
@@ -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.
rsf-design/commitlint.config.cjs
New file
@@ -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: {
    // 提交类型枚举,git提交type必须是以下类型
    'type-enum': [
      2,
      'always',
      [
        'feat', // 新增功能
        'fix', // 修复缺陷
        'docs', // 文档变更
        'style', // 代码格式(不影响功能,例如空格、分号等格式修正)
        'refactor', // 代码重构(不包括 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: '输入自定义issue前缀 :',
      footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
      generatingByAI: '正在通过 AI 生成你的提交简短描述...',
      generatedSelectByAI: '选择一个 AI 生成的简短描述:',
      confirmCommit: '是否提交或修改commit ?'
    },
    // prettier-ignore
    types: [
      { value: "feat",     name: "feat:     新增功能" },
      { value: "fix",      name: "fix:      修复缺陷" },
      { value: "docs",     name: "docs:     文档变更" },
      { value: "style",    name: "style:    代码格式(不影响功能,例如空格、分号等格式修正)" },
      { value: "refactor", name: "refactor: 代码重构(不包括 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: ''
  }
}
rsf-design/eslint.config.mjs
New file
@@ -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: {} }
  }
}
rsf-design/index.html
New file
@@ -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>
rsf-design/package.json
New file
@@ -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"
  }
}
rsf-design/pnpm-lock.yaml
New file
Diff too large
rsf-design/public/favicon.ico
rsf-design/scripts/build-local-iconify-collections.mjs
New file
@@ -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}`)
rsf-design/scripts/clean-dev.helpers.mjs
New file
@@ -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'")
}
rsf-design/scripts/clean-dev.js
New file
@@ -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('- 重写快速入口、路由别名、更新日志数据和菜单接口')
  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)
})
rsf-design/skill/art-design-pro/SKILL.md
New file
@@ -0,0 +1,750 @@
---
name: art-design-pro
description: "Use when 在当前 Art Design Pro JavaScript 版仓库中编写 Vue3 页面、组件、表格、表单、图表或布局,并且需要优先复用现有组件、hooks、页面模式和本地文档时。"
---
# 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 代码。**
## 🎨 样式规范(严格执行)
### ⚠️ 重要:样式使用规则
**严禁自我发挥编写样式**,必须严格遵循以下规范:
#### 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、Element 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**
当在模态窗(ElDialog)中使用 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 包裹 -->
    <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'
<!-- ✅ 正确:使用图表组件(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">
    <!-- ⚠️ 重要:ArtTableHeader 需要通过 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`**:内容长度固定的列(状态、角色、操作列、端口等)
   - ✅ **使用 `minWidth`**:内容长度可变的列(用户名、备注、域名、时间等)
   - ❌ **避免全部使用固定 `width`**:会导致表格总宽度固定,屏幕更宽时右侧出现空白
   **示例**:
   ```js
   // ✅ 正确:混合使用 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` - 图标背景样式(CSS 字符串)
## 工作流程
当用户请求编写页面时:
1. **分析需求** - 确定页面类型(列表、表单、仪表板等)
2. **搜索组件** - 使用 `search.py` 查找相关组件
3. **选择组件** - 根据功能需求选择最合适的组件
4. **查看文档** - 阅读组件的 props、slots、events
5. **编写代码** - 使用组件而不是自己编写 Vue 代码
## 禁止行为
### 组件使用
❌ **不要**自己编写表格组件 → 使用 `ArtTable`
❌ **不要**自己编写表单组件 → 使用 `ArtForm`
❌ **不要**自己编写图表组件 → 使用 `Art*Chart`
❌ **不要**自己编写统计卡片 → 使用 `ArtStatsCard`
❌ **不要**自己编写搜索栏 → 使用 `ArtSearchBar`
### 样式编写(⚠️ 严格执行)
❌ **不要**编写自定义 CSS 类 → 使用 Tailwind CSS 工具类
❌ **不要**使用内联 `style` 属性 → 使用 Tailwind 类
❌ **不要**硬编码颜色值 → 使用 `var(--art-*)` CSS 变量
❌ **不要**覆盖 Element Plus 样式 → 使用组件 props 控制
❌ **不要**编写 `<style scoped>` → 使用 Tailwind 工具类
❌ **不要**引入外部样式库 → 只用 Tailwind + Element Plus
**为什么这么严格?**
- ✅ 保持 UI 风格统一
- ✅ 避免样式冲突
- ✅ 减少代码体积
- ✅ 确保主题切换正常
- ✅ 符合项目设计规范
## 组件位置
所有组件位于:`src/components/core/`
## 相关资源
### 官方文档
- Art Design Pro 官方文档:https://www.artd.pro/docs/
- Element Plus 文档:https://element-plus.org/
### 本地文档
**文档已重组为面向集成的结构**,查看完整索引:`docs/00-index.md`
#### 集成相关(最重要)
- `INTEGRATION_GUIDE.md` - **完整集成指南**:将 Art Design Pro 集成到其他项目
- `templates/` - 配置模板:vite.config.js、package.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` - **必读**:接口对接、网络请求、菜单配置
- `docs/getting-started/04-standard.md` - 代码规范
- `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` - 代码生成器使用指南
**使用方法**:
所有文档都是本地 Markdown 文件,在编写代码前先查阅相关文档,确保遵循官方规范。
rsf-design/skill/art-design-pro/data/components.csv
New file
@@ -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,数据导出到Excel,@/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,散点图,Scatter Chart,ECharts散点图,@/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,让SVG图片跟随主题的组件,@/components/core/theme/theme-svg,size,themeColor,src,default,主题相关SVG
rsf-design/skill/art-design-pro/docs/00-index.md
New file
@@ -0,0 +1,100 @@
# Art Design Pro 文档索引
**本地化状态**: ✅ 100%
本目录包含 Art Design Pro 的文档,已重组为面向集成的结构。
---
## 📚 快速开始
| 文件 | 标题 | 说明 |
|------|------|------|
| [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) | 规范 | 代码规范和提交规范 |
## 🔧 配置和集成
| 文件 | 标题 | 说明 |
|------|------|------|
| [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、CSS 变量、响应式设计 |
## 🏗️ 核心概念
| 文件 | 标题 | 说明 |
|------|------|------|
| [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) | 常见问题 | 常见问题解答 |
| [BEST_PRACTICES.md](../BEST_PRACTICES.md) | 最佳实践 | 最佳实践指南 |
| [CHEATSHEET.md](../CHEATSHEET.md) | 速查表 | 组件速查表 |
| [generator-guide.md](generator-guide.md) | 代码生成器 | 代码生成器使用指南 |
| [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)
---
## ✅ 本地化优势
- **零网络依赖** - 所有文档本地保存
- **零幻觉风险** - 内容来自官方文档和实际代码
- **快速访问** - 无需等待网络加载
- **永久可用** - 不受 URL 变化影响
---
**更新日期**: 2026-03-04
**版本**: v2.2.1+
**官方文档**: https://www.artd.pro/docs/
rsf-design/skill/art-design-pro/docs/components/art-form.md
New file
@@ -0,0 +1,632 @@
# ArtForm 表单组件 | Art Design Pro
## 概述
ArtForm 是一个功能强大的表单组件,基于 Element Plus 封装,支持双向绑定、表单验证、响应式布局等特性。
## 特性
- **双向绑定支持** - 使用 `defineModel`,支持 `v-model` 和 `:model` 两种绑定方式
- **表单验证** - 完整的表单验证支持,与 Element Plus Form 规则兼容
- **响应式布局** - 基于 24 栅格系统,自适应不同屏幕尺寸
- **多种控件类型** - 支持 input、number、select、switch、date 等 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:v-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>
<!-- ✅ 正确:隐藏 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
**维护者**:Art Design Pro Skill Team
rsf-design/skill/art-design-pro/docs/components/art-search-bar.md
New file
@@ -0,0 +1,577 @@
# ArtSearchBar 搜索栏组件 | Art Design Pro
来源:https://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' // 这里的type会被插槽覆盖
  }
]
</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>
```
rsf-design/skill/art-design-pro/docs/core-concepts/permission.md
New file
@@ -0,0 +1,199 @@
# 14 Permission
> 来源:14 Permission
本系统支持两种权限控制模式,基于用户角色或菜单列表动态管理页面访问和按钮显示权限。
## 权限控制模式​
### 概述​
系统提供以下两种权限控制模式:
- 基于角色:通过接口获取用户角色,控制页面访问和按钮显示权限。
- 基于菜单:通过接口获取菜单列表,依据菜单结构控制页面访问和按钮权限。
### 配置方式​
权限控制模式通过根目录下的.env文件配置。修改VITE_ACCESS_MODE的值可切换模式:
`.env``VITE_ACCESS_MODE`- frontend:前端控制模式,基于后端返回的角色标识进行权限控制。
- backend:后端控制模式,基于后端返回的菜单列表进行权限控制。
```
# 权限控制模式(frontend | backend)
VITE_ACCESS_MODE=frontend
```
## 前端控制模式​
### 原理​
前端维护菜单列表。用户登录后,接口返回角色标识(如R_SUPER)。前端根据角色遍历菜单列表,若菜单的roles字段包含该角色,则允许访问对应路由。若未设置roles,则默认所有用户可访问。
`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字段匹配,否则用户无法访问受限页面。
## 后端控制模式​
### 原理​
后端生成菜单列表。用户登录后,接口返回菜单数据,前端校验后动态注册路由,实现权限控制。
### 数据结构​
菜单数据结构定义位于:/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,
        },
      },
    ],
  },
];
```
### 注意事项​
- 后端返回的菜单数据结构必须与前端定义一致,否则可能导致路由注册失败。
## 前后端控制模式对比​
- 前端控制模式:适用于角色固定的系统。后端角色变更需同步更新前端路由配置。实现简单,适合小型项目。
- 后端控制模式:适用于权限复杂的系统。后端返回完整菜单列表,前端动态注册路由。更灵活,但需确保前后端数据结构一致。
## 按钮权限控制​
按钮权限控制支持精细化管理,通过用户角色或接口返回的权限码动态控制按钮显示。
### 权限码​
权限码适用于前端和后端控制模式:
- 前端控制模式:登录接口需返回权限码列表。
- 后端控制模式:菜单列表需包含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" },
      ],
    },
  },
];
```
#### 使用方式​
通过系统提供的hasAuth方法控制按钮显示:
`hasAuth````
import { useAuth } from "@/composables/useAuth";
const { hasAuth } = useAuth();
```
```
<ElButton v-if="hasAuth('add')">添加</ElButton>
```
### 自定义指令(v-auth)​
在后端控制模式下,可通过自定义指令v-auth基于authList的authMark控制按钮显示。
`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)​
可基于用户信息接口中返回的roles进行权限控制。
`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>
```
## 注意事项​
- 确保登录接口返回的角色或权限码与路由表配置一致。
- 后端控制模式下,菜单数据需严格遵循前端定义的结构。
- 测试权限控制时,验证不同角色用户的页面和按钮显示是否符合预期。
rsf-design/skill/art-design-pro/docs/core-concepts/project-structure.md
New file
@@ -0,0 +1,75 @@
# 项目结构 | Art Design Pro
来源:https://www.artd.pro/docs/zh/guide/essentials/project-introduce.html
```
├── src
│   ├── api                     # API 接口相关代码
│   │   ├── 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                # 系统组件(Art 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                 # 项目主入口文件
│   ├── mock                    # Mock 数据目录
│   ├── router                  # Vue Router 路由相关代码
│   │   ├── 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 的核心组件库,包含所有可复用的业务组件:
- **表格**:ArtTable, ArtTableHeader
- **表单**:ArtForm, ArtSearchBar, ArtButtonTable
- **图表**:ArtStatsCard, ArtLineChart, ArtBarChart
- **布局**:ArtPageContent, ArtBreadcrumb, ArtHeaderBar
### views/
页面组件目录,所有业务页面都应该放在这里。
### router/routes/
- **staticRoutes.ts**: 静态路由(登录、404等)
- **asyncRoutes.ts**: 动态路由(业务页面)
### config/
项目配置,包括系统名称、主题配置等。
rsf-design/skill/art-design-pro/docs/core-concepts/routing.md
New file
@@ -0,0 +1,149 @@
# 路由和菜单 | Art Design Pro
来源:https://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: "&#xe721;",
    },
    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: "&#xe721;",
  },
  children: [
    {
      path: "basic",
      name: "Basic",
      component: "/form/basic",
      meta: {
        title: "基础表单",
        keepAlive: true,
      },
    }
  ],
}
```
### 3. 访问页面
访问 `http://localhost:3006/form/basic` 即可查看新建的页面。
rsf-design/skill/art-design-pro/docs/examples/forms/search-bar.md
New file
@@ -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`
rsf-design/skill/art-design-pro/docs/examples/index.md
New file
@@ -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. 使用代码生成器
使用代码生成器快速创建页面模板:
```bash
# 生成 CRUD 页面
python skill/art-design-pro/scripts/generate.py \
  crud \
  --name "User" \
  --path "system/user" \
  --fields "username,email,phone,status"
```
详见:[代码生成器使用指南](../generator-guide.md)
### 3. 参考实际项目代码
所有示例都提取自实际项目,你可以在以下位置找到完整代码:
- 表格示例:`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/)
- [代码生成器](../generator-guide.md)
## 🤝 贡献示例
如果你有好的示例想要分享,请:
1. 将代码提交到 `src/views/examples/`
2. 在本目录添加对应的文档
3. 更新索引文件
---
**最后更新**:2026-03-03
**维护者**:Art Design Pro Skill Team
rsf-design/skill/art-design-pro/docs/examples/permission/button-permission.md
New file
@@ -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>
    <!-- 多个权限码都具备时才可见(AND 逻辑) -->
    <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')
  // 检查多个权限码(OR 逻辑)
  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[] | - | 角色代码 |
| 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. 前后端权限验证
前端权限控制只是为了用户体验,真正的权限验证必须在后端进行:
```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`
rsf-design/skill/art-design-pro/docs/examples/tables/basic-table.md
New file
@@ -0,0 +1,180 @@
# 基础表格使用示例
## 概述
这是一个最简单的表格使用示例,展示了如何使用 `useTable` Hook 快速创建一个数据表格。
## 完整代码
```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`
rsf-design/skill/art-design-pro/docs/examples/templates/crud-page.md
New file
@@ -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`
## 使用代码生成器
可以使用代码生成器快速创建类似页面:
```bash
python skill/art-design-pro/scripts/generate.py \
  crud \
  --name "User" \
  --path "system/user" \
  --fields "username,email,phone,status"
```
详见 [代码生成器使用指南](../../generator-guide.md)。
rsf-design/skill/art-design-pro/docs/generator-guide.md
New file
@@ -0,0 +1,378 @@
# Art Design Pro 代码生成器使用指南
> **重要更新 (2026-03-04)**: 代码生成器已更新为使用 ArtForm 组件和正确的 ArtSearchBar 用法。
## 📖 简介
代码生成器是 Art Design Pro Skill 的核心工具之一,可以自动生成标准的 Vue3 页面代码,大幅提升开发效率。
## ✨ 最新更新
### v2.3.0 (2026-03-04)
- ✅ **修正 ArtSearchBar 用法** - 使用 `v-model` + `:items` + `@search` + `@reset`
- ✅ **弹窗使用 ArtForm** - 替换原来的 `ElForm`,使用 `ArtForm` + `:model`
- ✅ **添加表单项配置生成** - 自动生成 `searchItems` 和 `formItems` 配置
- ✅ **支持多种数据类型** - 自动识别 `number`、`boolean`、`date` 等类型
- ✅ **修正导入语句** - 添加 `FormInstance` 类型导入
### 生成代码特点
**搜索栏组件:**
```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)
- ✅ 弹窗组件(modules/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`)
生成数据统计和图表展示页面,包含:
- 统计卡片(ArtStatsCard)
- 图表组件(ArtLineChart、ArtBarChart 等)
- 响应式布局
**使用示例**:
```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` - 饼图(ArtRingChart)
- `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. **生成代码**
   ```bash
   python skill/art-design-pro/scripts/generate.py crud \
     --name "User" \
     --path "system/user" \
     --fields "username,email,status"
   ```
2. **复制代码到项目**
   - 将输出的代码保存到对应文件
   - 创建目录结构:`src/views/system/user/`
3. **调整代码**
   - 替换 `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**:在生成代码后,修改 `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` 中的模板字符串,自定义生成的代码风格。
### 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
**维护者**:Art Design Pro Skill Team
rsf-design/skill/art-design-pro/docs/getting-started/01-introduce.md
New file
@@ -0,0 +1,56 @@
# 介绍 | Art Design Pro
来源:https://www.artd.pro/docs/zh/guide/introduce.html
## 关于 Art Design Pro
作为一名开发者,我发现传统系统在用户体验和视觉设计上不能完全满足需求。这让我开始思考,如何设计一款既精致美观又能提供顺畅操作体验的后台系统。
经过反复思考,我决定推出 Art Design Pro —— 它不仅融汇了最新的技术,更秉持着对完美用户体验和视觉设计的追求。
## 解决的问题
### 视觉疲劳与乏味
长时间面对冷冰冰的界面,难免会让人感到疲惫。本模板力求通过精心设计的配色、排版和动效,让每一次点击、每一次滚动都像是一段美好的互动体验。
### 用户体验不佳
管理系统不仅要看得舒服,更要用得顺手。我希望通过简洁直观的布局和流畅自然的交互,让用户能迅速找到所需功能,享受到一种人性化的关怀。
### 重复造轮子,开发成本高
我都曾为从零开始设计一套完整的前端界面而头疼。这个模板提供了一整套经过反复打磨的 UI 组件和设计规范,让你可以快速搭建出既美观又实用的后台管理系统。
## 核心特色
### 美学与实用并重
我相信技术和艺术可以并肩同行。模板采用了流行而不浮夸的设计风格,每一处细节都经过精心打磨,力求让界面既赏心悦目,又不会喧宾夺主。
### 情感化的用户体验
每一个交互动效,每一处按钮反馈,都是为了让用户在操作时感受到温暖与人性化。
### 模块化、灵活定制
我提供了高度模块化的设计,各个组件都可以根据实际需求自由调整。
### 全响应式设计
模板不仅适配各种屏幕尺寸,更通过精细的设计让不同设备下的用户都能获得一致而优质的体验。
### 拒绝过度封装
我坚持简洁和透明的原则,避免对系统进行过度封装。你可以轻松修改和扩展任意部分。
## 技术栈
- **开发框架**:Vue3、JavaScript、Vite、Element-Plus、Scss
- **代码规范**:Eslint、Prettier、Stylelint、Husky、Lint-staged、cz-git
## 浏览器支持
Chrome、Edge、Firefox、Safari、Opera 等现代浏览器。
rsf-design/skill/art-design-pro/docs/getting-started/02-quick-start.md
New file
@@ -0,0 +1,55 @@
# 快速开始 | Art Design Pro
来源:https://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
```
项目启动后会自动打开浏览器运行,默认访问地址:http://localhost:3006
rsf-design/skill/art-design-pro/docs/getting-started/03-must-read.md
New file
@@ -0,0 +1,88 @@
# 开发必读文档 | Art Design Pro
来源:https://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) {
    // 这里可以根据状态码进行不同的处理
  }
}
```
## 菜单数据(asyncRoutes.ts)
- `RoutesAlias.Layout` 指向的是布局容器
- 后端返回的菜单数据中,component 字段需要指向 `/index/index`
- `roles` 字段用于前端控制模式
**前端模式**:通过获取用户信息接口返回的 roles 跟菜单数据 asyncRoutes 中的 roles 进行对比实现菜单过滤
**后端模式**:直接通过接口返回对应角色的菜单即可,不需要返回 `roles` 字段
示例:
```js
{
  name: 'Dashboard',
  path: '/dashboard',
  component: RoutesAlias.Layout,
  meta: {
    title: 'menus.dashboard.title',
    icon: '&#xe721;',
    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 左右:
- 精简或替换图标库
- 移除非必要图片资源
- 减少第三方库依赖,或替换为更轻量的方案
rsf-design/skill/art-design-pro/docs/getting-started/04-standard.md
New file
@@ -0,0 +1,106 @@
# 规范 | Art Design Pro
来源:https://www.artd.pro/docs/zh/guide/project/standard.html
## 代码规范工具
- **Eslint**: JS 代码检查
- **Prettier**: 代码格式化
- **Stylelint**: CSS 代码检查
- **Commitlint**: Git 提交信息检查
- **Husky**: Git 钩子工具
- **Lint-staged**: Git 提交前运行代码校验
- **cz-git**: 可视化提交工具
## 自动化
代码提交会自动执行配置好的文件,自动完成代码校验和格式化。
`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    // 代码格式(不影响功能)
refactor // 代码重构(不包括 bug 修复、功能新增)
perf     // 性能优化
test     // 添加疏漏测试或已有测试改动
build    // 构建流程、外部依赖变更
ci       // 修改 CI 配置、脚本
revert   // 回滚 commit
chore    // 对构建过程或辅助工具的更改
wip      // 对构建过程或辅助工具的更改
```
### 提交代码流程
```bash
git add .
pnpm commit
# 选择提交类型,填写提交信息
git push
```
## 注意事项
1. **不要跳过 Husky 钩子**:使用 `pnpm commit` 而不是 `git commit`
2. **提交前自动格式化**:Lint-staged 会自动格式化暂存的文件
3. **遵循提交规范**:使用 Commitizen 选择正确的提交类型
rsf-design/skill/art-design-pro/docs/getting-started/components-basics.md
New file
@@ -0,0 +1,137 @@
# 组件和图标基础 | Art Design Pro
本文档介绍 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 提供:
- **基础组件**:按钮、表单、表格等
- **反馈组件**:弹窗、消息提示等
- **布局组件**:容器、栅格系统等
**官方文档**:https://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">&#xe649;</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#环境变量)
rsf-design/skill/art-design-pro/docs/getting-started/configuration-guide.md
New file
@@ -0,0 +1,286 @@
# 系统配置指南 | Art Design Pro
本文档介绍 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
# 权限模式(frontend | 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)
rsf-design/skill/art-design-pro/docs/getting-started/style-guide.md
New file
@@ -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"> 正常字体 </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`: 中等及以上显示
---
## 📝 代码审查清单
在提交代码前,确认:
- [ ] 所有样式使用 Tailwind CSS 工具类
- [ ] 颜色使用 CSS 变量(`var(--art-*)`)
- [ ] 没有自定义 `<style scoped>` 块
- [ ] 没有内联 `style` 属性(除非动态绑定)
- [ ] 响应式使用断点前缀(`md:`, `lg:` 等)
- [ ] 没有硬编码颜色值
- [ ] 没有覆盖 Element Plus 样式
- [ ] 使用组件 props 而非样式覆盖
---
**违反本规范可能导致**:
- ❌ UI 不统一
- ❌ 样式冲突
- ❌ 主题切换失效
- ❌ 响应式布局错误
- ❌ 代码审查不通过
---
**遵循本规范的好处**:
- ✅ 保持设计一致性
- ✅ 减少样式冲突
- ✅ 代码更易维护
- ✅ 主题自动适配
- ✅ 响应式开箱即用
rsf-design/skill/art-design-pro/docs/hooks/use-table.md
New file
@@ -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` | 状态、角色、端口等 | `{ width: 80 }` |
| **操作列** | `width: 150-260` | 编辑、删除等操作按钮 | `{ width: 200, fixed: 'right' }` |
| **可变内容列** | `minWidth: 100-200` | 用户名、备注、域名等 | `{ minWidth: 120 }` |
| **时间日期列** | `minWidth: 150-180` | 创建时间、更新时间等 | `{ minWidth: 160 }` |
**✅ 正确示例**:
```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
rsf-design/skill/art-design-pro/scripts/generate.py
New file
@@ -0,0 +1,816 @@
#!/usr/bin/env python3
"""
Art Design Pro 代码生成器
自动生成标准的 Vue3 页面代码,包括:
- 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:
    """代码生成器基类"""
    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]]:
        """解析字段参数"""
        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):
        """生成代码(子类实现)"""
        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):
        """生成仪表板页代码"""
        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 代码生成器',
        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、Product)')
    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='图表类型(逗号分隔:line,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✅ 代码生成完成!')
    print(f'💡 提示:请将生成的代码复制到对应目录,并根据实际需求调整')
    print(f'📂 目标目录: src/views/{args.path}/')
if __name__ == '__main__':
    main()
rsf-design/skill/art-design-pro/scripts/init.py
New file
@@ -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 生成代码")
            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
    # 代码生成配置
    output_path = input(f"生成代码输出路径 [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🔍 正在自动检测项目配置...")
        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. 生成代码: python scripts/generate.py crud --name 'User' --path 'system/user' --fields 'username,email'")
    print("  3. 检查集成状态: python scripts/verify.py")
    print()
if __name__ == "__main__":
    main()
rsf-design/skill/art-design-pro/scripts/list.py
New file
@@ -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()
rsf-design/skill/art-design-pro/scripts/search.py
New file
@@ -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()
rsf-design/skill/art-design-pro/scripts/verify.py
New file
@@ -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 配置正确")
            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🎉 所有检查通过!你的项目已正确集成 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()
rsf-design/skill/art-design-pro/templates/env/.env.example
New file
@@ -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
rsf-design/skill/art-design-pro/templates/package.json.template
New file
@@ -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"
  }
}
rsf-design/skill/art-design-pro/templates/tailwind.config.js.template
New file
@@ -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: [],
}
rsf-design/skill/art-design-pro/templates/vite.config.js.template
New file
@@ -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)
}
rsf-design/src/App.vue
New file
@@ -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>
rsf-design/src/api/auth.js
New file
@@ -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 }
rsf-design/src/api/system-manage.js
New file
@@ -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 }
rsf-design/src/assets/images/avatar/avatar.webp
Binary files differ
rsf-design/src/assets/images/avatar/avatar1.webp
Binary files differ
rsf-design/src/assets/images/avatar/avatar10.webp
Binary files differ
rsf-design/src/assets/images/avatar/avatar2.webp
Binary files differ
rsf-design/src/assets/images/avatar/avatar3.webp
Binary files differ
rsf-design/src/assets/images/avatar/avatar4.webp
Binary files differ
rsf-design/src/assets/images/avatar/avatar5.webp
Binary files differ
rsf-design/src/assets/images/avatar/avatar6.webp
Binary files differ
rsf-design/src/assets/images/avatar/avatar7.webp
Binary files differ
rsf-design/src/assets/images/avatar/avatar8.webp
Binary files differ
rsf-design/src/assets/images/avatar/avatar9.webp
Binary files differ
rsf-design/src/assets/images/ceremony/hb.png
rsf-design/src/assets/images/ceremony/sd.png
rsf-design/src/assets/images/ceremony/xc.png
rsf-design/src/assets/images/ceremony/yd.png
rsf-design/src/assets/images/common/logo.webp
Binary files differ
rsf-design/src/assets/images/draw/draw1.png
rsf-design/src/assets/images/favicon.ico
rsf-design/src/assets/images/lock/bg_dark.webp
Binary files differ
rsf-design/src/assets/images/lock/bg_light.webp
Binary files differ
rsf-design/src/assets/images/login/lf_icon2.webp
Binary files differ
rsf-design/src/assets/images/settings/menu_layouts/dual_column.png
rsf-design/src/assets/images/settings/menu_layouts/horizontal.png
rsf-design/src/assets/images/settings/menu_layouts/mixed.png
rsf-design/src/assets/images/settings/menu_layouts/vertical.png
rsf-design/src/assets/images/settings/menu_styles/dark.png
rsf-design/src/assets/images/settings/menu_styles/design.png
rsf-design/src/assets/images/settings/menu_styles/light.png
rsf-design/src/assets/images/settings/theme_styles/dark.png
rsf-design/src/assets/images/settings/theme_styles/light.png
rsf-design/src/assets/images/settings/theme_styles/system.png
rsf-design/src/assets/images/svg/403.svg
New file
@@ -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>
rsf-design/src/assets/images/svg/404.svg
New file
@@ -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>
rsf-design/src/assets/images/svg/500.svg
New file
@@ -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>
rsf-design/src/assets/images/svg/login_icon.svg
New file
@@ -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>
rsf-design/src/assets/images/user/avatar.webp
Binary files differ
rsf-design/src/assets/images/user/bg.webp
Binary files differ
rsf-design/src/assets/styles/core/app.scss
New file
@@ -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;
  }
}
rsf-design/src/assets/styles/core/dark.scss
New file
@@ -0,0 +1,93 @@
/*
* 深色主题
* 单页面移除深色主题 document.getElementsByTagName("html")[0].removeAttribute('class')
*/
$font-color: rgba(#ffffff, 0.85);
/* 覆盖element-plus默认深色背景色 */
html.dark {
  // element-plus
  --el-bg-color: var(--default-box-color);
  --el-text-color-regular: #{$font-color};
  // 富文本编辑器
  // 工具栏背景颜色
  --w-e-toolbar-bg-color: #18191c;
  // 输入区域背景颜色
  --w-e-textarea-bg-color: #090909;
  // 工具栏文字颜色
  --w-e-toolbar-color: var(--art-gray-600);
  // 选中菜单颜色
  --w-e-toolbar-active-bg-color: #25262b;
  // 弹窗边框颜色
  --w-e-toolbar-border-color: var(--default-border-dashed);
  // 分割线颜色
  --w-e-textarea-border-color: var(--default-border-dashed);
  // 链接输入框边框颜色
  --w-e-modal-button-border-color: var(--default-border-dashed);
  // 表格头颜色
  --w-e-textarea-slight-bg-color: #090909;
  // 按钮背景颜色
  --w-e-modal-button-bg-color: #090909;
  // hover toolbar 背景颜色
  --w-e-toolbar-active-color: var(--art-gray-800);
}
.dark {
  .page-content .article-list .item .left .outer > div {
    border-right-color: var(--dark-border-color) !important;
  }
  // 富文本编辑器
  .editor-wrapper {
    *:not(pre code *) {
      color: inherit !important;
    }
  }
  // 分隔线
  .w-e-bar-divider {
    background-color: var(--art-gray-300) !important;
  }
  .w-e-select-list,
  .w-e-drop-panel,
  .w-e-bar-item-group .w-e-bar-item-menus-container,
  .w-e-text-container [data-slate-editor] pre > code {
    border: 1px solid var(--default-border) !important;
  }
  // 下拉选择框
  .w-e-select-list {
    background-color: var(--default-box-color) !important;
  }
  /* 下拉选择框 hover 样式调整 */
  .w-e-select-list ul li:hover,
    /* 工具栏 hover 按钮背景颜色 */
    .w-e-bar-item button:hover {
    background-color: #090909 !important;
  }
  /* 代码块 */
  .w-e-text-container [data-slate-editor] pre > code {
    background-color: #25262b !important;
    text-shadow: none !important;
  }
  /* 引用 */
  .w-e-text-container [data-slate-editor] blockquote {
    border-left: 4px solid var(--default-border-dashed) !important;
    background-color: var(--art-color);
  }
  .editor-wrapper {
    .w-e-text-container [data-slate-editor] .table-container th:last-of-type {
      border-right: 1px solid var(--default-border-dashed) !important;
    }
    .w-e-modal {
      background-color: var(--art-color);
    }
  }
}
rsf-design/src/assets/styles/core/el-dark.scss
New file
@@ -0,0 +1,2 @@
// 导入暗黑主题
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;
rsf-design/src/assets/styles/core/el-light.scss
New file
@@ -0,0 +1,34 @@
// https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/common/var.scss
// 自定义Element 亮色主题
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
  $colors: (
    'white': #ffffff,
    'black': #000000,
    'success': (
      'base': #13deb9
    ),
    'warning': (
      'base': #ffae1f
    ),
    'danger': (
      'base': #ff4d4f
    ),
    'error': (
      'base': #fa896b
    )
  ),
  $button: (
    'hover-bg-color': var(--el-color-primary-light-9),
    'hover-border-color': var(--el-color-primary),
    'border-color': var(--el-color-primary),
    'text-color': var(--el-color-primary)
  ),
  $messagebox: (
    'border-radius': '12px'
  ),
  $popover: (
    'padding': '14px',
    'border-radius': '10px'
  )
);
rsf-design/src/assets/styles/core/el-ui.scss
New file
@@ -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、dropdown 的三角
.el-select__popper,
.el-dropdown__popper {
  margin-top: -6px !important;
  .el-popper__arrow {
    display: none;
  }
}
.el-dropdown-selfdefine:focus {
  outline: none !important;
}
// 处理移动端组件兼容性
@media screen and (max-width: 640px) {
  .el-message-box,
  .el-dialog {
    width: calc(100% - 24px) !important;
  }
  .el-date-picker.has-sidebar.has-time {
    width: calc(100% - 24px);
    left: 12px !important;
  }
  .el-picker-panel *[slot='sidebar'],
  .el-picker-panel__sidebar {
    display: none;
  }
  .el-picker-panel *[slot='sidebar'] + .el-picker-panel__body,
  .el-picker-panel__sidebar + .el-picker-panel__body {
    margin-left: 0;
  }
}
// 修改el-button样式
.el-button {
  &.el-button--text {
    background-color: transparent !important;
    padding: 0 !important;
    span {
      margin-left: 0 !important;
    }
  }
}
// 修改el-tag样式
.el-tag {
  font-weight: 500;
  transition: all 0s !important;
  &.el-tag--default {
    height: 26px !important;
  }
}
.el-checkbox-group {
  &.el-table-filter__checkbox-group label.el-checkbox {
    height: 17px !important;
    .el-checkbox__label {
      font-weight: 400 !important;
    }
  }
}
.el-radio--default {
  // 优化单选按钮大小
  .el-radio__input {
    .el-radio__inner {
      width: 16px;
      height: 16px;
      &::after {
        width: 6px;
        height: 6px;
      }
    }
  }
}
.el-checkbox {
  .el-checkbox__inner {
    border-radius: 2px !important;
  }
}
// 优化复选框样式
.el-checkbox--default {
  .el-checkbox__inner {
    width: 16px !important;
    height: 16px !important;
    border-radius: 4px !important;
    &::before {
      content: '';
      height: 4px !important;
      top: 5px !important;
      background-color: #fff !important;
      transform: scale(0.6) !important;
    }
  }
  .is-checked {
    .el-checkbox__inner {
      &::after {
        width: 3px;
        height: 8px;
        margin: auto;
        border: 2px solid var(--el-checkbox-checked-icon-color);
        border-left: 0;
        border-top: 0;
        transform: translate(-45%, -60%) rotate(45deg) scale(0.86) !important;
        transform-origin: center;
      }
    }
  }
}
.el-notification .el-notification__icon {
  font-size: 22px !important;
}
// 修改 el-message-box 样式
.el-message-box__headerbtn .el-message-box__close,
.el-dialog__headerbtn .el-dialog__close {
  top: 7px;
  right: 7px;
  width: 30px;
  height: 30px;
  border-radius: 5px;
  transition: all 0.3s;
  &:hover {
    background-color: var(--art-hover-color) !important;
    color: var(--art-gray-900) !important;
  }
}
.el-message-box {
  padding: 25px 20px !important;
}
.el-message-box__title {
  font-weight: 500 !important;
}
.el-table__column-filter-trigger i {
  color: var(--theme-color) !important;
  margin: -3px 0 0 2px;
}
// 去除 el-dropdown 鼠标放上去出现的边框
.el-tooltip__trigger:focus-visible {
  outline: unset;
}
// ipad 表单右侧按钮优化
@media screen and (max-width: 1180px) {
  .el-table-fixed-column--right {
    padding-right: 0 !important;
  }
}
.login-out-dialog {
  padding: 30px 20px !important;
  border-radius: 10px !important;
}
// 修改 dialog 动画
.dialog-fade-enter-active {
  .el-dialog:not(.is-draggable) {
    animation: dialog-open 0.3s cubic-bezier(0.32, 0.14, 0.15, 0.86);
    // 修复 el-dialog 动画后宽度不自适应问题
    .el-select__selected-item {
      display: inline-block;
    }
  }
}
.dialog-fade-leave-active {
  animation: fade-out 0.2s linear;
  .el-dialog:not(.is-draggable) {
    animation: dialog-close 0.5s;
  }
}
@keyframes dialog-open {
  0% {
    opacity: 0;
    transform: scale(0.2);
  }
  100% {
    opacity: 1;
    transform: scale(1);
  }
}
@keyframes dialog-close {
  0% {
    opacity: 1;
    transform: scale(1);
  }
  100% {
    opacity: 0;
    transform: scale(0.2);
  }
}
// 遮罩层动画
@keyframes fade-out {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}
// 修改 el-select 样式
.el-select__popper:not(.el-tree-select__popper) {
  .el-select-dropdown__list {
    padding: 5px !important;
    .el-select-dropdown__item {
      height: 34px !important;
      line-height: 34px !important;
      border-radius: 6px !important;
      &.is-selected {
        color: var(--art-gray-900) !important;
        font-weight: 400 !important;
        background-color: var(--art-el-active-color) !important;
        margin-bottom: 4px !important;
      }
      &:hover {
        background-color: var(--art-hover-color) !important;
      }
    }
    .el-select-dropdown__item:hover ~ .is-selected,
    .el-select-dropdown__item.is-selected:has(~ .el-select-dropdown__item:hover) {
      background-color: transparent !important;
    }
  }
}
// 修改 el-tree-select 样式
.el-tree-select__popper {
  .el-select-dropdown__list {
    padding: 5px !important;
    .el-tree-node {
      .el-tree-node__content {
        height: 36px !important;
        border-radius: 6px !important;
        &:hover {
          background-color: var(--art-gray-200) !important;
        }
      }
    }
  }
}
// 实现水波纹在文字下面效果
.el-button > span {
  position: relative;
  z-index: 10;
}
// 优化颜色选择器圆角
.el-color-picker__color {
  border-radius: 2px !important;
}
// 优化日期时间选择器底部圆角
.el-picker-panel {
  .el-picker-panel__footer {
    border-radius: 0 0 var(--el-border-radius-base) var(--el-border-radius-base);
  }
}
// 优化树型菜单样式
.el-tree-node__content {
  border-radius: 4px;
  margin-bottom: 4px;
  padding: 1px 0;
  &:hover {
    background-color: var(--art-hover-color) !important;
  }
}
.dark {
  .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
    background-color: var(--art-gray-300) !important;
  }
}
// 隐藏折叠菜单弹窗 hover 出现的边框
.menu-left-popper:focus-within,
.horizontal-menu-popper:focus-within {
  box-shadow: none !important;
  outline: none !important;
}
// 数字输入组件右侧按钮高度跟随自定义组件高度
.el-input-number--default.is-controls-right {
  .el-input-number__decrease,
  .el-input-number__increase {
    height: calc((var(--el-component-size) / 2)) !important;
  }
}
rsf-design/src/assets/styles/core/md.scss
New file
@@ -0,0 +1,1036 @@
/* 文章标题设置(h1-h6)*/
/* ------------------------------------------------ */
$font-color: #24292e;
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
  color: var(--art-gray-800) !important;
  margin: 30px 0 10px 0;
  font-weight: 600;
}
.markdown-body h1 {
  font-size: 30px;
}
@media only screen and (max-width: 550px) {
  .markdown-body h1 {
    font-size: 26px;
  }
  .markdown-body h2 {
    font-size: 22px;
  }
  .markdown-body h3 {
    font-size: 18px;
  }
}
/* 块引用 */
/* ------------------------------------------------ */
.markdown-body blockquote {
  color: rgba(60, 60, 67, 0.7);
  font-size: 15px !important;
  border-left: 0.18em solid #e7e7e8;
  background: #f8f8f8;
  padding: 15px 1em;
  font-weight: 400 !important;
}
/* 详情页文章字体颜色 */
/* ------------------------------------------------ */
.markdown-body p {
  line-height: 28px;
  margin-bottom: 10px;
}
.markdown-body li,
.markdown-body p {
  color: var(--art-gray-800) !important;
  font-size: 16px !important;
}
.dark .markdown-body li span {
  color: var(--art-gray-800) !important;
  background-color: transparent !important;
}
.dark .markdown-body p span {
  color: var(--art-gray-800) !important;
  background-color: transparent !important;
}
.line-numbers-mode {
  background-color: var(--art-code-bg);
  border-radius: 8px;
  position: relative;
  padding-left: 32px;
  box-sizing: border-box;
}
.line-numbers-mode pre {
  flex: 1;
  border-radius: 0 8px 8px 0;
  background-color: var(--art-code-bg);
}
.line-numbers-mode .line-numbers-wrapper {
  width: 32px;
  height: 100%;
  text-align: center;
  padding: 16px 0;
  box-sizing: border-box;
  border-right: 1px solid #000000;
  position: absolute;
  left: 0;
  top: 0;
}
.line-numbers-mode .line-numbers-wrapper span {
  height: 23.6px;
  line-height: 23.6px;
  display: block;
  color: #72747b;
  font-size: 13px;
  box-sizing: border-box;
}
.line-numbers-mode .copy-btn {
  display: inline-block;
  display: flex;
  position: absolute;
  right: 10px;
  top: 10px;
  cursor: pointer;
  opacity: 0;
  background-color: #000;
  border-radius: 5px;
  text-align: center;
  color: rgba(255, 255, 255, 0.6);
  transition: opacity 0.3s;
}
.line-numbers-mode .copy-btn div {
  width: 34px;
  height: 34px;
  line-height: 34px;
  cursor: pointer;
  text-align: center;
  font-size: 20px;
}
.line-numbers-mode:hover .copy-btn {
  opacity: 1;
}
.line-numbers-mode .copy-btn span {
  height: 34px;
  line-height: 34px;
  font-size: 13px;
  padding-left: 10px;
  display: none;
}
.line-numbers-mode .copy-btn .show-copy {
  opacity: 1;
  display: block;
}
.line-numbers-mode ::-webkit-scrollbar-track {
  background-color: #292b30 !important;
}
.markdown-body .anchor {
  float: left;
  line-height: 1;
  margin-left: -20px;
  padding-right: 4px;
}
.markdown-body .anchor:focus {
  outline: none;
}
.markdown-body h1 .octicon-link,
.markdown-body h2 .octicon-link,
.markdown-body h3 .octicon-link,
.markdown-body h4 .octicon-link,
.markdown-body h5 .octicon-link,
.markdown-body h6 .octicon-link {
  color: #1b1f23;
  vertical-align: middle;
  visibility: hidden;
}
.markdown-body h1:hover .anchor,
.markdown-body h2:hover .anchor,
.markdown-body h3:hover .anchor,
.markdown-body h4:hover .anchor,
.markdown-body h5:hover .anchor,
.markdown-body h6:hover .anchor {
  text-decoration: none;
}
.markdown-body h1:hover .anchor .octicon-link,
.markdown-body h2:hover .anchor .octicon-link,
.markdown-body h3:hover .anchor .octicon-link,
.markdown-body h4:hover .anchor .octicon-link,
.markdown-body h5:hover .anchor .octicon-link,
.markdown-body h6:hover .anchor .octicon-link {
  visibility: visible;
}
.markdown-body h1:hover .anchor .octicon-link:before,
.markdown-body h2:hover .anchor .octicon-link:before,
.markdown-body h3:hover .anchor .octicon-link:before,
.markdown-body h4:hover .anchor .octicon-link:before,
.markdown-body h5:hover .anchor .octicon-link:before,
.markdown-body h6:hover .anchor .octicon-link:before {
  width: 16px;
  height: 16px;
  content: ' ';
  display: inline-block;
}
.markdown-body {
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
  line-height: 1.5;
  color: $font-color;
  font-size: 16px;
  line-height: 1.5;
  word-wrap: break-word;
}
.markdown-body details {
  display: block;
}
.markdown-body summary {
  display: list-item;
}
.markdown-body a {
  background-color: initial;
}
.markdown-body a:active,
.markdown-body a:hover {
  outline-width: 0;
}
.markdown-body strong {
  font-weight: inherit;
  font-weight: bolder;
}
.markdown-body p br {
  display: inline;
  line-height: 11px;
}
.markdown-body img {
  border-style: none;
}
.markdown-body hr {
  box-sizing: initial;
  height: 0;
  overflow: visible;
}
.markdown-body input {
  font: inherit;
  margin: 0;
}
.markdown-body input {
  overflow: visible;
}
.markdown-body [type='checkbox'] {
  box-sizing: border-box;
  padding: 0;
}
.markdown-body * {
  box-sizing: border-box;
}
.markdown-body input {
  font-size: inherit;
  line-height: inherit;
}
.markdown-body a {
  color: #0366d6;
  text-decoration: none;
}
.markdown-body a:hover {
  text-decoration: underline;
}
.markdown-body strong {
  font-weight: 600;
}
.markdown-body hr {
  height: 0;
  margin: 15px 0;
  overflow: hidden;
  background: transparent;
  border: 0;
  border-bottom: 1px solid #dfe2e5;
}
.markdown-body hr:after,
.markdown-body hr:before {
  display: table;
  content: '';
}
.markdown-body hr:after {
  clear: both;
}
.markdown-body table {
  border-spacing: 0;
  border-collapse: collapse;
}
.markdown-body td,
.markdown-body th {
  padding: 0;
}
.markdown-body details summary {
  cursor: pointer;
}
.markdown-body kbd {
  display: inline-block;
  padding: 3px 5px;
  font:
    11px SFMono-Regular,
    Consolas,
    Liberation Mono,
    Menlo,
    monospace;
  line-height: 10px;
  color: #444d56;
  vertical-align: middle;
  background-color: #fafbfc;
  border: 1px solid #d1d5da;
  border-radius: 3px;
  box-shadow: inset 0 -1px 0 #d1d5da;
}
.markdown-body blockquote {
  margin: 0;
}
.markdown-body ol,
.markdown-body ul {
  padding-left: 0;
  margin-top: 0;
  margin-bottom: 0;
}
.markdown-body ol ol,
.markdown-body ul ol {
  list-style-type: lower-roman;
}
.markdown-body ol ol ol,
.markdown-body ol ul ol,
.markdown-body ul ol ol,
.markdown-body ul ul ol {
  list-style-type: lower-alpha;
}
.markdown-body dd {
  margin-left: 0;
}
.markdown-body code,
.markdown-body pre,
.markdown-body .line-number {
  font-size: 14px !important;
  border-radius: 8px;
  background-color: #282c34;
}
.dark {
  .markdown-body code,
  .markdown-body pre,
  .markdown-body .line-number {
    background-color: #252525;
  }
}
.markdown-body pre {
  margin-top: 0;
  margin-bottom: 0;
}
.markdown-body input::-webkit-inner-spin-button,
.markdown-body input::-webkit-outer-spin-button {
  margin: 0;
  -webkit-appearance: none;
  appearance: none;
}
.markdown-body :checked + .radio-label {
  position: relative;
  z-index: 1;
  border-color: #0366d6;
}
.markdown-body .border {
  border: 1px solid #e1e4e8 !important;
}
.markdown-body .border-0 {
  border: 0 !important;
}
.markdown-body .border-bottom {
  border-bottom: 1px solid #e1e4e8 !important;
}
.markdown-body .rounded-1 {
  border-radius: 3px !important;
}
.markdown-body .bg-white {
  background-color: #fff !important;
}
.markdown-body .bg-gray-light {
  background-color: #fafbfc !important;
}
.markdown-body .text-gray-light {
  color: #6a737d !important;
}
.markdown-body .mb-0 {
  margin-bottom: 0 !important;
}
.markdown-body .my-2 {
  margin-top: 8px !important;
  margin-bottom: 8px !important;
}
.markdown-body .pl-0 {
  padding-left: 0 !important;
}
.markdown-body .py-0 {
  padding-top: 0 !important;
  padding-bottom: 0 !important;
}
.markdown-body .pl-1 {
  padding-left: 4px !important;
}
.markdown-body .pl-2 {
  padding-left: 8px !important;
}
.markdown-body .py-2 {
  padding-top: 8px !important;
  padding-bottom: 8px !important;
}
.markdown-body .pl-3,
.markdown-body .px-3 {
  padding-left: 16px !important;
}
.markdown-body .px-3 {
  padding-right: 16px !important;
}
.markdown-body .pl-4 {
  padding-left: 24px !important;
}
.markdown-body .pl-5 {
  padding-left: 32px !important;
}
.markdown-body .pl-6 {
  padding-left: 40px !important;
}
.markdown-body .f6 {
  font-size: 12px !important;
}
.markdown-body .lh-condensed {
  line-height: 1.25 !important;
}
.markdown-body .text-bold {
  font-weight: 600 !important;
}
.markdown-body .pl-c {
  color: #6a737d;
}
.markdown-body .pl-c1,
.markdown-body .pl-s .pl-v {
  color: #005cc5;
}
.markdown-body .pl-e,
.markdown-body .pl-en {
  color: #6f42c1;
}
.markdown-body .pl-s .pl-s1,
.markdown-body .pl-smi {
  color: $font-color;
}
.markdown-body .pl-ent {
  color: #22863a;
}
.markdown-body .pl-k {
  color: #d73a49;
}
.markdown-body .pl-pds,
.markdown-body .pl-s,
.markdown-body .pl-s .pl-pse .pl-s1,
.markdown-body .pl-sr,
.markdown-body .pl-sr .pl-cce,
.markdown-body .pl-sr .pl-sra,
.markdown-body .pl-sr .pl-sre {
  color: #032f62;
}
.markdown-body .pl-smw,
.markdown-body .pl-v {
  color: #e36209;
}
.markdown-body .pl-bu {
  color: #b31d28;
}
.markdown-body .pl-ii {
  color: #fafbfc;
  background-color: #b31d28;
}
.markdown-body .pl-c2 {
  color: #fafbfc;
  background-color: #d73a49;
}
.markdown-body .pl-c2:before {
  content: '^M';
}
.markdown-body .pl-sr .pl-cce {
  font-weight: 700;
  color: #22863a;
}
.markdown-body .pl-ml {
  color: #735c0f;
}
.markdown-body .pl-mh,
.markdown-body .pl-mh .pl-en,
.markdown-body .pl-ms {
  font-weight: 700;
  color: #005cc5;
}
.markdown-body .pl-mi {
  font-style: italic;
  color: $font-color;
}
.markdown-body .pl-mb {
  font-weight: 700;
  color: $font-color;
}
.markdown-body .pl-md {
  color: #b31d28;
  background-color: #ffeef0;
}
.markdown-body .pl-mi1 {
  color: #22863a;
  background-color: #f0fff4;
}
.markdown-body .pl-mc {
  color: #e36209;
  background-color: #ffebda;
}
.markdown-body .pl-mi2 {
  color: #f6f8fa;
  background-color: #005cc5;
}
.markdown-body .pl-mdr {
  font-weight: 700;
  color: #6f42c1;
}
.markdown-body .pl-ba {
  color: #586069;
}
.markdown-body .pl-sg {
  color: #959da5;
}
.markdown-body .pl-corl {
  text-decoration: underline;
  color: #032f62;
}
.markdown-body .mb-0 {
  margin-bottom: 0 !important;
}
.markdown-body .my-2 {
  margin-bottom: 8px !important;
}
.markdown-body .my-2 {
  margin-top: 8px !important;
}
.markdown-body .pl-0 {
  padding-left: 0 !important;
}
.markdown-body .py-0 {
  padding-top: 0 !important;
  padding-bottom: 0 !important;
}
.markdown-body .pl-1 {
  padding-left: 4px !important;
}
.markdown-body .pl-2 {
  padding-left: 8px !important;
}
.markdown-body .py-2 {
  padding-top: 8px !important;
  padding-bottom: 8px !important;
}
.markdown-body .pl-3 {
  padding-left: 16px !important;
}
.markdown-body .pl-4 {
  padding-left: 24px !important;
}
.markdown-body .pl-5 {
  padding-left: 32px !important;
}
.markdown-body .pl-6 {
  padding-left: 40px !important;
}
.markdown-body .pl-7 {
  padding-left: 48px !important;
}
.markdown-body .pl-8 {
  padding-left: 64px !important;
}
.markdown-body .pl-9 {
  padding-left: 80px !important;
}
.markdown-body .pl-10 {
  padding-left: 96px !important;
}
.markdown-body .pl-11 {
  padding-left: 112px !important;
}
.markdown-body .pl-12 {
  padding-left: 128px !important;
}
.markdown-body hr {
  border-bottom-color: #eee;
}
.markdown-body kbd {
  display: inline-block;
  padding: 3px 5px;
  font:
    11px SFMono-Regular,
    Consolas,
    Liberation Mono,
    Menlo,
    monospace;
  line-height: 10px;
  color: #444d56;
  vertical-align: middle;
  background-color: #fafbfc;
  border: 1px solid #d1d5da;
  border-radius: 3px;
  box-shadow: inset 0 -1px 0 #d1d5da;
}
.markdown-body:after,
.markdown-body:before {
  display: table;
  content: '';
}
.markdown-body:after {
  clear: both;
}
.markdown-body > :first-child {
  margin-top: 0 !important;
}
.markdown-body > :last-child {
  margin-bottom: 0 !important;
}
.markdown-body a:not([href]) {
  color: inherit;
  text-decoration: none;
}
.markdown-body blockquote,
.markdown-body details,
.markdown-body dl,
.markdown-body ol,
.markdown-body pre,
.markdown-body table,
.markdown-body ul {
  margin-top: 0;
  margin-bottom: 16px;
}
.markdown-body hr {
  height: 0.25em;
  padding: 0;
  margin: 24px 0;
  background-color: #e1e4e8;
  border: 0;
}
.markdown-body blockquote > :first-child {
  margin-top: 0;
}
.markdown-body blockquote > :last-child {
  margin-bottom: 0;
}
.markdown-body ol,
.markdown-body ul {
  padding-left: 1em;
}
.markdown-body ol ol,
.markdown-body ol ul,
.markdown-body ul ol,
.markdown-body ul ul {
  margin-top: 0;
  margin-bottom: 0;
}
.markdown-body li {
  line-height: 28px;
  font-size: 14px;
  word-wrap: break-all;
  list-style: disc;
  margin-left: 10px;
}
.markdown-body li > p {
  margin-top: 16px;
}
.markdown-body li + li {
  margin-top: 0.25em;
}
.markdown-body dl {
  padding: 0;
}
.markdown-body dl dt {
  padding: 0;
  margin-top: 16px;
  font-size: 1em;
  font-style: italic;
  font-weight: 600;
}
.markdown-body dl dd {
  padding: 0 16px;
  margin-bottom: 16px;
}
.markdown-body table {
  display: block;
  width: 100%;
  overflow: auto;
}
.markdown-body table th {
  font-weight: 600;
}
.markdown-body table td,
.markdown-body table th {
  padding: 6px 13px;
  border: 1px solid #dfe2e5;
}
.markdown-body table tr {
  background-color: #fff;
  border-top: 1px solid #c6cbd1;
}
.markdown-body table tr:nth-child(2n) {
  background-color: #f6f8fa;
}
.markdown-body img {
  max-width: 100%;
  box-sizing: initial;
  background-color: #fff;
  border: 1px solid #eee;
  border: 1px solid var(--art-c-border-2);
  cursor: zoom-in;
}
.markdown-body img[align='right'] {
  padding-left: 20px;
}
.markdown-body img[align='left'] {
  padding-right: 20px;
}
.markdown-body code {
  padding: 0.2em 0.4em;
  margin: 0;
  font-size: 85%;
  background-color: rgba(27, 31, 35, 0.05);
  border-radius: 3px;
}
.markdown-body pre {
  word-wrap: normal;
}
.markdown-body pre > code {
  padding: 0;
  margin: 0;
  font-size: 100%;
  word-break: normal;
  white-space: pre;
  background: transparent;
  border: 0;
}
.markdown-body .highlight {
  margin-bottom: 16px;
}
.markdown-body .highlight pre {
  margin-bottom: 0;
  word-break: normal;
}
.markdown-body .highlight pre,
.markdown-body pre {
  padding: 15px 20px 15px 0;
  overflow: auto;
  font-size: 92%;
  line-height: 1.6;
}
.markdown-body pre code {
  display: inline;
  max-width: auto;
  padding: 0;
  margin: 0;
  overflow: visible;
  line-height: inherit;
  word-wrap: normal;
  background-color: initial;
  border: 0;
}
.markdown-body .commit-tease-sha {
  display: inline-block;
  font-size: 90%;
  color: #444d56;
}
.markdown-body .full-commit .btn-outline:not(:disabled):hover {
  color: #005cc5;
  border-color: #005cc5;
}
.markdown-body .blob-wrapper {
  overflow-x: auto;
  overflow-y: hidden;
}
.markdown-body .blob-wrapper-embedded {
  max-height: 240px;
  overflow-y: auto;
}
.markdown-body .blob-num {
  width: 1%;
  min-width: 50px;
  padding-right: 10px;
  padding-left: 10px;
  font-size: 12px;
  line-height: 20px;
  color: rgba(27, 31, 35, 0.3);
  text-align: right;
  white-space: nowrap;
  vertical-align: top;
  cursor: pointer;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}
.markdown-body .blob-num:hover {
  color: rgba(27, 31, 35, 0.6);
}
.markdown-body .blob-num:before {
  content: attr(data-line-number);
}
.markdown-body .blob-code {
  position: relative;
  padding-right: 10px;
  padding-left: 10px;
  line-height: 20px;
  vertical-align: top;
}
.markdown-body .blob-code-inner {
  overflow: visible;
  font-size: 12px;
  color: $font-color;
  word-wrap: normal;
  white-space: pre;
}
.markdown-body .pl-token.active,
.markdown-body .pl-token:hover {
  cursor: pointer;
  background: #ffea7f;
}
.markdown-body .tab-size[data-tab-size='1'] {
  -moz-tab-size: 1;
  tab-size: 1;
}
.markdown-body .tab-size[data-tab-size='2'] {
  -moz-tab-size: 2;
  tab-size: 2;
}
.markdown-body .tab-size[data-tab-size='3'] {
  -moz-tab-size: 3;
  tab-size: 3;
}
.markdown-body .tab-size[data-tab-size='4'] {
  -moz-tab-size: 4;
  tab-size: 4;
}
.markdown-body .tab-size[data-tab-size='5'] {
  -moz-tab-size: 5;
  tab-size: 5;
}
.markdown-body .tab-size[data-tab-size='6'] {
  -moz-tab-size: 6;
  tab-size: 6;
}
.markdown-body .tab-size[data-tab-size='7'] {
  -moz-tab-size: 7;
  tab-size: 7;
}
.markdown-body .tab-size[data-tab-size='8'] {
  -moz-tab-size: 8;
  tab-size: 8;
}
.markdown-body .tab-size[data-tab-size='9'] {
  -moz-tab-size: 9;
  tab-size: 9;
}
.markdown-body .tab-size[data-tab-size='10'] {
  -moz-tab-size: 10;
  tab-size: 10;
}
.markdown-body .tab-size[data-tab-size='11'] {
  -moz-tab-size: 11;
  tab-size: 11;
}
.markdown-body .tab-size[data-tab-size='12'] {
  -moz-tab-size: 12;
  tab-size: 12;
}
.markdown-body .task-list-item {
  list-style-type: none;
}
.markdown-body .task-list-item + .task-list-item {
  margin-top: 3px;
}
.markdown-body .task-list-item input {
  margin: 0 0.2em 0.25em -1.6em;
  vertical-align: middle;
}
rsf-design/src/assets/styles/core/mixin.scss
New file
@@ -0,0 +1,157 @@
// sass 混合宏(函数)
/**
* 溢出省略号
* @param {Number} 行数
*/
@mixin ellipsis($rowCount: 1) {
  @if $rowCount <=1 {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  } @else {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    display: -webkit-box;
    -webkit-line-clamp: $rowCount;
    -webkit-box-orient: vertical;
  }
}
/**
* 控制用户能否选中文本
* @param {String} 类型
*/
@mixin userSelect($value: none) {
  user-select: $value;
  -moz-user-select: $value;
  -ms-user-select: $value;
  -webkit-user-select: $value;
}
// 绝对定位居中
@mixin absoluteCenter() {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  margin: auto;
}
/**
* css3动画
*
*/
@mixin animation(
  $from: (
    width: 0px
  ),
  $to: (
    width: 100px
  ),
  $name: mymove,
  $animate: mymove 2s 1 linear infinite
) {
  -webkit-animation: $animate;
  -o-animation: $animate;
  animation: $animate;
  @keyframes #{$name} {
    from {
      @each $key, $value in $from {
        #{$key}: #{$value};
      }
    }
    to {
      @each $key, $value in $to {
        #{$key}: #{$value};
      }
    }
  }
  @-webkit-keyframes #{$name} {
    from {
      @each $key, $value in $from {
        $key: $value;
      }
    }
    to {
      @each $key, $value in $to {
        $key: $value;
      }
    }
  }
}
// 圆形盒子
@mixin circle($size: 11px, $bg: #fff) {
  border-radius: 50%;
  width: $size;
  height: $size;
  line-height: $size;
  text-align: center;
  background: $bg;
}
// placeholder
@mixin placeholder($color: #bbb) {
  // Firefox
  &::-moz-placeholder {
    color: $color;
    opacity: 1;
  }
  // Internet Explorer 10+
  &:-ms-input-placeholder {
    color: $color;
  }
  // Safari and Chrome
  &::-webkit-input-placeholder {
    color: $color;
  }
  &:placeholder-shown {
    text-overflow: ellipsis;
  }
}
//背景透明,文字不透明。兼容IE8
@mixin betterTransparentize($color, $alpha) {
  $c: rgba($color, $alpha);
  $ie_c: ie_hex_str($c);
  background: rgba($color, 1);
  background: $c;
  background: transparent \9;
  zoom: 1;
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c});
  -ms-filter: 'progid:DXImageTransform.Microsoft.gradient(startColorstr=#{$ie_c}, endColorstr=#{$ie_c})';
}
//添加浏览器前缀
@mixin browserPrefix($propertyName, $value) {
  @each $prefix in -webkit-, -moz-, -ms-, -o-, '' {
    #{$prefix}#{$propertyName}: $value;
  }
}
// 边框
@mixin border($color: red) {
  border: 1px solid $color;
}
// 背景滤镜
@mixin backdropBlur() {
  --tw-backdrop-blur: blur(30px);
  -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness)
    var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate)
    var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate)
    var(--tw-backdrop-sepia);
  backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast)
    var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert)
    var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);
}
rsf-design/src/assets/styles/core/reset.scss
New file
@@ -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;
  }
}
rsf-design/src/assets/styles/core/router-transition.scss
New file
@@ -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');
}
rsf-design/src/assets/styles/core/tailwind.css
New file
@@ -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;
        }
      }
    }
  }
}
rsf-design/src/assets/styles/core/theme-animation.scss
New file
@@ -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);
}
rsf-design/src/assets/styles/core/theme-change.scss
New file
@@ -0,0 +1,11 @@
// 主题切换过渡优化,优化除视觉上的不适感
.theme-change {
  * {
    transition: 0s !important;
  }
  .el-switch__core,
  .el-switch__action {
    transition: all 0.3s !important;
  }
}
rsf-design/src/assets/styles/custom/one-dark-pro.scss
New file
@@ -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;
}
rsf-design/src/assets/styles/index.scss
New file
@@ -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';
rsf-design/src/assets/svg/loading.js
New file
@@ -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
};
rsf-design/src/components/core/banners/art-basic-banner/index.vue
New file
@@ -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>
rsf-design/src/components/core/banners/art-card-banner/index.vue
New file
@@ -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>
rsf-design/src/components/core/base/art-back-to-top/index.vue
New file
@@ -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>
rsf-design/src/components/core/base/art-logo/index.vue
New file
@@ -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>
rsf-design/src/components/core/base/art-svg-icon/index.vue
New file
@@ -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>
rsf-design/src/components/core/cards/art-bar-chart-card/index.vue
New file
@@ -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>
rsf-design/src/components/core/cards/art-data-list-card/index.vue
New file
@@ -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>
rsf-design/src/components/core/cards/art-donut-chart-card/index.vue
New file
@@ -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>
rsf-design/src/components/core/cards/art-image-card/index.vue
New file
@@ -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>
rsf-design/src/components/core/cards/art-line-chart-card/index.vue
New file
@@ -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>
rsf-design/src/components/core/cards/art-progress-card/index.vue
New file
@@ -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>
rsf-design/src/components/core/cards/art-stats-card/index.vue
New file
@@ -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>
rsf-design/src/components/core/cards/art-timeline-list-card/index.vue
New file
@@ -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>
rsf-design/src/components/core/charts/art-bar-chart/index.vue
New file
@@ -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>
rsf-design/src/components/core/charts/art-dual-bar-compare-chart/index.vue
New file
@@ -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: '正向数据' },
    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))
          }),
          // 正向数据系列
          createSeriesConfig({
            name: props.positiveName,
            data: props.positiveData,
            borderRadius: props.positiveBorderRadius,
            labelPosition: 'top',
            colorIndex: 0
          })
        ]
      }
      return options
    }
  })
</script>
rsf-design/src/components/core/charts/art-h-bar-chart/index.vue
New file
@@ -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>
rsf-design/src/components/core/charts/art-k-line-chart/index.vue
New file
@@ -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>
rsf-design/src/components/core/charts/art-line-chart/index.vue
New file
@@ -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>
rsf-design/src/components/core/charts/art-radar-chart/index.vue
New file
@@ -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>
rsf-design/src/components/core/charts/art-ring-chart/index.vue
New file
@@ -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>
rsf-design/src/components/core/charts/art-scatter-chart/index.vue
New file
@@ -0,0 +1,101 @@
<!-- 散点图 -->
<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>
rsf-design/src/components/core/forms/art-button-more/index.vue
New file
@@ -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>
rsf-design/src/components/core/forms/art-button-table/index.vue
New file
@@ -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>
rsf-design/src/components/core/forms/art-drag-verify/index.vue
New file
@@ -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>
rsf-design/src/components/core/forms/art-excel-export/index.vue
New file
@@ -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>
rsf-design/src/components/core/forms/art-excel-import/index.vue
New file
@@ -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>
rsf-design/src/components/core/forms/art-form/index.vue
New file
@@ -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(/&nbsp;/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>
rsf-design/src/components/core/forms/art-search-bar/index.vue
New file
@@ -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(/&nbsp;/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>
rsf-design/src/components/core/forms/art-wang-editor/index.vue
New file
@@ -0,0 +1,179 @@
<!-- WangEditor 富文本编辑器 插件地址:https://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>
rsf-design/src/components/core/forms/art-wang-editor/style.scss
New file
@@ -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;
  }
  /* 下拉选择框 正文文字大小调整 */
  .w-e-select-list ul li:last-of-type {
    font-size: 16px !important;
  }
  /* 下拉选择框 hover 样式调整 */
  .w-e-select-list ul li:hover {
    background-color: var(--art-gray-200);
  }
  :root {
    /* 激活颜色 */
    --w-e-toolbar-active-bg-color: var(--art-gray-200);
    /* toolbar 图标和文字颜色 */
    --w-e-toolbar-color: #000;
    /* 表格选中时候的边框颜色 */
    --w-e-textarea-selected-border-color: #ddd;
    /* 表格头背景颜色 */
    --w-e-textarea-slight-bg-color: var(--art-gray-200);
  }
  /* 工具栏按钮样式 */
  .w-e-bar-item svg {
    fill: var(--art-gray-800);
  }
  .w-e-bar-item button {
    color: var(--art-gray-800);
    border-radius: $box-radius;
  }
  /* 工具栏 hover 按钮背景颜色 */
  .w-e-bar-item button:hover {
    background-color: var(--art-gray-200);
  }
  /* 工具栏分割线 */
  .w-e-bar-divider {
    height: 20px;
    margin-top: 10px;
    background-color: #ccc;
  }
  /* 工具栏菜单 */
  .w-e-bar-item-group .w-e-bar-item-menus-container {
    min-width: 120px;
    padding: 10px 0;
    border: none;
    border-radius: $box-radius;
    .w-e-bar-item {
      button {
        width: 100%;
        margin: 0 5px;
      }
    }
  }
  /* 代码块 */
  .w-e-text-container [data-slate-editor] pre > code {
    padding: 0.6rem 1rem;
    background-color: var(--art-gray-50);
    border-radius: $box-radius;
  }
  /* 弹出框 */
  .w-e-drop-panel {
    border: 0;
    border-radius: $box-radius;
  }
  a {
    color: #318ef4;
  }
  .w-e-text-container {
    [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;
    }
  }
}
rsf-design/src/components/core/layouts/art-breadcrumb/index.vue
New file
@@ -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>
rsf-design/src/components/core/layouts/art-chat-window/index.vue
New file
@@ -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: '你好!我是你的AI助手,有什么我可以帮你的吗?',
      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: '太好了,那我如何开始使用呢?',
      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>
rsf-design/src/components/core/layouts/art-fast-enter/index.vue
New file
@@ -0,0 +1,89 @@
<!-- 顶部快速入口面板 -->
<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>
rsf-design/src/components/core/layouts/art-fireworks-effect/index.vue
New file
@@ -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 },
      // 正方形粒子尺寸
      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(),时间复杂度从O(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)
      }
    }
    /**
     * 更新画布尺寸缓存
     * 在窗口大小改变时调用
     * @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>
rsf-design/src/components/core/layouts/art-global-component/index.vue
New file
@@ -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>
rsf-design/src/components/core/layouts/art-global-search/index.vue
New file
@@ -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>
rsf-design/src/components/core/layouts/art-header-bar/index.vue
New file
@@ -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>
rsf-design/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue
New file
@@ -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>
rsf-design/src/components/core/layouts/art-menus/art-horizontal-menu/index.vue
New file
@@ -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>
rsf-design/src/components/core/layouts/art-menus/art-horizontal-menu/widget/HorizontalSubmenu.vue
New file
@@ -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>
rsf-design/src/components/core/layouts/art-menus/art-mixed-menu/index.vue
New file
@@ -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>
rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/index.vue
New file
@@ -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>
rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/style.scss
New file
@@ -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;
    }
    // 折叠状态下的header样式
    .menu-left-close .header {
      .logo {
        display: none;
      }
      p {
        left: 16px;
        font-size: 0;
        opacity: 0 !important;
      }
    }
    .menu-model {
      position: fixed;
      top: 0;
      left: 0;
      z-index: -1;
      display: block;
      width: 100%;
      height: 100vh;
      background: rgba($color: #000, $alpha: 50%);
      transition: opacity 0.2s ease-in-out;
    }
  }
}
@media only screen and (width <= 640px) {
  .layout-sidebar {
    border-right: 0 !important;
  }
}
.dark {
  .layout-sidebar {
    border-right: 1px solid rgb(255 255 255 / 13%);
    :deep(.el-scrollbar__thumb) {
      background-color: #777;
    }
    .dual-menu-left {
      border-right: 1px solid rgb(255 255 255 / 9%) !important;
    }
  }
}
rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/theme.scss
New file
@@ -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;
    }
  }
}
rsf-design/src/components/core/layouts/art-menus/art-sidebar-menu/widget/SidebarSubmenu.vue
New file
@@ -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>
rsf-design/src/components/core/layouts/art-notification/index.vue
New file
@@ -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>
rsf-design/src/components/core/layouts/art-page-content/index.vue
New file
@@ -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>
rsf-design/src/components/core/layouts/art-screen-lock/index.vue
New file
@@ -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>
    <!-- 解锁界面 -->
    <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('密码解密失败:', 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 truncated after the above file
rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsConfig.js rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsHandlers.js rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsPanel.js rsf-design/src/components/core/layouts/art-settings-panel/composables/useSettingsState.js rsf-design/src/components/core/layouts/art-settings-panel/index.vue rsf-design/src/components/core/layouts/art-settings-panel/style.scss rsf-design/src/components/core/layouts/art-settings-panel/widget/BasicSettings.vue rsf-design/src/components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue rsf-design/src/components/core/layouts/art-settings-panel/widget/ColorSettings.vue rsf-design/src/components/core/layouts/art-settings-panel/widget/ContainerSettings.vue rsf-design/src/components/core/layouts/art-settings-panel/widget/MenuLayoutSettings.vue rsf-design/src/components/core/layouts/art-settings-panel/widget/MenuStyleSettings.vue rsf-design/src/components/core/layouts/art-settings-panel/widget/SectionTitle.vue rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingActions.vue rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingDrawer.vue rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingHeader.vue rsf-design/src/components/core/layouts/art-settings-panel/widget/SettingItem.vue rsf-design/src/components/core/layouts/art-settings-panel/widget/ThemeSettings.vue rsf-design/src/components/core/layouts/art-work-tab/index.vue rsf-design/src/components/core/media/art-cutter-img/index.vue rsf-design/src/components/core/media/art-video-player/index.vue rsf-design/src/components/core/others/art-menu-right/index.vue rsf-design/src/components/core/others/art-watermark/index.vue rsf-design/src/components/core/tables/art-table-header/index.vue rsf-design/src/components/core/tables/art-table/index.vue rsf-design/src/components/core/tables/art-table/style.scss rsf-design/src/components/core/text-effect/art-count-to/index.vue rsf-design/src/components/core/text-effect/art-festival-text-scroll/index.vue rsf-design/src/components/core/text-effect/art-text-scroll/index.vue rsf-design/src/components/core/theme/theme-svg/index.vue rsf-design/src/components/core/views/exception/ArtException.vue rsf-design/src/components/core/views/login/AuthTopBar.vue rsf-design/src/components/core/views/login/LoginLeftView.vue rsf-design/src/components/core/views/result/ArtResultPage.vue rsf-design/src/components/core/widget/art-icon-button/index.vue rsf-design/src/config/assets/images.js rsf-design/src/config/index.js rsf-design/src/config/modules/component.js rsf-design/src/config/modules/fastEnter.js rsf-design/src/config/modules/festival.js rsf-design/src/config/modules/headerBar.js rsf-design/src/config/setting.js rsf-design/src/directives/business/highlight.js rsf-design/src/directives/business/ripple.js rsf-design/src/directives/core/auth.js rsf-design/src/directives/core/roles.js rsf-design/src/directives/index.js rsf-design/src/enums/appEnum.js rsf-design/src/enums/formEnum.js rsf-design/src/hooks/core/useAppMode.js rsf-design/src/hooks/core/useAuth.js rsf-design/src/hooks/core/useCeremony.js rsf-design/src/hooks/core/useChart.js rsf-design/src/hooks/core/useCommon.js rsf-design/src/hooks/core/useFastEnter.js rsf-design/src/hooks/core/useHeaderBar.js rsf-design/src/hooks/core/useLayoutHeight.js rsf-design/src/hooks/core/useTable.js rsf-design/src/hooks/core/useTableColumns.js rsf-design/src/hooks/core/useTableHeight.js rsf-design/src/hooks/core/useTheme.js rsf-design/src/hooks/index.js rsf-design/src/locales/index.js rsf-design/src/locales/langs/en.json rsf-design/src/locales/langs/zh.json rsf-design/src/main.js rsf-design/src/mock/temp/formData.js rsf-design/src/mock/upgrade/changeLog.js rsf-design/src/plugins/echarts.js rsf-design/src/plugins/iconify.collections.js rsf-design/src/plugins/iconify.js rsf-design/src/plugins/index.js rsf-design/src/router/core/ComponentLoader.js rsf-design/src/router/core/IframeRouteManager.js rsf-design/src/router/core/MenuProcessor.js rsf-design/src/router/core/RoutePermissionValidator.js rsf-design/src/router/core/RouteRegistry.js rsf-design/src/router/core/RouteTransformer.js rsf-design/src/router/core/RouteValidator.js rsf-design/src/router/core/index.js rsf-design/src/router/guards/afterEach.js rsf-design/src/router/guards/beforeEach.js rsf-design/src/router/index.js rsf-design/src/router/modules/dashboard.js rsf-design/src/router/modules/exception.js rsf-design/src/router/modules/index.js rsf-design/src/router/modules/result.js rsf-design/src/router/modules/system.js rsf-design/src/router/routes/asyncRoutes.js rsf-design/src/router/routes/staticRoutes.js rsf-design/src/router/routesAlias.js rsf-design/src/store/index.js rsf-design/src/store/modules/menu.js rsf-design/src/store/modules/setting.js rsf-design/src/store/modules/table.js rsf-design/src/store/modules/user.js rsf-design/src/store/modules/worktab.js rsf-design/src/utils/constants/index.js rsf-design/src/utils/constants/links.js rsf-design/src/utils/form/index.js rsf-design/src/utils/form/responsive.js rsf-design/src/utils/form/validator.js rsf-design/src/utils/http/error.js rsf-design/src/utils/http/index.js rsf-design/src/utils/http/status.js rsf-design/src/utils/index.js rsf-design/src/utils/navigation/index.js rsf-design/src/utils/navigation/jump.js rsf-design/src/utils/navigation/route.js rsf-design/src/utils/navigation/worktab.js rsf-design/src/utils/router.js rsf-design/src/utils/socket/index.js rsf-design/src/utils/storage/index.js rsf-design/src/utils/storage/storage-config.js rsf-design/src/utils/storage/storage-key-manager.js rsf-design/src/utils/storage/storage.js rsf-design/src/utils/sys/console.js rsf-design/src/utils/sys/error-handle.js rsf-design/src/utils/sys/index.js rsf-design/src/utils/sys/mittBus.js rsf-design/src/utils/sys/upgrade.js rsf-design/src/utils/table/tableCache.js rsf-design/src/utils/table/tableConfig.js rsf-design/src/utils/table/tableUtils.js rsf-design/src/utils/ui/animation.js rsf-design/src/utils/ui/colors.js rsf-design/src/utils/ui/emojo.js rsf-design/src/utils/ui/iconify-loader.js rsf-design/src/utils/ui/index.js rsf-design/src/utils/ui/loading.js rsf-design/src/utils/ui/tabs.js rsf-design/src/views/auth/forget-password/index.vue rsf-design/src/views/auth/login/index.vue rsf-design/src/views/auth/login/style.css rsf-design/src/views/auth/register/index.vue rsf-design/src/views/dashboard/console/index.vue rsf-design/src/views/dashboard/console/modules/about-project.vue rsf-design/src/views/dashboard/console/modules/active-user.vue rsf-design/src/views/dashboard/console/modules/card-list.vue rsf-design/src/views/dashboard/console/modules/dynamic-stats.vue rsf-design/src/views/dashboard/console/modules/new-user.vue rsf-design/src/views/dashboard/console/modules/sales-overview.vue rsf-design/src/views/dashboard/console/modules/todo-list.vue rsf-design/src/views/exception/403/index.vue rsf-design/src/views/exception/404/index.vue rsf-design/src/views/exception/500/index.vue rsf-design/src/views/index/index.vue rsf-design/src/views/index/style.scss rsf-design/src/views/outside/Iframe.vue rsf-design/src/views/result/fail/index.vue rsf-design/src/views/result/success/index.vue rsf-design/src/views/system/menu/index.vue rsf-design/src/views/system/menu/modules/menu-dialog.vue rsf-design/src/views/system/role/index.vue rsf-design/src/views/system/role/modules/role-edit-dialog.vue rsf-design/src/views/system/role/modules/role-permission-dialog.vue rsf-design/src/views/system/role/modules/role-search.vue rsf-design/src/views/system/user-center/index.vue rsf-design/src/views/system/user/index.vue rsf-design/src/views/system/user/modules/user-dialog.vue rsf-design/src/views/system/user/modules/user-search.vue rsf-design/tests/clean-dev-helpers.test.mjs rsf-design/tests/iconify-local-minimal.test.mjs rsf-design/tests/iconify-local-prefixes.test.mjs rsf-design/tests/manual-chunks.test.mjs rsf-design/tests/repo-hygiene.test.mjs rsf-design/vite.config.js