Переглянути джерело

Merge remote-tracking branch 'origin/dev' into dev

gssong 3 місяців тому
батько
коміт
9dcb392220

+ 5 - 1
.eslintrc-auto-import.json

@@ -17,6 +17,7 @@
     "MaybeRefOrGetter": true,
     "PropType": true,
     "Ref": true,
+    "ShallowRef": true,
     "Slot": true,
     "Slots": true,
     "VNode": true,
@@ -54,6 +55,7 @@
     "getActivePinia": true,
     "getCurrentInstance": true,
     "getCurrentScope": true,
+    "getCurrentWatcher": true,
     "h": true,
     "ignorableWatch": true,
     "inject": true,
@@ -63,6 +65,7 @@
     "isReactive": true,
     "isReadonly": true,
     "isRef": true,
+    "isShallow": true,
     "makeDestructurable": true,
     "mapActions": true,
     "mapGetters": true,
@@ -106,11 +109,11 @@
     "refAutoReset": true,
     "refDebounced": true,
     "refDefault": true,
+    "refManualReset": true,
     "refThrottled": true,
     "refWithControl": true,
     "resolveComponent": true,
     "resolveRef": true,
-    "resolveUnref": true,
     "setActivePinia": true,
     "setMapStoreSuffix": true,
     "shallowReactive": true,
@@ -168,6 +171,7 @@
     "useCountdown": true,
     "useCounter": true,
     "useCssModule": true,
+    "useCssSupports": true,
     "useCssVar": true,
     "useCssVars": true,
     "useCurrentElement": true,

+ 1 - 1
README.md

@@ -1,7 +1,7 @@
 ## 平台简介
 
 - 本仓库为前端技术栈 [Vue3](https://v3.cn.vuejs.org) + [TS](https://www.typescriptlang.org/) + [Element Plus](https://element-plus.org/zh-CN) + [Vite](https://cn.vitejs.dev) 版本。
-- 成员项目: 基于 vben5(ant-design-vue) 的前端项目 [ruoyi-plus-vben5](https://gitee.com/dapppp/ruoyi-plus-vben5)
+- 成员项目: 基于 vben5(ant-design-vue) 的前端项目 [ruoyi-plus-vben5](https://github.com/imdap/ruoyi-plus-vben5)
 - 成员项目: 基于soybean 的前端项目 [ruoyi-plus-soybean](https://gitee.com/xlsea/ruoyi-plus-soybean)
 
 ## 配套后端代码仓库地址

+ 30 - 30
package.json

@@ -23,68 +23,68 @@
     "@element-plus/icons-vue": "2.3.2",
     "@highlightjs/vue-plugin": "2.1.2",
     "@vueup/vue-quill": "1.2.0",
-    "@vueuse/core": "13.9.0",
+    "@vueuse/core": "14.2.1",
     "animate.css": "4.1.1",
     "await-to-js": "3.0.0",
-    "axios": "1.13.1",
+    "axios": "1.13.6",
     "crypto-js": "4.2.0",
-    "echarts": "5.6.0",
-    "element-plus": "2.11.7",
+    "echarts": "6.0.0",
+    "element-plus": "2.13.5",
     "file-saver": "2.0.5",
     "highlight.js": "11.11.1",
     "image-conversion": "2.1.1",
     "js-cookie": "3.0.5",
     "jsencrypt": "3.5.4",
     "nprogress": "0.2.0",
-    "pinia": "3.0.3",
+    "pinia": "3.0.4",
     "screenfull": "6.0.2",
-    "vue": "3.5.22",
+    "vue": "3.5.30",
     "vue-cropper": "1.1.4",
-    "vue-i18n": "11.1.12",
+    "vue-i18n": "11.3.0",
     "vue-json-pretty": "2.6.0",
-    "vue-router": "4.6.3",
+    "vue-router": "5.0.3",
     "vue-types": "6.0.0",
-    "vxe-table": "4.17.7"
+    "vxe-table": "4.18.1"
   },
   "devDependencies": {
-    "@iconify/json": "^2.2.403",
+    "@iconify/json": "^2.2.448",
     "@types/crypto-js": "4.2.2",
     "@types/file-saver": "2.0.7",
     "@types/js-cookie": "3.0.6",
-    "@types/node": "^22.19.0",
+    "@types/node": "^25.4.0",
     "@types/nprogress": "0.2.3",
-    "@unocss/preset-attributify": "66.5.4",
-    "@unocss/preset-icons": "66.5.4",
-    "@unocss/preset-uno": "66.5.4",
-    "@vitejs/plugin-vue": "5.2.4",
-    "@vue/compiler-sfc": "3.5.22",
+    "@unocss/preset-attributify": "66.6.6",
+    "@unocss/preset-icons": "66.6.6",
+    "@unocss/preset-uno": "66.6.6",
+    "@vitejs/plugin-vue": "6.0.4",
+    "@vue/compiler-sfc": "3.5.30",
     "@vue/eslint-config-prettier": "10.2.0",
     "@vue/eslint-config-typescript": "14.6.0",
-    "autoprefixer": "10.4.21",
+    "autoprefixer": "10.4.27",
     "eslint": "9.39.1",
-    "eslint-plugin-prettier": "5.5.4",
+    "eslint-plugin-prettier": "5.5.5",
     "eslint-plugin-vue": "9.33.0",
-    "globals": "16.5.0",
-    "prettier": "3.6.2",
-    "sass": "1.93.3",
+    "globals": "17.4.0",
+    "prettier": "3.8.1",
+    "sass": "1.98.0",
     "typescript": "~5.9.3",
-    "unocss": "66.5.4",
-    "unplugin-auto-import": "19.3.0",
-    "unplugin-icons": "22.5.0",
-    "unplugin-vue-components": "28.8.0",
+    "unocss": "66.6.6",
+    "unplugin-auto-import": "21.0.0",
+    "unplugin-icons": "23.0.1",
+    "unplugin-vue-components": "31.0.0",
     "unplugin-vue-setup-extend-plus": "1.0.1",
-    "vite": "6.4.1",
+    "vite": "7.3.1",
     "vite-plugin-compression": "0.5.1",
     "vite-plugin-svg-icons-ng": "^1.5.2",
-    "vite-plugin-vue-devtools": "8.0.3",
-    "vitest": "3.2.4",
-    "vue-tsc": "^2.2.12"
+    "vite-plugin-vue-devtools": "8.0.7",
+    "vitest": "4.0.18",
+    "vue-tsc": "^3.2.5"
   },
   "overrides": {
     "quill": "1.3.7"
   },
   "engines": {
-    "node": ">=20.15.0",
+    "node": ">=20.19.0",
     "npm": ">=8.19.0"
   },
   "browserslist": [

+ 1 - 1
src/api/workflow/task/types.ts

@@ -43,7 +43,7 @@ export interface ButtonList {
 }
 export interface FlowCopyVo {
   userId: string | number;
-  userName: string;
+  nickName: string;
 }
 
 export interface TaskOperationBo {

+ 2 - 2
src/assets/styles/btn.scss

@@ -45,7 +45,7 @@
   font-size: 14px;
   color: #fff;
   padding: 14px 36px;
-  border-radius: 8px;
+  border-radius: var(--app-radius-md);
   border: none;
   outline: none;
   transition: 600ms ease all;
@@ -95,5 +95,5 @@
   margin: 0;
   padding: 10px 15px;
   font-size: 14px;
-  border-radius: 4px;
+  border-radius: var(--app-radius-sm);
 }

+ 124 - 0
src/assets/styles/element-ui.scss

@@ -74,6 +74,9 @@
 
     .el-dialog {
       margin: 0 auto !important;
+      border-radius: var(--app-radius-lg);
+      box-shadow: var(--app-shadow-md);
+      overflow: hidden;
 
       .el-dialog__body {
         padding: 15px !important;
@@ -108,6 +111,8 @@
 
 // dropdown
 .el-dropdown-menu {
+  border-radius: var(--app-radius-md);
+  box-shadow: var(--app-shadow-sm);
   a {
     display: block;
   }
@@ -151,3 +156,122 @@
 .el-message-box .el-message-box__message {
   word-break: break-word;
 }
+
+.el-message-box {
+  border-radius: var(--app-radius-lg);
+  box-shadow: var(--app-shadow-md);
+}
+
+.el-message,
+.el-notification,
+.el-alert {
+  border-radius: var(--app-radius-md);
+  box-shadow: var(--app-shadow-sm);
+}
+
+// Modern rounded inputs
+.el-input__wrapper,
+.el-textarea__inner,
+.el-select__wrapper,
+.el-date-editor,
+.el-range-editor,
+.el-input-number,
+.el-input-number__decrease,
+.el-input-number__increase {
+  border-radius: var(--app-radius-md);
+  transition: box-shadow 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
+}
+
+.el-input__wrapper.is-focus,
+.el-textarea__inner:focus,
+.el-select__wrapper.is-focus,
+.el-date-editor.is-focus,
+.el-range-editor.is-focus {
+  box-shadow: 0 0 0 2px var(--el-color-primary-light-8);
+}
+
+// Buttons
+.el-button {
+  border-radius: var(--app-radius-md);
+  transition: transform 0.15s ease, box-shadow 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
+}
+
+.el-button:not(.is-text):not(.is-link):hover {
+  transform: translateY(-1px);
+  box-shadow: var(--app-shadow-sm);
+}
+
+// Tags and badges
+.el-tag {
+  border-radius: var(--app-radius-md);
+}
+
+// Cards, popovers, drawers
+.el-popover,
+.el-tooltip__popper,
+.el-popper {
+  border-radius: var(--app-radius-md);
+  box-shadow: var(--app-shadow-sm);
+}
+
+.el-drawer {
+  border-radius: var(--app-radius-lg);
+  box-shadow: var(--app-shadow-md);
+}
+
+.el-drawer__header {
+  margin-bottom: 0;
+  padding: 16px 20px;
+  border-bottom: 1px solid var(--el-border-color-light);
+}
+
+// Table polish
+.el-table {
+  border-radius: var(--app-radius-lg);
+  overflow: hidden;
+  box-shadow: var(--app-shadow-sm);
+  border: 1px solid var(--el-border-color-lighter);
+}
+
+.el-table__header-wrapper,
+.el-table__body-wrapper {
+  background: var(--el-bg-color);
+}
+
+.el-table__row:hover td.el-table__cell {
+  background-color: var(--el-fill-color-light) !important;
+}
+
+// Tabs
+.el-tabs__header {
+  margin: 0 0 12px 0;
+}
+
+.el-tabs__item {
+  border-radius: var(--app-radius-md);
+  margin: 0 2px;
+}
+
+.el-tabs__nav-wrap::after {
+  background-color: var(--el-border-color-lighter);
+}
+
+// Pagination
+.el-pagination .btn-prev,
+.el-pagination .btn-next,
+.el-pagination .el-pager li {
+  border-radius: var(--app-radius-sm);
+  transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
+}
+
+.el-pagination .el-pager li.is-active {
+  box-shadow: var(--app-shadow-sm);
+}
+
+// Breadcrumb
+.el-breadcrumb {
+  padding: 0;
+  background: transparent;
+  border: none;
+  border-radius: 0;
+}

+ 28 - 13
src/assets/styles/index.scss

@@ -14,18 +14,20 @@ body {
   -moz-osx-font-smoothing: grayscale;
   -webkit-font-smoothing: antialiased;
   text-rendering: optimizeLegibility;
+  background: var(--el-bg-color-page);
   font-family:
-    Helvetica Neue,
-    Helvetica,
-    PingFang SC,
-    Hiragino Sans GB,
-    Microsoft YaHei,
-    Arial,
+    'MiSans',
+    'HarmonyOS Sans SC',
+    'PingFang SC',
+    'Source Han Sans SC',
+    'Noto Sans SC',
+    'Hiragino Sans GB',
+    'Microsoft YaHei',
     sans-serif;
 }
 
 label {
-  font-weight: 700;
+  font-weight: 600;
 }
 
 html {
@@ -116,11 +118,18 @@ aside {
   background: #eef1f6;
   padding: 8px 24px;
   margin-bottom: 20px;
-  border-radius: 2px;
+  border-radius: var(--app-radius-md);
   display: block;
   line-height: 32px;
   font-size: 16px;
-  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+  font-family:
+    'MiSans',
+    'HarmonyOS Sans SC',
+    'PingFang SC',
+    'Source Han Sans SC',
+    'Noto Sans SC',
+    'Hiragino Sans GB',
+    'Microsoft YaHei',
     sans-serif;
   color: #2c3e50;
   -webkit-font-smoothing: antialiased;
@@ -139,21 +148,27 @@ aside {
 //main-container全局样式
 .app-container {
   padding: 20px;
+  background: var(--app-surface-bg);
+  border: 1px solid var(--app-surface-border);
+  border-radius: var(--app-radius-lg);
+  box-shadow: var(--app-shadow-sm);
 }
 
 // search面板样式
 .panel,
 .search {
   margin-bottom: 0.75rem;
-  border-radius: 0.25rem;
+  border-radius: var(--app-radius-lg);
   border: 1px solid var(--el-border-color-light);
   background-color: var(--el-bg-color-overlay);
   padding: 0.75rem;
-  transition: all ease 0.3s;
+  box-shadow: 0 0 0 rgba(0, 0, 0, 0);
+  transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
 
   &:hover {
-    box-shadow: 0 2px 12px #0000001a;
-    transition: all ease 0.3s;
+    box-shadow: var(--app-shadow-sm);
+    border-color: var(--el-border-color);
+    transform: translateY(-1px);
   }
 }
 

+ 23 - 2
src/assets/styles/ruoyi.scss

@@ -74,7 +74,7 @@ h6 {
 }
 
 .el-form .el-form-item__label {
-  font-weight: 700;
+  font-weight: 600;
 }
 .el-dialog:not(.is-fullscreen) {
   margin-top: 6vh !important;
@@ -127,7 +127,7 @@ h6 {
   margin-top: 5px;
   border: 1px solid #e5e6e7;
   background: #ffffff none;
-  border-radius: 4px;
+  border-radius: var(--app-radius-md);
   width: 100%;
 }
 
@@ -190,12 +190,27 @@ h6 {
 .el-card__header {
   padding: 14px 15px 7px !important;
   min-height: 40px;
+  background: var(--el-fill-color-blank);
+  border-bottom: 1px solid var(--el-border-color-light);
 }
 
 .el-card__body {
   padding: 15px 20px 20px 20px !important;
 }
 
+.el-card {
+  border-radius: var(--app-radius-lg);
+  box-shadow: var(--app-shadow-sm);
+  border-color: var(--el-border-color-lighter);
+  overflow: hidden;
+  transition: box-shadow 0.2s ease, transform 0.2s ease;
+}
+
+.el-card:hover {
+  box-shadow: var(--app-shadow-md);
+  transform: translateY(-1px);
+}
+
 .card-box {
   margin-bottom: 10px;
 }
@@ -282,3 +297,9 @@ h6 {
 .top-right-btn {
   margin-left: auto;
 }
+
+/* horizontal el menu */
+.el-menu--horizontal .el-menu-item .svg-icon + span,
+.el-menu--horizontal .el-sub-menu__title .svg-icon + span {
+  margin-left: 3px;
+}

+ 12 - 11
src/assets/styles/sidebar.scss

@@ -6,6 +6,7 @@
     transition: margin-left 0.28s;
     margin-left: $base-sidebar-width;
     position: relative;
+    background: var(--el-bg-color-page);
   }
 
   .sidebarHide {
@@ -88,7 +89,7 @@
     // menu hover
     .theme-dark .sub-menu-title-noDropdown,
     .theme-dark .el-sub-menu__title {
-      border-radius: 8px;
+      border-radius: var(--app-radius-md);
       margin: 1px 5px 1px 5px;
       &:hover {
         background-color: $base-sub-menu-title-hover !important;
@@ -96,7 +97,7 @@
     }
     .sub-menu-title-noDropdown,
     .el-sub-menu__title {
-      border-radius: 8px;
+      border-radius: var(--app-radius-md);
       margin: 1px 5px 1px 5px;
       &:hover {
         background-color: rgba(0, 0, 0, 0.05) !important;
@@ -110,7 +111,7 @@
     & .nest-menu .el-sub-menu > .el-sub-menu__title,
     & .el-sub-menu .el-menu-item {
       min-width: calc($base-sidebar-width - 20px) !important;
-      border-radius: 8px;
+      border-radius: var(--app-radius-md);
       height: 45px;
       margin: 1px 5px 1px 5px;
       &:not(.is-active):hover {
@@ -121,7 +122,7 @@
     & .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
     & .theme-dark .el-sub-menu .el-menu-item {
       background-color: $base-sub-menu-background !important;
-      border-radius: 8px;
+      border-radius: var(--app-radius-md);
       height: 45px;
       margin: 1px 5px 1px 5px;
 
@@ -137,7 +138,7 @@
 
     & .theme-dark .nest-menu .el-sub-menu > .el-sub-menu__title,
     & .theme-dark .el-menu-item {
-      border-radius: 8px;
+      border-radius: var(--app-radius-md);
       height: 45px;
       margin: 1px 5px 1px 5px;
 
@@ -153,7 +154,7 @@
 
     & .nest-menu .el-sub-menu > .el-sub-menu__title,
     & .el-menu-item {
-      border-radius: 8px;
+      border-radius: var(--app-radius-md);
       height: 45px;
       margin: 1px 5px 1px 5px;
 
@@ -196,9 +197,9 @@
 
     & .el-sub-menu {
       overflow: hidden;
-      border-radius: 8px;
+      border-radius: var(--app-radius-md);
       .el-sub-menu__title.el-tooltip__trigger {
-        border-radius: 8px;
+        border-radius: var(--app-radius-md);
         height: 45px;
       }
 
@@ -286,11 +287,11 @@
 }
 // 收起菜单后悬浮的菜单样式
 .el-popper.is-pure{
-  border-radius: 8px;
+  border-radius: var(--app-radius-md);
   .el-menu--popup{
-    border-radius: 8px;
+    border-radius: var(--app-radius-md);
   }
   .el-menu-item{
-    border-radius: 4px;
+    border-radius: var(--app-radius-sm);
   }
 }

+ 148 - 3
src/assets/styles/variables.module.scss

@@ -25,6 +25,24 @@
   // 添加 tag 相关变量
   --tags-view-active-bg: var(--el-color-primary);
   --tags-view-active-border-color: var(--el-color-primary);
+
+  // Modern rounded style + soft shadows
+  --app-radius-sm: 6px;
+  --app-radius-md: 10px;
+  --app-radius-lg: 14px;
+  --app-shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.08), 0 6px 16px rgba(15, 23, 42, 0.08);
+  --app-shadow-md: 0 8px 24px rgba(15, 23, 42, 0.12);
+  --app-shadow-lg: 0 12px 32px rgba(15, 23, 42, 0.16);
+  --app-surface-bg: #ffffff;
+  --app-surface-border: var(--el-border-color-lighter);
+
+  // Element Plus tokens
+  --el-border-radius-base: var(--app-radius-md);
+  --el-border-radius-small: var(--app-radius-sm);
+  --el-border-radius-round: 999px;
+  --el-box-shadow-light: var(--app-shadow-sm);
+  --el-box-shadow: var(--app-shadow-md);
+  --el-bg-color-page: #f5f7fb;
 }
 
 html.dark {
@@ -48,8 +66,103 @@ html.dark {
   }
 
   .el-button--primary {
-    --el-button-bg-color: var(--el-color-primary-dark-6);
-    --el-button-border-color: var(--el-color-primary-light-2);
+    --el-button-bg-color: #2b6bd3;
+    --el-button-border-color: #3a7be8;
+    --el-button-text-color: #eef4ff;
+    --el-button-hover-bg-color: #3a7be8;
+    --el-button-hover-border-color: #3a7be8;
+    --el-button-active-bg-color: #255fb8;
+    --el-button-active-border-color: #255fb8;
+  }
+
+  .el-button--primary.is-plain {
+    --el-button-bg-color: rgba(43, 107, 211, 0.12);
+    --el-button-border-color: rgba(58, 123, 232, 0.5);
+    --el-button-text-color: #dbe8ff;
+    --el-button-hover-bg-color: rgba(58, 123, 232, 0.2);
+    --el-button-hover-border-color: rgba(58, 123, 232, 0.7);
+    --el-button-active-bg-color: rgba(43, 107, 211, 0.28);
+    --el-button-active-border-color: rgba(43, 107, 211, 0.8);
+  }
+
+  .el-button--success {
+    --el-button-bg-color: #1f8a5a;
+    --el-button-border-color: #29a46d;
+    --el-button-text-color: #eefaf4;
+    --el-button-hover-bg-color: #29a46d;
+    --el-button-hover-border-color: #29a46d;
+    --el-button-active-bg-color: #1b784f;
+    --el-button-active-border-color: #1b784f;
+  }
+
+  .el-button--success.is-plain {
+    --el-button-bg-color: rgba(31, 138, 90, 0.12);
+    --el-button-border-color: rgba(41, 164, 109, 0.5);
+    --el-button-text-color: #dbf6e8;
+    --el-button-hover-bg-color: rgba(41, 164, 109, 0.2);
+    --el-button-hover-border-color: rgba(41, 164, 109, 0.7);
+    --el-button-active-bg-color: rgba(31, 138, 90, 0.28);
+    --el-button-active-border-color: rgba(31, 138, 90, 0.8);
+  }
+
+  .el-button--warning {
+    --el-button-bg-color: #b87922;
+    --el-button-border-color: #d6953b;
+    --el-button-text-color: #fff7e6;
+    --el-button-hover-bg-color: #d6953b;
+    --el-button-hover-border-color: #d6953b;
+    --el-button-active-bg-color: #a56c1d;
+    --el-button-active-border-color: #a56c1d;
+  }
+
+  .el-button--warning.is-plain {
+    --el-button-bg-color: rgba(184, 121, 34, 0.12);
+    --el-button-border-color: rgba(214, 149, 59, 0.5);
+    --el-button-text-color: #ffecc8;
+    --el-button-hover-bg-color: rgba(214, 149, 59, 0.2);
+    --el-button-hover-border-color: rgba(214, 149, 59, 0.7);
+    --el-button-active-bg-color: rgba(184, 121, 34, 0.28);
+    --el-button-active-border-color: rgba(184, 121, 34, 0.8);
+  }
+
+  .el-button--danger {
+    --el-button-bg-color: #b24a4a;
+    --el-button-border-color: #d16060;
+    --el-button-text-color: #ffecec;
+    --el-button-hover-bg-color: #d16060;
+    --el-button-hover-border-color: #d16060;
+    --el-button-active-bg-color: #9c3f3f;
+    --el-button-active-border-color: #9c3f3f;
+  }
+
+  .el-button--danger.is-plain {
+    --el-button-bg-color: rgba(178, 74, 74, 0.12);
+    --el-button-border-color: rgba(209, 96, 96, 0.5);
+    --el-button-text-color: #ffd6d6;
+    --el-button-hover-bg-color: rgba(209, 96, 96, 0.2);
+    --el-button-hover-border-color: rgba(209, 96, 96, 0.7);
+    --el-button-active-bg-color: rgba(178, 74, 74, 0.28);
+    --el-button-active-border-color: rgba(178, 74, 74, 0.8);
+  }
+
+  .el-button--info {
+    --el-button-bg-color: #4b5563;
+    --el-button-border-color: #667085;
+    --el-button-text-color: #f3f4f6;
+    --el-button-hover-bg-color: #667085;
+    --el-button-hover-border-color: #667085;
+    --el-button-active-bg-color: #3f4753;
+    --el-button-active-border-color: #3f4753;
+  }
+
+  .el-button--info.is-plain {
+    --el-button-bg-color: rgba(75, 85, 99, 0.16);
+    --el-button-border-color: rgba(102, 112, 133, 0.55);
+    --el-button-text-color: #e5e7eb;
+    --el-button-hover-bg-color: rgba(102, 112, 133, 0.22);
+    --el-button-hover-border-color: rgba(102, 112, 133, 0.75);
+    --el-button-active-bg-color: rgba(75, 85, 99, 0.3);
+    --el-button-active-border-color: rgba(75, 85, 99, 0.85);
   }
 
   .el-switch {
@@ -62,9 +175,41 @@ html.dark {
     --el-tag-border-color: var(--el-color-primary-light-2);
   }
 
+  .el-tag--success {
+    --el-tag-bg-color: rgba(31, 138, 90, 0.18);
+    --el-tag-border-color: rgba(41, 164, 109, 0.6);
+    --el-tag-text-color: #c7f2df;
+  }
+
+  .el-tag--warning {
+    --el-tag-bg-color: rgba(184, 121, 34, 0.18);
+    --el-tag-border-color: rgba(214, 149, 59, 0.6);
+    --el-tag-text-color: #ffe6bb;
+  }
+
+  .el-tag--danger {
+    --el-tag-bg-color: rgba(178, 74, 74, 0.18);
+    --el-tag-border-color: rgba(209, 96, 96, 0.6);
+    --el-tag-text-color: #ffd0d0;
+  }
+
+  .el-tag--info {
+    --el-tag-bg-color: rgba(75, 85, 99, 0.18);
+    --el-tag-border-color: rgba(102, 112, 133, 0.6);
+    --el-tag-text-color: #e5e7eb;
+  }
+
   // 在深色模式下使用更深的颜色
   --tags-view-active-bg: var(--el-color-primary-dark-6);
   --tags-view-active-border-color: var(--el-color-primary-light-2);
+
+  // Modern rounded style + soft shadows (dark)
+  --app-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.28), 0 8px 18px rgba(0, 0, 0, 0.25);
+  --app-shadow-md: 0 10px 26px rgba(0, 0, 0, 0.35);
+  --app-shadow-lg: 0 14px 34px rgba(0, 0, 0, 0.4);
+  --app-surface-bg: #151922;
+  --app-surface-border: var(--el-border-color);
+  --el-bg-color-page: #0f1115;
   // vxe-table 主题
   --vxe-font-color: #98989e;
   --vxe-primary-color: #2c7ecf;
@@ -137,4 +282,4 @@ $base-sidebar-width: 200px;
   dangerColor: $--color-danger;
   infoColor: $--color-info;
   warningColor: $--color-warning;
-}
+}

+ 41 - 34
src/components/Editor/index.vue

@@ -1,15 +1,13 @@
 <template>
   <div>
     <el-upload
-      v-if="type === 'url'"
-      :action="upload.url"
+      v-if="type"
+      action=""
       :before-upload="handleBeforeUpload"
-      :on-success="handleUploadSuccess"
-      :on-error="handleUploadError"
+      :http-request="handleUploadRequest"
       class="editor-img-uploader"
       name="file"
       :show-file-list="false"
-      :headers="upload.headers"
     >
       <i ref="uploadRef"></i>
     </el-upload>
@@ -31,7 +29,7 @@ import '@vueup/vue-quill/dist/vue-quill.snow.css';
 
 import { QuillEditor, Quill } from '@vueup/vue-quill';
 import { propTypes } from '@/utils/propTypes';
-import { globalHeaders } from '@/utils/request';
+import type { UploadRequestHandler, UploadRequestOptions } from 'element-plus';
 
 defineEmits(['update:modelValue']);
 
@@ -47,15 +45,11 @@ const props = defineProps({
   /* 上传文件大小限制(MB) */
   fileSize: propTypes.number.def(5),
   /* 类型(base64格式、url格式) */
-  type: propTypes.string.def('url')
+  type: propTypes.string.def('base64')
 });
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
-const upload = reactive<UploadOption>({
-  headers: globalHeaders(),
-  url: import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload'
-});
 const quillEditorRef = ref();
 const uploadRef = ref<HTMLDivElement>();
 
@@ -116,28 +110,9 @@ watch(
   { immediate: true }
 );
 
-// 图片上传成功返回图片地址
-const handleUploadSuccess = (res: any) => {
-  // 如果上传成功
-  if (res.code === 200) {
-    // 获取富文本实例
-    const quill = toRaw(quillEditorRef.value).getQuill();
-    // 获取光标位置
-    const length = quill.selection.savedRange.index;
-    // 插入图片,res为服务器返回的图片链接地址
-    quill.insertEmbed(length, 'image', res.data.url);
-    // 调整光标到最后
-    quill.setSelection(length + 1);
-    proxy?.$modal.closeLoading();
-  } else {
-    proxy?.$modal.msgError('图片插入失败');
-    proxy?.$modal.closeLoading();
-  }
-};
-
 // 图片上传前拦截
 const handleBeforeUpload = (file: any) => {
-  const type = ['image/jpeg', 'image/jpg', 'image/png', 'image/svg'];
+  const type = ['image/jpeg', 'image/jpg', 'image/png', 'image/svg', 'image/svg+xml'];
   const isJPG = type.includes(file.type);
   //检验文件格式
   if (!isJPG) {
@@ -156,9 +131,41 @@ const handleBeforeUpload = (file: any) => {
   return true;
 };
 
-// 图片失败拦截
-const handleUploadError = (err: any) => {
-  proxy?.$modal.msgError('上传文件失败');
+// base64 模式插入图片
+const handleUploadRequest: UploadRequestHandler = (options: UploadRequestOptions) => {
+  return new Promise<void>((resolve, reject) => {
+    const file = options.file as File;
+    const quill = toRaw(quillEditorRef.value)?.getQuill();
+    if (!quill) {
+      proxy?.$modal.msgError('编辑器未就绪');
+      proxy?.$modal.closeLoading();
+      reject(new Error('editor not ready'));
+      return;
+    }
+    const reader = new FileReader();
+    reader.onload = () => {
+      const base64 = reader.result as string;
+      const range = quill.selection?.savedRange;
+      const length = range ? range.index : quill.getLength();
+      quill.insertEmbed(length, 'image', base64);
+      quill.setSelection(length + 1);
+      proxy?.$modal.closeLoading();
+      options.onSuccess?.({ url: base64 });
+      resolve();
+    };
+    reader.onerror = () => {
+      proxy?.$modal.msgError('图片插入失败');
+      proxy?.$modal.closeLoading();
+      const err = Object.assign(new Error('read image failed'), {
+        status: 0,
+        method: 'POST',
+        url: options.action || ''
+      });
+      options.onError?.(err as any);
+      reject(err);
+    };
+    reader.readAsDataURL(file);
+  });
 };
 </script>
 

+ 10 - 4
src/components/Process/submitVerify.vue

@@ -14,7 +14,7 @@
       <el-form-item label="抄送" v-if="buttonObj.copy">
         <el-button type="primary" icon="Plus" circle @click="openUserSelectCopy" />
         <el-tag v-for="user in selectCopyUserList" :key="user.userId" closable style="margin: 2px" @close="handleCopyCloseTag(user)">
-          {{ user.userName }}
+          {{ user.nickName }}
         </el-tag>
       </el-form-item>
       <el-form-item v-if="buttonObj.pop && nestNodeList && nestNodeList.length > 0" label="下一步审批人" prop="assigneeMap">
@@ -80,7 +80,13 @@
     <!-- 加签组件 -->
     <UserSelect ref="multiInstanceUserRef" :multiple="true" @confirm-call-back="addMultiInstanceUser"></UserSelect>
     <!-- 弹窗选人 -->
-    <UserSelect ref="porUserRef" :data="form.assigneeMap[nodeCode]" :multiple="true" :userIds="popUserIds" @confirm-call-back="handlePopUser"></UserSelect>
+    <UserSelect
+      ref="porUserRef"
+      :data="form.assigneeMap[nodeCode]"
+      :multiple="true"
+      :userIds="popUserIds"
+      @confirm-call-back="handlePopUser"
+    ></UserSelect>
 
     <!-- 驳回开始 -->
     <el-dialog v-model="backVisible" draggable title="驳回" width="40%" :close-on-click-modal="false">
@@ -265,7 +271,7 @@ const openDialog = async (id?: string) => {
   selectCopyUserList.value = task.value.copyList;
   selectCopyUserIds.value = task.value.copyList.map((e) => e.userId).join(',');
   varNodeList.value = task.value.varList;
-  console.log('varNodeList', varNodeList.value)
+  console.log('varNodeList', varNodeList.value);
   buttonDisabled.value = false;
   try {
     const data = {
@@ -310,7 +316,7 @@ const handleCompleteTask = async () => {
     selectCopyUserList.value.forEach((e) => {
       const copyUser = {
         userId: e.userId,
-        userName: e.userName
+        nickName: e.nickName
       };
       flowCopyList.push(copyUser);
     });

+ 17 - 0
src/enums/NavTypeEnum.ts

@@ -0,0 +1,17 @@
+/**
+ * 导航栏布局枚举
+ */
+export enum NavTypeEnum {
+  /**
+   * 左侧导航
+   */
+  LEFT = 'left',
+  /**
+   * 顶部导航
+   */
+  TOP = 'top',
+  /**
+   * 混合导航
+   */
+  MIX = 'mix'
+}

+ 3 - 3
src/layout/components/AppMain.vue

@@ -89,11 +89,11 @@ function addIframe() {
 }
 
 ::-webkit-scrollbar-track {
-  background-color: #f1f1f1;
+  background-color: var(--el-fill-color-lighter);
 }
 
 ::-webkit-scrollbar-thumb {
-  background-color: #c0c0c0;
-  border-radius: 3px;
+  background-color: var(--el-text-color-placeholder);
+  border-radius: 999px;
 }
 </style>

+ 51 - 12
src/layout/components/Navbar.vue

@@ -1,9 +1,14 @@
 <template>
-  <div class="navbar">
+  <div class="navbar" :class="'nav' + navType">
     <hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggle-click="toggleSideBar" />
-    <breadcrumb v-if="!settingsStore.topNav" id="breadcrumb-container" class="breadcrumb-container" />
-    <top-nav v-if="settingsStore.topNav" id="topmenu-container" class="topmenu-container" />
 
+    <breadcrumb v-if="navType == NavTypeEnum.LEFT" id="breadcrumb-container" class="breadcrumb-container" />
+    <top-nav v-if="navType == NavTypeEnum.MIX" id="topmenu-container" class="topmenu-container" />
+
+    <template v-if="navType == NavTypeEnum.TOP">
+      <logo v-show="showLogo" :collapse="false"></logo>
+      <top-bar id="topbar-container" class="topbar-container" />
+    </template>
     <div class="right-menu flex align-center">
       <template v-if="appStore.device !== 'mobile'">
         <el-select
@@ -99,6 +104,9 @@ import { TenantVO } from '@/api/types';
 import notice from './notice/index.vue';
 import router from '@/router';
 import { ElMessageBoxOptions } from 'element-plus/es/components/message-box/src/message-box.type';
+import { NavTypeEnum } from '@/enums/NavTypeEnum';
+import Logo from "@/layout/components/Sidebar/Logo.vue";
+import TopBar from './TopBar'
 
 const appStore = useAppStore();
 const userStore = useUserStore();
@@ -109,6 +117,9 @@ const newNotice = ref(<number>0);
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const userId = ref(userStore.userId);
+const navType = computed(() => settingsStore.navType);
+const showLogo = computed(() => settingsStore.sidebarLogo);
+
 const companyName = ref(undefined);
 const tenantList = ref<TenantVO[]>([]);
 // 是否切换了租户
@@ -201,6 +212,12 @@ watch(
 </script>
 
 <style lang="scss" scoped>
+.navbar.navtop {
+  .hamburger-container {
+    display: none !important;
+  }
+}
+
 :deep(.el-select .el-input__wrapper) {
   height: 30px;
 }
@@ -221,24 +238,34 @@ watch(
   height: 50px;
   overflow: hidden;
   position: relative;
-  //background: #fff;
-  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
+  background: var(--el-bg-color);
+  border-bottom: 1px solid var(--el-border-color-lighter);
+  box-shadow: none;
+  display: flex;
+  align-items: center;
+  // padding: 0 8px;
+  box-sizing: border-box;
 
   .hamburger-container {
     line-height: 46px;
     height: 100%;
-    float: left;
+    //float: left;
     cursor: pointer;
     transition: background 0.3s;
     -webkit-tap-highlight-color: transparent;
+    display: flex;
+    align-items: center;
+    flex-shrink: 0;
+    margin-right: 8px;
 
     &:hover {
-      background: rgba(0, 0, 0, 0.025);
+      background: var(--el-fill-color-lighter);
     }
   }
 
   .breadcrumb-container {
-    float: left;
+    //float: left;
+    flex-shrink: 0;
   }
 
   .topmenu-container {
@@ -246,16 +273,28 @@ watch(
     left: 50px;
   }
 
+  .topbar-container {
+    flex: 1;
+    min-width: 0;
+    display: flex;
+    align-items: center;
+    overflow: hidden;
+    margin-left: 8px;
+  }
+
+
   .errLog-container {
     display: inline-block;
     vertical-align: top;
   }
 
   .right-menu {
-    float: right;
+    //float: right;
     height: 100%;
     line-height: 50px;
     display: flex;
+    align-items: center;
+    margin-left: auto;
 
     &:focus {
       outline: none;
@@ -266,7 +305,7 @@ watch(
       padding: 0 8px;
       height: 100%;
       font-size: 18px;
-      color: #5a5e66;
+      color: var(--el-text-color-regular);
       vertical-align: text-bottom;
 
       &.hover-effect {
@@ -274,7 +313,7 @@ watch(
         transition: background 0.3s;
 
         &:hover {
-          background: rgba(0, 0, 0, 0.025);
+          background: var(--el-fill-color-lighter);
         }
       }
     }
@@ -290,7 +329,7 @@ watch(
           cursor: pointer;
           width: 40px;
           height: 40px;
-          border-radius: 10px;
+          border-radius: var(--app-radius-md);
           margin-top: 10px;
         }
 

+ 121 - 14
src/layout/components/Settings/index.vue

@@ -1,5 +1,40 @@
 <template>
   <el-drawer v-model="showSettings" :with-header="false" direction="rtl" size="300px" close-on-click-modal>
+    <h3 class="drawer-title">菜单导航设置</h3>
+    <div class="nav-wrap">
+      <el-tooltip content="左侧菜单" placement="bottom">
+        <div
+          class="item left"
+          @click="handleNavType(NavTypeEnum.LEFT)"
+          :style="{ '--theme': theme }"
+          :class="{ activeItem: navType == NavTypeEnum.LEFT }"
+        >
+          <b></b><b></b>
+        </div>
+      </el-tooltip>
+
+      <el-tooltip content="混合菜单" placement="bottom">
+        <div
+          class="item mix"
+          @click="handleNavType(NavTypeEnum.MIX)"
+          :style="{ '--theme': theme }"
+          :class="{ activeItem: navType == NavTypeEnum.MIX }"
+        >
+          <b></b><b></b>
+        </div>
+      </el-tooltip>
+      <el-tooltip content="顶部菜单" placement="bottom">
+        <div
+          class="item top"
+          @click="handleNavType(NavTypeEnum.TOP)"
+          :style="{ '--theme': theme }"
+          :class="{ activeItem: navType == NavTypeEnum.TOP }"
+        >
+          <b></b><b></b>
+        </div>
+      </el-tooltip>
+    </div>
+
     <h3 class="drawer-title">主题风格设置</h3>
 
     <div class="setting-drawer-block-checbox">
@@ -45,13 +80,6 @@
 
     <h3 class="drawer-title">系统布局配置</h3>
 
-    <div class="drawer-item">
-      <span>开启 TopNav</span>
-      <span class="comp-style">
-        <el-switch v-model="settingsStore.topNav" class="drawer-switch" @change="topNavChange" />
-      </span>
-    </div>
-
     <div class="drawer-item">
       <span>开启 Tags-Views</span>
       <span class="comp-style">
@@ -101,6 +129,7 @@ import { useSettingsStore } from '@/store/modules/settings';
 import { usePermissionStore } from '@/store/modules/permission';
 import { handleThemeStyle } from '@/utils/theme';
 import { SideThemeEnum } from '@/enums/SideThemeEnum';
+import { NavTypeEnum } from '@/enums/NavTypeEnum';
 import defaultSettings from '@/settings';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -113,7 +142,7 @@ const theme = ref(settingsStore.theme);
 const sideTheme = ref(settingsStore.sideTheme);
 const storeSettings = computed(() => settingsStore);
 const predefineColors = ref(['#409EFF', '#ff4500', '#ff8c00', '#ffd700', '#90ee90', '#00ced1', '#1e90ff', '#c71585']);
-
+const navType = ref(settingsStore.navType);
 // 是否暗黑模式
 const isDark = useDark({
   storageKey: 'useDarkKey',
@@ -130,11 +159,26 @@ watch(isDark, () => {
 });
 const toggleDark = () => useToggle(isDark);
 
-const topNavChange = (val: any) => {
-  if (!val) {
-    appStore.toggleSideBarHide(false);
-    permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
-  }
+/** 菜单导航设置 */
+watch(
+  () => navType,
+  (val: string) => {
+    if (val.value === NavTypeEnum.TOP) {
+      appStore.toggleSideBarHide(true);
+      permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
+    } else if (val.value === NavTypeEnum.LEFT) {
+      appStore.toggleSideBarHide(false);
+      permissionStore.setSidebarRouters(permissionStore.defaultRoutes as any);
+    } else if (val.value === NavTypeEnum.MIX) {
+      appStore.toggleSideBarHide(false);
+    }
+  },
+  { immediate: true, deep: true }
+);
+
+const handleNavType = (val: NavTypeEnum) => {
+  settingsStore.navType = val;
+  navType.value = val;
 };
 
 const dynamicTitleChange = () => {
@@ -158,7 +202,6 @@ const handleTheme = (val: string) => {
 const saveSetting = () => {
   proxy?.$modal.loading('正在保存到本地,请稍候...');
   const settings = useStorage<LayoutSetting>('layout-setting', defaultSettings);
-  settings.value.topNav = storeSettings.value.topNav;
   settings.value.tagsView = storeSettings.value.tagsView;
   settings.value.tagsIcon = storeSettings.value.tagsIcon;
   settings.value.fixedHeader = storeSettings.value.fixedHeader;
@@ -166,6 +209,7 @@ const saveSetting = () => {
   settings.value.dynamicTitle = storeSettings.value.dynamicTitle;
   settings.value.sideTheme = storeSettings.value.sideTheme;
   settings.value.theme = storeSettings.value.theme;
+  settings.value.navType = storeSettings.value.navType;
   setTimeout(() => {
     proxy?.$modal.closeLoading();
   }, 1000);
@@ -243,4 +287,67 @@ defineExpose({
     margin: -3px 8px 0px 0px;
   }
 }
+
+// 导航模式
+.nav-wrap {
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+  margin-top: 10px;
+  margin-bottom: 20px;
+
+  .activeItem {
+    border: 2px solid #{'var(--theme)'} !important;
+  }
+
+  .item {
+    position: relative;
+    margin-right: 16px;
+    cursor: pointer;
+    width: 56px;
+    height: 48px;
+    border-radius: 4px;
+    background: #f0f2f5;
+    border: 2px solid transparent;
+  }
+
+  .left {
+    b:first-child {
+      display: block;
+      height: 30%;
+      background: #fff;
+    }
+    b:last-child {
+      width: 30%;
+      background: #1b2a47;
+      position: absolute;
+      height: 100%;
+      top: 0;
+      border-radius: 4px 0 0 4px;
+    }
+  }
+  .mix {
+    b:first-child {
+      border-radius: 4px 4px 0 0;
+      display: block;
+      height: 30%;
+      background: #1b2a47;
+    }
+    b:last-child {
+      width: 30%;
+      background: #1b2a47;
+      position: absolute;
+      height: 70%;
+      border-radius: 0 0 0 4px;
+    }
+  }
+  .top {
+    b:first-child {
+      display: block;
+      height: 30%;
+      background: #1b2a47;
+      border-radius: 4px 4px 0 0;
+    }
+  }
+}
 </style>

+ 29 - 12
src/layout/components/Sidebar/Logo.vue

@@ -2,18 +2,17 @@
   <div
     class="sidebar-logo-container"
     :class="{ collapse: collapse }"
-    :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"
   >
     <transition :enter-active-class="proxy?.animate.logoAnimate.enter" mode="out-in">
       <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
         <img v-if="logo" :src="logo" class="sidebar-logo" />
-        <h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">
+        <h1 v-else class="sidebar-title">
           {{ title }}
         </h1>
       </router-link>
       <router-link v-else key="expand" class="sidebar-logo-link" to="/">
         <img v-if="logo" :src="logo" class="sidebar-logo" />
-        <h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">
+        <h1 class="sidebar-title">
           {{ title }}
         </h1>
       </router-link>
@@ -26,6 +25,7 @@ import variables from '@/assets/styles/variables.module.scss';
 import logo from '@/assets/logo/logo.png';
 import { useSettingsStore } from '@/store/modules/settings';
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
+import { NavTypeEnum } from '@/enums/NavTypeEnum';
 
 defineProps({
   collapse: {
@@ -37,6 +37,28 @@ defineProps({
 const title = import.meta.env.VITE_APP_LOGO_TITLE;
 const settingsStore = useSettingsStore();
 const sideTheme = computed(() => settingsStore.sideTheme);
+
+// 获取Logo背景色
+const getLogoBackground = computed(() => {
+  if (settingsStore.isDark) {
+    return 'var(--sidebar-bg)'
+  }
+  if (settingsStore.navType == NavTypeEnum.TOP) {
+    return variables.menuLightBackground
+  }
+  return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBackground
+})
+
+// 获取Logo文字颜色
+const getLogoTextColor = computed(() => {
+  if (settingsStore.isDark) {
+    return 'var(--sidebar-text)'
+  }
+  if (settingsStore.navType == NavTypeEnum.TOP) {
+    return variables.logoLightTitleColor
+  }
+  return sideTheme.value === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor
+})
 </script>
 
 <style lang="scss" scoped>
@@ -51,10 +73,9 @@ const sideTheme = computed(() => settingsStore.sideTheme);
 
 .sidebar-logo-container {
   position: relative;
-  width: 100%;
   height: 50px;
   line-height: 50px;
-  background: #2b2f3a;
+  background: v-bind(getLogoBackground);
   text-align: center;
   overflow: hidden;
 
@@ -67,21 +88,17 @@ const sideTheme = computed(() => settingsStore.sideTheme);
       height: 32px;
       vertical-align: middle;
       margin-right: 12px;
+      margin-left: 12px;
     }
 
     & .sidebar-title {
       display: inline-block;
       margin: 0;
-      color: #fff;
+      color: v-bind(getLogoTextColor);
       font-weight: 600;
       line-height: 50px;
       font-size: 14px;
-      font-family:
-        Avenir,
-        Helvetica Neue,
-        Arial,
-        Helvetica,
-        sans-serif;
+      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
       vertical-align: middle;
     }
   }

+ 10 - 7
src/layout/components/TagsView/index.vue

@@ -244,9 +244,8 @@ onMounted(() => {
   width: 100%;
   background-color: var(--el-bg-color);
   border: 1px solid var(--el-border-color-light);
-  box-shadow:
-    0 1px 3px 0 rgba(0, 0, 0, 0.12),
-    0 0 3px 0 rgba(0, 0, 0, 0.04);
+  border-radius: var(--app-radius-md);
+  box-shadow: var(--app-shadow-sm);
   .tags-view-wrapper {
     .tags-view-item {
       display: inline-block;
@@ -261,9 +260,13 @@ onMounted(() => {
       font-size: 12px;
       margin-left: 5px;
       margin-top: 4px;
-      border-radius: 4px;
+      border-radius: var(--app-radius-md);
+      transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease, color 0.2s ease;
       &:hover {
         color: var(--el-color-primary);
+        border-color: var(--el-color-primary-light-5);
+        box-shadow: var(--app-shadow-sm);
+        transform: translateY(-1px);
       }
       &:first-of-type {
         margin-left: 15px;
@@ -302,16 +305,16 @@ onMounted(() => {
     position: absolute;
     list-style-type: none;
     padding: 5px 0;
-    border-radius: 4px;
+    border-radius: var(--app-radius-md);
     font-size: 12px;
     font-weight: 400;
-    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
+    box-shadow: var(--app-shadow-md);
     li {
       margin: 0;
       padding: 7px 16px;
       cursor: pointer;
       &:hover {
-        background: #eee;
+        background: var(--el-fill-color-light);
       }
     }
   }

+ 103 - 0
src/layout/components/TopBar/index.vue

@@ -0,0 +1,103 @@
+<template>
+  <el-menu class="topbar-menu" :ellipsis="false" :default-active="activeMenu" :active-text-color="theme" mode="horizontal">
+    <sidebar-item :key="route.path + index" v-for="(route, index) in topMenus" :item="route" :base-path="route.path" />
+
+    <el-sub-menu index="more" class="el-sub-menu__hide-arrow" v-if="moreRoutes.length > 0">
+      <template #title>
+        <span>更多菜单</span>
+      </template>
+      <sidebar-item :key="route.path + index" v-for="(route, index) in moreRoutes" :item="route" :base-path="route.path" />
+    </el-sub-menu>
+  </el-menu>
+</template>
+
+<script setup>
+import SidebarItem from '../Sidebar/SidebarItem'
+import {useAppStore} from '@/store/modules/app'
+import {useSettingsStore} from '@/store/modules/settings'
+import {usePermissionStore} from '@/store/modules/permission'
+
+const route = useRoute()
+const appStore = useAppStore()
+const settingsStore = useSettingsStore()
+const permissionStore = usePermissionStore()
+
+const sidebarRouters = computed(() => permissionStore.sidebarRouters)
+const theme = computed(() => settingsStore.theme)
+const device = computed(() => appStore.device)
+const activeMenu = computed(() => {
+  const { meta, path } = route
+  if (meta.activeMenu) {
+    return meta.activeMenu
+  }
+  return path
+})
+
+const visibleNumber = ref(5)
+const topMenus = computed(() => {
+  return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(0, visibleNumber.value)
+})
+const moreRoutes = computed(() => {
+  return permissionStore.sidebarRouters.filter((f) => !f.hidden).slice(visibleNumber.value, sidebarRouters.value.length - visibleNumber.value)
+})
+function setVisibleNumber() {
+  let width = document.body.getBoundingClientRect().width
+  if (width >= 1000) {
+    width -= 500
+  }
+  visibleNumber.value = parseInt(width / 3 / 85)
+}
+
+onMounted(() => {
+  window.addEventListener('resize', setVisibleNumber)
+})
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', setVisibleNumber)
+})
+
+onMounted(() => {
+  setVisibleNumber()
+})
+</script>
+
+<style lang="scss">
+
+/* menu item */
+#app .topbar-menu.el-menu--horizontal .el-sub-menu__title, #app .topbar-menu.el-menu--horizontal .el-menu-item {
+  padding: 0 10px !important;
+}
+
+.topbar-menu.el-menu--horizontal > .el-menu-item {
+  float: left;
+  height: 50px !important;
+  line-height: 50px !important;
+  color: #303133 !important;
+  padding: 0 5px !important;
+  margin: 0 10px !important;
+}
+
+.el-sub-menu.is-active .svg-icon, .el-menu-item.is-active .svg-icon + span, .el-sub-menu.is-active .svg-icon + span, .el-sub-menu.is-active .el-sub-menu__title span {
+  color: v-bind(theme);
+}
+
+/* sub-menu item */
+.topbar-menu.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
+  float: left;
+  line-height: 50px !important;
+  color: #303133 !important;
+  margin: 0 15px -3px!important;
+}
+
+/* topbar more arrow */
+.topbar-menu .el-sub-menu .el-sub-menu__icon-arrow {
+  position: static;
+  margin-left: 8px;
+  margin-top: 0px;
+  display: block !important;
+}
+
+/* menu__title el-menu-item */
+.topbar-menu.el-menu--horizontal .el-sub-menu__title, .topbar-menu.el-menu--horizontal .el-menu-item {
+  height: 60px;
+}
+</style>

+ 8 - 0
src/layout/index.vue

@@ -26,6 +26,7 @@ import SideBar from './components/Sidebar/index.vue';
 import { AppMain, Navbar, Settings, TagsView } from './components';
 import { useAppStore } from '@/store/modules/app';
 import { useSettingsStore } from '@/store/modules/settings';
+import { NavTypeEnum } from '@/enums/NavTypeEnum';
 import { initWebSocket } from '@/utils/websocket';
 import { initSSE } from '@/utils/sse';
 
@@ -35,6 +36,13 @@ const sidebar = computed(() => useAppStore().sidebar);
 const device = computed(() => useAppStore().device);
 const needTagsView = computed(() => settingsStore.tagsView);
 const fixedHeader = computed(() => settingsStore.fixedHeader);
+const layout = computed(() => settingsStore.navType);
+
+// 根据布局模式判断是否显示侧边栏
+const showSidebar = computed(() => {
+  if (sidebar.value.hide) return false;
+  return layout.value === NavTypeEnum.LEFT || layout.value === NavTypeEnum.MIX;
+});
 
 const classObj = computed(() => ({
   hideSidebar: !sidebar.value.opened,

+ 0 - 6
src/main.ts

@@ -28,9 +28,6 @@ import ElementIcons from '@/plugins/svgicon';
 // permission control
 import './permission';
 
-// 开发者工具保护
-import { initDevToolsProtection } from '@/utils/devtools-protection';
-
 // 国际化
 import i18n from '@/lang/index';
 
@@ -58,6 +55,3 @@ app.use(plugins);
 directive(app);
 
 app.mount('#app');
-
-// 初始化开发者工具保护(仅生产环境)
-initDevToolsProtection();

+ 4 - 3
src/settings.ts

@@ -1,4 +1,5 @@
 import { LanguageEnum } from '@/enums/LanguageEnum';
+import { NavTypeEnum } from '@/enums/NavTypeEnum';
 
 const setting: DefaultSettings = {
   /**
@@ -18,9 +19,9 @@ const setting: DefaultSettings = {
   showSettings: true,
 
   /**
-   * 是否显示顶部导航
+   * 默认布局
    */
-  topNav: false,
+  navType: NavTypeEnum.LEFT,
 
   /**
    * 是否显示 tagsView
@@ -35,7 +36,7 @@ const setting: DefaultSettings = {
   /**
    * 是否固定头部
    */
-  fixedHeader: false,
+  fixedHeader: true,
 
   /**
    * 是否显示logo

+ 5 - 3
src/store/modules/settings.ts

@@ -3,6 +3,7 @@ import defaultSettings from '@/settings';
 import { useDynamicTitle } from '@/utils/dynamicTitle';
 import { useStorage } from '@vueuse/core';
 import { ref } from 'vue';
+import { NavTypeEnum } from '@/enums/NavTypeEnum';
 
 export const useSettingsStore = defineStore('setting', () => {
   const storageSetting = useStorage<LayoutSetting>('layout-setting', {
@@ -13,13 +14,13 @@ export const useSettingsStore = defineStore('setting', () => {
     sidebarLogo: defaultSettings.sidebarLogo,
     dynamicTitle: defaultSettings.dynamicTitle,
     sideTheme: defaultSettings.sideTheme,
-    theme: defaultSettings.theme
+    theme: defaultSettings.theme,
+    navType: defaultSettings.navType
   });
   const title = ref<string>(defaultSettings.title);
   const theme = ref<string>(storageSetting.value.theme);
   const sideTheme = ref<string>(storageSetting.value.sideTheme);
   const showSettings = ref<boolean>(defaultSettings.showSettings);
-  const topNav = ref<boolean>(storageSetting.value.topNav);
   const tagsView = ref<boolean>(storageSetting.value.tagsView);
   const tagsIcon = ref<boolean>(storageSetting.value.tagsIcon);
   const fixedHeader = ref<boolean>(storageSetting.value.fixedHeader);
@@ -27,6 +28,7 @@ export const useSettingsStore = defineStore('setting', () => {
   const dynamicTitle = ref<boolean>(storageSetting.value.dynamicTitle);
   const animationEnable = ref<boolean>(defaultSettings.animationEnable);
   const dark = ref<boolean>(defaultSettings.dark);
+  const navType = ref<NavTypeEnum>(storageSetting.value.navType || NavTypeEnum.LEFT);
 
   const setTitle = (value: string) => {
     title.value = value;
@@ -37,7 +39,6 @@ export const useSettingsStore = defineStore('setting', () => {
     theme,
     sideTheme,
     showSettings,
-    topNav,
     tagsView,
     tagsIcon,
     fixedHeader,
@@ -45,6 +46,7 @@ export const useSettingsStore = defineStore('setting', () => {
     dynamicTitle,
     animationEnable,
     dark,
+    navType,
     setTitle
   };
 });

+ 3 - 2
src/types/global.d.ts

@@ -1,5 +1,6 @@
 import type { PropType as VuePropType, ComponentInternalInstance as ComponentInstance } from 'vue';
 import { LanguageEnum } from '@/enums/LanguageEnum';
+import { NavTypeEnum } from '@/enums/NavTypeEnum';
 
 declare global {
   /** vue Instance */
@@ -90,9 +91,9 @@ declare global {
   }
   declare interface LayoutSetting {
     /**
-     * 是否显示顶部导航
+     * 默认布局
      */
-    topNav: boolean;
+    navType: NavTypeEnum;
 
     /**
      * 是否显示多标签导航

+ 0 - 158
src/utils/devtools-protection.ts

@@ -1,158 +0,0 @@
-/**
- * 开发者工具保护
- * 检测开发者工具是否打开,如果打开则循环执行 debugger 阻止调试
- */
-
-// 检测开发者工具是否打开
-function detectDevTools(): boolean {
-  try {
-    // 方法1: 检测窗口尺寸差异(最可靠的方法)
-    const widthThreshold = 160; // 开发者工具最小宽度
-    const heightThreshold = 160; // 开发者工具最小高度
-
-    const widthDiff = window.outerWidth - window.innerWidth;
-    const heightDiff = window.outerHeight - window.innerHeight;
-
-    if (widthDiff > widthThreshold || heightDiff > heightThreshold) {
-      return true;
-    }
-
-    // 方法2: 检测 debugger 执行时间(最准确的方法)
-    const start = performance.now();
-    debugger; // 这个 debugger 用于检测,不会被移除
-    const end = performance.now();
-    const timeDiff = end - start;
-
-    // 如果 debugger 被跳过(开发者工具关闭),时间差会很小(通常 < 1ms)
-    // 如果 debugger 暂停(开发者工具打开),时间差会很大(通常 > 100ms)
-    // 降低阈值以提高检测灵敏度
-    if (timeDiff > 10) {
-      return true;
-    }
-
-    // 方法3: 检测控制台对象
-    let devtoolsDetected = false;
-    const element = document.createElement('div');
-    Object.defineProperty(element, 'id', {
-      get: function () {
-        devtoolsDetected = true;
-        return '';
-      }
-    });
-
-    // 使用 console 来触发 getter(仅在开发者工具打开时)
-    try {
-      console.log(element);
-      console.clear();
-    } catch (e) {
-      // 忽略错误
-    }
-
-    if (devtoolsDetected) {
-      return true;
-    }
-
-    // 方法4: 检测控制台是否被重写(开发者工具打开时)
-    const devtoolsRegex = /./;
-    // @ts-expect-error - 动态添加属性
-    devtoolsRegex.toString = function () {
-      // @ts-expect-error - 动态添加属性
-      this.opened = true;
-    };
-    console.log('%c', devtoolsRegex);
-    // @ts-expect-error - 检查动态添加的属性
-    if (devtoolsRegex.opened) {
-      return true;
-    }
-  } catch (e) {
-    // 如果检测过程中出错,默认返回 false
-    return false;
-  }
-
-  return false;
-}
-
-// 开发者工具保护主函数
-export function initDevToolsProtection(): void {
-  // 可以通过环境变量控制是否启用
-  // 生产环境默认启用,开发环境可以通过 VITE_ENABLE_ANTI_DEBUG=true 来启用测试
-  const isProduction = import.meta.env.MODE === 'production';
-  const enableAntiDebug = import.meta.env.VITE_ENABLE_ANTI_DEBUG === 'true' || isProduction;
-
-  if (!enableAntiDebug) {
-    return;
-  }
-
-  let devToolsOpen = false;
-  let debuggerInterval: number | null = null;
-
-  // 立即执行一次检测
-  const initialCheck = detectDevTools();
-  if (initialCheck) {
-    devToolsOpen = true;
-    debuggerInterval = window.setInterval(() => {
-      debugger; // 循环执行 debugger
-    }, 50); // 更频繁的 debugger,每 50ms 一次
-  }
-
-  // 循环检测开发者工具(更频繁的检测)
-  const checkInterval = setInterval(() => {
-    const isOpen = detectDevTools();
-
-    if (isOpen && !devToolsOpen) {
-      // 开发者工具刚打开
-      devToolsOpen = true;
-
-      // 开始循环执行 debugger(更频繁)
-      if (debuggerInterval === null) {
-        debuggerInterval = window.setInterval(() => {
-          debugger; // 循环执行 debugger
-        }, 50); // 每 50ms 执行一次,更激进
-      }
-    } else if (!isOpen && devToolsOpen) {
-      // 开发者工具关闭了
-      devToolsOpen = false;
-
-      // 停止循环 debugger
-      if (debuggerInterval !== null) {
-        clearInterval(debuggerInterval);
-        debuggerInterval = null;
-      }
-    }
-  }, 500); // 每 500ms 检测一次,更频繁
-
-  // 页面卸载时清理
-  window.addEventListener('beforeunload', () => {
-    clearInterval(checkInterval);
-    if (debuggerInterval !== null) {
-      clearInterval(debuggerInterval);
-    }
-  });
-
-  // 额外的检测:监听窗口大小变化
-  let lastWidth = window.innerWidth;
-  let lastHeight = window.innerHeight;
-
-  window.addEventListener('resize', () => {
-    const currentWidth = window.innerWidth;
-    const currentHeight = window.innerHeight;
-
-    // 如果窗口尺寸变化很大,可能是开发者工具打开/关闭
-    if (Math.abs(currentWidth - lastWidth) > 200 || Math.abs(currentHeight - lastHeight) > 200) {
-      // 重新检测
-      const isOpen = detectDevTools();
-      if (isOpen && !devToolsOpen) {
-        devToolsOpen = true;
-        if (debuggerInterval === null) {
-          debuggerInterval = window.setInterval(() => {
-            debugger; // 循环执行 debugger
-          }, 50); // 更频繁的 debugger
-        }
-      }
-    }
-
-    lastWidth = currentWidth;
-    lastHeight = currentHeight;
-  });
-}
-

+ 74 - 10
src/views/login.vue

@@ -240,29 +240,38 @@ onMounted(() => {
   height: 100%;
   background-image: url('../assets/images/login-background.jpg');
   background-size: cover;
+  background-position: center;
 }
 
 .title-box {
   display: flex;
+  align-items: center;
+  gap: 8px;
 
   .title {
-    margin: 0px auto 30px auto;
+    margin: 0px auto 26px auto;
     text-align: center;
-    color: #707070;
+    color: var(--el-text-color-primary);
+    font-weight: 600;
+    letter-spacing: 0.5px;
   }
 
   :deep(.lang-select--style) {
     line-height: 0;
-    color: #7483a3;
+    color: var(--el-text-color-secondary);
   }
 }
 
 .login-form {
-  border-radius: 6px;
-  background: #ffffff;
-  width: 400px;
-  padding: 25px 25px 5px 25px;
+  border-radius: var(--app-radius-lg);
+  background: rgba(255, 255, 255, 0.94);
+  border: 1px solid rgba(255, 255, 255, 0.5);
+  width: min(420px, 90vw);
+  padding: 32px 30px 12px 30px;
   z-index: 1;
+  box-shadow: var(--app-shadow-lg);
+  backdrop-filter: blur(6px);
+  -webkit-backdrop-filter: blur(6px);
   .el-input {
     height: 40px;
     input {
@@ -283,14 +292,48 @@ onMounted(() => {
   color: #bfbfbf;
 }
 
+.login-form :deep(.el-input__wrapper) {
+  background-color: rgba(255, 255, 255, 0.9);
+}
+
+.login-form :deep(.el-input__wrapper.is-focus) {
+  box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
+}
+
+.login-form :deep(.el-button--primary) {
+  border-radius: var(--app-radius-md);
+  box-shadow: 0 8px 20px rgba(59, 130, 246, 0.25);
+}
+
+.login-form :deep(.el-button.is-circle) {
+  background: rgba(15, 23, 42, 0.04);
+  border: 1px solid rgba(15, 23, 42, 0.08);
+  color: var(--el-text-color-regular);
+}
+
+.login-form :deep(.el-button.is-circle:hover) {
+  background: rgba(59, 130, 246, 0.1);
+  border-color: rgba(59, 130, 246, 0.2);
+}
+
 .login-code {
-  width: 33%;
+  width: calc(37% - 10px);
   height: 40px;
   float: right;
+  margin-left: 10px;
+  box-sizing: border-box;
+  border-radius: var(--app-radius-sm);
+  overflow: hidden;
+  background: rgba(255, 255, 255, 0.9);
+  border: 1px solid var(--el-border-color-light);
 
   img {
     cursor: pointer;
     vertical-align: middle;
+    display: block;
+    width: 100%;
+    height: 40px;
+    object-fit: cover;
   }
 }
 
@@ -301,7 +344,7 @@ onMounted(() => {
   bottom: 0;
   width: 100%;
   text-align: center;
-  color: #fff;
+  color: rgba(255, 255, 255, 0.75);
   font-family: Arial, serif;
   font-size: 12px;
   letter-spacing: 1px;
@@ -309,6 +352,27 @@ onMounted(() => {
 
 .login-code-img {
   height: 40px;
-  padding-left: 12px;
+  padding-left: 0;
+}
+
+:global(html.dark) {
+  .login-form {
+    background: rgba(17, 24, 39, 0.9);
+    border-color: rgba(148, 163, 184, 0.2);
+  }
+
+  .login-form :deep(.el-input__wrapper) {
+    background-color: rgba(17, 24, 39, 0.7);
+  }
+
+  .login-form :deep(.el-button.is-circle) {
+    background: rgba(148, 163, 184, 0.12);
+    border-color: rgba(148, 163, 184, 0.25);
+    color: #e5e7eb;
+  }
+
+  .el-login-footer {
+    color: rgba(226, 232, 240, 0.65);
+  }
 }
 </style>

+ 0 - 309
src/views/system/dict/data.vue

@@ -1,309 +0,0 @@
-<template>
-  <div class="p-2">
-    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
-      <div v-show="showSearch" class="mb-[10px]">
-        <el-card shadow="hover">
-          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <el-form-item label="字典名称" prop="dictType">
-              <el-select v-model="queryParams.dictType">
-                <el-option v-for="item in typeOptions" :key="item.dictId" :label="item.dictName" :value="item.dictType" />
-              </el-select>
-            </el-form-item>
-            <el-form-item label="字典标签" prop="dictLabel">
-              <el-input v-model="queryParams.dictLabel" placeholder="请输入字典标签" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item>
-              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-            </el-form-item>
-          </el-form>
-        </el-card>
-      </div>
-    </transition>
-    <el-card shadow="hover">
-      <template #header>
-        <el-row :gutter="10" class="mb8">
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:dict:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:dict:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">修改</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:dict:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
-              删除
-            </el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:dict:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button type="warning" plain icon="Close" @click="handleClose">关闭</el-button>
-          </el-col>
-          <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
-        </el-row>
-      </template>
-
-      <el-table v-loading="loading" border :data="dataList" @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" align="center" />
-        <el-table-column v-if="false" label="字典编码" align="center" prop="dictCode" />
-        <el-table-column label="字典标签" align="center" prop="dictLabel">
-          <template #default="scope">
-            <span
-              v-if="(scope.row.listClass === '' || scope.row.listClass === 'default') && (scope.row.cssClass === '' || scope.row.cssClass == null)"
-              >{{ scope.row.dictLabel }}</span
-            >
-            <el-tag
-              v-else
-              :type="scope.row.listClass === 'primary' || scope.row.listClass === 'default' ? 'primary' : scope.row.listClass"
-              :class="scope.row.cssClass"
-              >{{ scope.row.dictLabel }}</el-tag
-            >
-          </template>
-        </el-table-column>
-        <el-table-column label="字典键值" align="center" prop="dictValue" />
-        <el-table-column label="字典排序" align="center" prop="dictSort" />
-        <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
-        <el-table-column label="创建时间" align="center" prop="createTime" width="180">
-          <template #default="scope">
-            <span>{{ proxy.parseTime(scope.row.createTime) }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
-          <template #default="scope">
-            <el-tooltip content="修改" placement="top">
-              <el-button v-hasPermi="['system:dict:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
-            </el-tooltip>
-            <el-tooltip content="删除" placement="top">
-              <el-button v-hasPermi="['system:dict:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
-            </el-tooltip>
-          </template>
-        </el-table-column>
-      </el-table>
-
-      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
-    </el-card>
-    <!-- 添加或修改参数配置对话框 -->
-    <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
-      <el-form ref="dataFormRef" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="字典类型">
-          <el-input v-model="form.dictType" :disabled="true" />
-        </el-form-item>
-        <el-form-item label="数据标签" prop="dictLabel">
-          <el-input v-model="form.dictLabel" placeholder="请输入数据标签" />
-        </el-form-item>
-        <el-form-item label="数据键值" prop="dictValue">
-          <el-input v-model="form.dictValue" placeholder="请输入数据键值" />
-        </el-form-item>
-        <el-form-item label="样式属性" prop="cssClass">
-          <el-input v-model="form.cssClass" placeholder="请输入样式属性" />
-        </el-form-item>
-        <el-form-item label="显示排序" prop="dictSort">
-          <el-input-number v-model="form.dictSort" controls-position="right" :min="0" />
-        </el-form-item>
-        <el-form-item label="回显样式" prop="listClass">
-          <el-select v-model="form.listClass">
-            <el-option
-              v-for="item in listClassOptions"
-              :key="item.value"
-              :label="item.label + '(' + item.value + ')'"
-              :value="item.value"
-            ></el-option>
-          </el-select>
-        </el-form-item>
-        <el-form-item label="备注" prop="remark">
-          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="primary" @click="submitForm">确 定</el-button>
-          <el-button @click="cancel">取 消</el-button>
-        </div>
-      </template>
-    </el-dialog>
-  </div>
-</template>
-
-<script setup name="Data" lang="ts">
-import { useDictStore } from '@/store/modules/dict';
-import { optionselect as getDictOptionselect, getType } from '@/api/system/dict/type';
-import { listData, getData, delData, addData, updateData } from '@/api/system/dict/data';
-import { DictTypeVO } from '@/api/system/dict/type/types';
-import { DictDataForm, DictDataQuery, DictDataVO } from '@/api/system/dict/data/types';
-import { RouteLocationNormalized } from 'vue-router';
-
-const { proxy } = getCurrentInstance() as ComponentInternalInstance;
-const route = useRoute();
-
-const dataList = ref<DictDataVO[]>([]);
-const loading = ref(true);
-const showSearch = ref(true);
-const ids = ref<Array<string | number>>([]);
-const single = ref(true);
-const multiple = ref(true);
-const total = ref(0);
-const defaultDictType = ref('');
-const typeOptions = ref<DictTypeVO[]>([]);
-
-const dataFormRef = ref<ElFormInstance>();
-const queryFormRef = ref<ElFormInstance>();
-
-const dialog = reactive<DialogOption>({
-  visible: false,
-  title: ''
-});
-
-// 数据标签回显样式
-const listClassOptions = ref<Array<{ value: string; label: string }>>([
-  { value: 'default', label: '默认' },
-  { value: 'primary', label: '主要' },
-  { value: 'success', label: '成功' },
-  { value: 'info', label: '信息' },
-  { value: 'warning', label: '警告' },
-  { value: 'danger', label: '危险' }
-]);
-
-const initFormData: DictDataForm = {
-  dictCode: undefined,
-  dictLabel: '',
-  dictValue: '',
-  cssClass: '',
-  listClass: 'primary',
-  dictSort: 0,
-  remark: ''
-};
-const data = reactive<PageData<DictDataForm, DictDataQuery>>({
-  form: { ...initFormData },
-  queryParams: {
-    pageNum: 1,
-    pageSize: 10,
-    dictName: '',
-    dictType: '',
-    dictLabel: ''
-  },
-  rules: {
-    dictLabel: [{ required: true, message: '数据标签不能为空', trigger: 'blur' }],
-    dictValue: [{ required: true, message: '数据键值不能为空', trigger: 'blur' }],
-    dictSort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }]
-  }
-});
-
-const { queryParams, form, rules } = toRefs(data);
-
-/** 查询字典类型详细 */
-const getTypes = async (dictId: string | number) => {
-  const { data } = await getType(dictId);
-  queryParams.value.dictType = data.dictType;
-  defaultDictType.value = data.dictType;
-  getList();
-};
-
-/** 查询字典类型列表 */
-const getTypeList = async () => {
-  const res = await getDictOptionselect();
-  typeOptions.value = res.data;
-};
-/** 查询字典数据列表 */
-const getList = async () => {
-  loading.value = true;
-  const res = await listData(queryParams.value);
-  dataList.value = res.rows;
-  total.value = res.total;
-  loading.value = false;
-};
-/** 取消按钮 */
-const cancel = () => {
-  dialog.visible = false;
-  reset();
-};
-/** 表单重置 */
-const reset = () => {
-  form.value = { ...initFormData };
-  dataFormRef.value?.resetFields();
-};
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.value.pageNum = 1;
-  getList();
-};
-/** 返回按钮操作 */
-const handleClose = () => {
-  const obj: RouteLocationNormalized = {
-    fullPath: '',
-    hash: '',
-    matched: [],
-    meta: undefined,
-    name: undefined,
-    params: undefined,
-    query: undefined,
-    redirectedFrom: undefined,
-    path: '/system/dict'
-  };
-  proxy?.$tab.closeOpenPage(obj);
-};
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value?.resetFields();
-  queryParams.value.dictType = defaultDictType.value;
-  handleQuery();
-};
-/** 新增按钮操作 */
-const handleAdd = () => {
-  reset();
-  form.value.dictType = queryParams.value.dictType;
-  dialog.visible = true;
-  dialog.title = '添加字典数据';
-};
-/** 多选框选中数据 */
-const handleSelectionChange = (selection: DictDataVO[]) => {
-  ids.value = selection.map((item) => item.dictCode);
-  single.value = selection.length != 1;
-  multiple.value = !selection.length;
-};
-/** 修改按钮操作 */
-const handleUpdate = async (row?: DictDataVO) => {
-  reset();
-  const dictCode = row?.dictCode || ids.value[0];
-  const res = await getData(dictCode);
-  Object.assign(form.value, res.data);
-  dialog.visible = true;
-  dialog.title = '修改字典数据';
-};
-/** 提交按钮 */
-const submitForm = () => {
-  dataFormRef.value?.validate(async (valid: boolean) => {
-    if (valid) {
-      form.value.dictCode ? await updateData(form.value) : await addData(form.value);
-      useDictStore().removeDict(queryParams.value.dictType);
-      proxy?.$modal.msgSuccess('操作成功');
-      dialog.visible = false;
-      await getList();
-    }
-  });
-};
-/** 删除按钮操作 */
-const handleDelete = async (row?: DictDataVO) => {
-  const dictCodes = row?.dictCode || ids.value;
-  await proxy?.$modal.confirm('是否确认删除字典编码为"' + dictCodes + '"的数据项?');
-  await delData(dictCodes);
-  await getList();
-  proxy?.$modal.msgSuccess('删除成功');
-  useDictStore().removeDict(queryParams.value.dictType);
-};
-/** 导出按钮操作 */
-const handleExport = () => {
-  proxy?.download(
-    'system/dict/data/export',
-    {
-      ...queryParams.value
-    },
-    `dict_data_${new Date().getTime()}.xlsx`
-  );
-};
-
-onMounted(() => {
-  getTypes(route.params && (route.params.dictId as string));
-  getTypeList();
-});
-</script>

+ 584 - 161
src/views/system/dict/index.vue

@@ -1,97 +1,219 @@
-<template>
-  <div class="p-2">
-    <transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
-      <div v-show="showSearch" class="mb-[10px]">
-        <el-card shadow="hover">
-          <el-form ref="queryFormRef" :model="queryParams" :inline="true">
-            <el-form-item label="字典名称" prop="dictName">
-              <el-input v-model="queryParams.dictName" placeholder="请输入字典名称" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="字典类型" prop="dictType">
-              <el-input v-model="queryParams.dictType" placeholder="请输入字典类型" clearable @keyup.enter="handleQuery" />
-            </el-form-item>
-            <el-form-item label="创建时间" style="width: 308px">
-              <el-date-picker
-                v-model="dateRange"
-                value-format="YYYY-MM-DD HH:mm:ss"
-                type="daterange"
-                range-separator="-"
-                start-placeholder="开始日期"
-                end-placeholder="结束日期"
-                :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
-              ></el-date-picker>
-            </el-form-item>
-            <el-form-item>
-              <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
-              <el-button icon="Refresh" @click="resetQuery">重置</el-button>
-            </el-form-item>
-          </el-form>
-        </el-card>
-      </div>
-    </transition>
-    <el-card shadow="hover">
-      <template #header>
-        <el-row :gutter="10" class="mb8">
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:dict:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:dict:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">修改</el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:dict:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
-              删除
-            </el-button>
-          </el-col>
-          <el-col :span="1.5">
-            <el-button v-hasPermi="['system:dict:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
-          </el-col>
-          <el-col :span="1.5">
+<template>
+  <div class="p-2 dict-page">
+    <el-row :gutter="16" class="dict-grid">
+      <!-- 字典类型 -->
+      <el-col :xs="24" :lg="12">
+        <el-card shadow="hover" class="dict-card">
+          <template #header>
+            <div class="dict-card__header">
+              <div class="dict-card__title">字典管理</div>
+              <right-toolbar v-model:show-search="showTypeSearch" @query-table="getTypeList" />
+            </div>
+          </template>
+
+          <div v-show="showTypeSearch" class="dict-form-scroll">
+            <el-form ref="typeQueryFormRef" :model="typeQueryParams" :inline="true">
+              <el-form-item label="字典名称" prop="dictName">
+                <el-input v-model="typeQueryParams.dictName" placeholder="请输入字典名称" clearable @keyup.enter="handleTypeQuery" />
+              </el-form-item>
+              <el-form-item label="字典类型" prop="dictType">
+                <el-input v-model="typeQueryParams.dictType" placeholder="请输入字典类型" clearable @keyup.enter="handleTypeQuery" />
+              </el-form-item>
+              <el-form-item label="创建时间" style="width: 308px">
+                <el-date-picker
+                  v-model="dateRange"
+                  value-format="YYYY-MM-DD HH:mm:ss"
+                  type="daterange"
+                  range-separator="-"
+                  start-placeholder="开始日期"
+                  end-placeholder="结束日期"
+                  :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
+                ></el-date-picker>
+              </el-form-item>
+              <el-form-item>
+                <el-button type="primary" icon="Search" @click="handleTypeQuery">搜索</el-button>
+                <el-button icon="Refresh" @click="handleTypeResetQuery">重置</el-button>
+              </el-form-item>
+            </el-form>
+          </div>
+
+          <div class="dict-actions">
+            <el-button v-hasPermi="['system:dict:add']" type="primary" plain icon="Plus" @click="handleTypeAdd">新增</el-button>
+            <el-button v-hasPermi="['system:dict:edit']" type="success" plain icon="Edit" :disabled="typeSingle" @click="handleTypeUpdate()"
+              >修改</el-button
+            >
+            <el-button v-hasPermi="['system:dict:remove']" type="danger" plain icon="Delete" :disabled="typeMultiple" @click="handleTypeDelete()"
+              >删除</el-button
+            >
+            <el-button v-hasPermi="['system:dict:export']" type="warning" plain icon="Download" @click="handleTypeExport">导出</el-button>
             <el-button v-hasPermi="['system:dict:remove']" type="danger" plain icon="Refresh" @click="handleRefreshCache">刷新缓存</el-button>
-          </el-col>
-          <right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
-        </el-row>
-      </template>
+          </div>
 
-      <el-table v-loading="loading" border :data="typeList" @selection-change="handleSelectionChange">
-        <el-table-column type="selection" width="55" align="center" />
-        <el-table-column v-if="false" label="字典编号" align="center" prop="dictId" />
-        <el-table-column label="字典名称" align="center" prop="dictName" :show-overflow-tooltip="true" />
-        <el-table-column label="字典类型" align="center" :show-overflow-tooltip="true">
-          <template #default="scope">
-            <router-link :to="'/system/dict-data/index/' + scope.row.dictId" class="link-type">
-              <span>{{ scope.row.dictType }}</span>
-            </router-link>
-          </template>
-        </el-table-column>
-        <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
-        <el-table-column label="创建时间" align="center" prop="createTime" width="180">
-          <template #default="scope">
-            <span>{{ proxy.parseTime(scope.row.createTime) }}</span>
-          </template>
-        </el-table-column>
-        <el-table-column label="操作" align="center" width="160" class-name="small-padding fixed-width">
-          <template #default="scope">
-            <el-tooltip content="修改" placement="top">
-              <el-button v-hasPermi="['system:dict:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
-            </el-tooltip>
-            <el-tooltip content="删除" placement="top">
-              <el-button v-hasPermi="['system:dict:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
-            </el-tooltip>
+          <div class="dict-table-wrap">
+            <el-table
+              ref="typeTableRef"
+              v-loading="typeLoading"
+              border
+              :data="typeList"
+              highlight-current-row
+              @row-click="handleTypeRowClick"
+              @selection-change="handleTypeSelectionChange"
+            >
+              <el-table-column type="selection" width="55" align="center" />
+              <el-table-column v-if="false" label="字典编号" align="center" prop="dictId" />
+              <el-table-column label="字典名称" align="center" prop="dictName" width="120" />
+              <el-table-column label="字典类型" align="center" prop="dictType" width="160">
+                <template #default="scope">
+                  <span class="link-type" @click.stop="handleTypeRowClick(scope.row)">{{ scope.row.dictType }}</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="备注" align="center" prop="remark" width="160"/>
+              <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+                <template #default="scope">
+                  <span>{{ proxy.parseTime(scope.row.createTime) }}</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" fixed="right" align="center" width="120" class-name="small-padding fixed-width">
+                <template #default="scope">
+                  <el-tooltip content="修改" placement="top">
+                    <el-button v-hasPermi="['system:dict:edit']" link type="primary" icon="Edit" @click="handleTypeUpdate(scope.row)"></el-button>
+                  </el-tooltip>
+                  <el-tooltip content="删除" placement="top">
+                    <el-button v-hasPermi="['system:dict:remove']" link type="primary" icon="Delete" @click="handleTypeDelete(scope.row)"></el-button>
+                  </el-tooltip>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+
+          <pagination
+            v-show="typeTotal > 0"
+            v-model:page="typeQueryParams.pageNum"
+            v-model:limit="typeQueryParams.pageSize"
+            :total="typeTotal"
+            @pagination="getTypeList"
+          />
+        </el-card>
+      </el-col>
+
+      <!-- 字典数据 -->
+      <el-col :xs="24" :lg="12">
+        <el-card shadow="hover" class="dict-card">
+          <template #header>
+            <div class="dict-card__header">
+              <div class="dict-card__title">
+                字典数据
+                <span class="dict-card__subtitle">{{ currentDictLabel }}</span>
+              </div>
+              <right-toolbar v-model:show-search="showDataSearch" @query-table="getDataList" />
+            </div>
           </template>
-        </el-table-column>
-      </el-table>
-
-      <pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
-    </el-card>
-    <!-- 添加或修改参数配置对话框 -->
-    <el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
-      <el-form ref="dictFormRef" :model="form" :rules="rules" label-width="100px">
+
+          <div v-show="showDataSearch" class="dict-form-scroll">
+            <el-form ref="dataQueryFormRef" :model="dataQueryParams" :inline="true">
+              <el-form-item label="字典标签" prop="dictLabel">
+                <el-input
+                  v-model="dataQueryParams.dictLabel"
+                  placeholder="请输入字典标签"
+                  clearable
+                  :disabled="!hasCurrentDict"
+                  @keyup.enter="handleDataQuery"
+                />
+              </el-form-item>
+              <el-form-item>
+                <el-button type="primary" icon="Search" :disabled="!hasCurrentDict" @click="handleDataQuery">搜索</el-button>
+                <el-button icon="Refresh" :disabled="!hasCurrentDict" @click="handleDataResetQuery">重置</el-button>
+              </el-form-item>
+            </el-form>
+          </div>
+
+          <div class="dict-actions">
+            <el-button v-hasPermi="['system:dict:add']" type="primary" plain icon="Plus" :disabled="!hasCurrentDict" @click="handleDataAdd"
+              >新增</el-button
+            >
+            <el-button
+              v-hasPermi="['system:dict:edit']"
+              type="success"
+              plain
+              icon="Edit"
+              :disabled="dataSingle || !hasCurrentDict"
+              @click="handleDataUpdate()"
+              >修改</el-button
+            >
+            <el-button
+              v-hasPermi="['system:dict:remove']"
+              type="danger"
+              plain
+              icon="Delete"
+              :disabled="dataMultiple || !hasCurrentDict"
+              @click="handleDataDelete()"
+              >删除</el-button
+            >
+            <el-button v-hasPermi="['system:dict:export']" type="warning" plain icon="Download" :disabled="!hasCurrentDict" @click="handleDataExport"
+              >导出</el-button
+            >
+          </div>
+
+          <div class="dict-table-wrap">
+            <el-table v-loading="dataLoading" border :data="dataList" @selection-change="handleDataSelectionChange">
+              <el-table-column type="selection" width="55" align="center" />
+              <el-table-column v-if="false" label="字典编码" align="center" prop="dictCode" />
+              <el-table-column label="字典标签" align="center" prop="dictLabel" width="80">
+                <template #default="scope">
+                  <span
+                    v-if="
+                      (scope.row.listClass === '' || scope.row.listClass === 'default') && (scope.row.cssClass === '' || scope.row.cssClass == null)
+                    "
+                    >{{ scope.row.dictLabel }}</span
+                  >
+                  <el-tag
+                    v-else
+                    :type="scope.row.listClass === 'primary' || scope.row.listClass === 'default' ? 'primary' : scope.row.listClass"
+                    :class="scope.row.cssClass"
+                    >{{ scope.row.dictLabel }}</el-tag
+                  >
+                </template>
+              </el-table-column>
+              <el-table-column label="字典键值" align="center" prop="dictValue" width="80" />
+              <el-table-column label="字典排序" align="center" prop="dictSort" width="80" />
+              <el-table-column label="备注" align="center" prop="remark" width="100" />
+              <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+                <template #default="scope">
+                  <span>{{ proxy.parseTime(scope.row.createTime) }}</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" fixed="right" align="center" width="120" class-name="small-padding fixed-width">
+                <template #default="scope">
+                  <el-tooltip content="修改" placement="top">
+                    <el-button v-hasPermi="['system:dict:edit']" link type="primary" icon="Edit" @click="handleDataUpdate(scope.row)"></el-button>
+                  </el-tooltip>
+                  <el-tooltip content="删除" placement="top">
+                    <el-button v-hasPermi="['system:dict:remove']" link type="primary" icon="Delete" @click="handleDataDelete(scope.row)"></el-button>
+                  </el-tooltip>
+                </template>
+              </el-table-column>
+            </el-table>
+          </div>
+
+          <pagination
+            v-show="dataTotal > 0"
+            v-model:page="dataQueryParams.pageNum"
+            v-model:limit="dataQueryParams.pageSize"
+            :total="dataTotal"
+            @pagination="getDataList"
+          />
+        </el-card>
+      </el-col>
+    </el-row>
+
+    <!-- 字典类型对话框 -->
+    <el-dialog v-model="typeDialog.visible" :title="typeDialog.title" width="500px" append-to-body>
+      <el-form ref="typeFormRef" :model="typeForm" :rules="typeRules" label-width="100px">
         <el-form-item label="字典名称" prop="dictName">
-          <el-input v-model="form.dictName" placeholder="请输入字典名称" />
+          <el-input v-model="typeForm.dictName" placeholder="请输入字典名称" />
         </el-form-item>
         <el-form-item prop="dictType">
-          <el-input v-model="form.dictType" placeholder="请输入字典类型" maxlength="100" />
+          <el-input v-model="typeForm.dictType" placeholder="请输入字典类型" maxlength="100" />
           <span slot="label">
             <el-tooltip content="数据存储中的Key值,如:sys_user_sex" placement="top">
               <i class="el-icon-question"></i>
@@ -100,13 +222,53 @@
           </span>
         </el-form-item>
         <el-form-item label="备注" prop="remark">
-          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
+          <el-input v-model="typeForm.remark" type="textarea" placeholder="请输入内容"></el-input>
         </el-form-item>
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button type="primary" @click="submitForm">确 定</el-button>
-          <el-button @click="cancel">取 消</el-button>
+          <el-button type="primary" @click="submitTypeForm">确 定</el-button>
+          <el-button @click="cancelType">取 消</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 字典数据对话框 -->
+    <el-dialog v-model="dataDialog.visible" :title="dataDialog.title" width="500px" append-to-body>
+      <el-form ref="dataFormRef" :model="dataForm" :rules="dataRules" label-width="80px">
+        <el-form-item label="字典类型">
+          <el-input v-model="dataForm.dictType" :disabled="true" />
+        </el-form-item>
+        <el-form-item label="数据标签" prop="dictLabel">
+          <el-input v-model="dataForm.dictLabel" placeholder="请输入数据标签" />
+        </el-form-item>
+        <el-form-item label="数据键值" prop="dictValue">
+          <el-input v-model="dataForm.dictValue" placeholder="请输入数据键值" />
+        </el-form-item>
+        <el-form-item label="样式属性" prop="cssClass">
+          <el-input v-model="dataForm.cssClass" placeholder="请输入样式属性" />
+        </el-form-item>
+        <el-form-item label="显示排序" prop="dictSort">
+          <el-input-number v-model="dataForm.dictSort" controls-position="right" :min="0" />
+        </el-form-item>
+        <el-form-item label="回显样式" prop="listClass">
+          <el-select v-model="dataForm.listClass">
+            <el-option
+              v-for="item in listClassOptions"
+              :key="item.value"
+              :label="item.label + '(' + item.value + ')'"
+              :value="item.value"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="dataForm.remark" type="textarea" placeholder="请输入内容"></el-input>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitDataForm">确 定</el-button>
+          <el-button @click="cancelData">取 消</el-button>
         </div>
       </template>
     </el-dialog>
@@ -116,35 +278,39 @@
 <script setup name="Dict" lang="ts">
 import { useDictStore } from '@/store/modules/dict';
 import { listType, getType, delType, addType, updateType, refreshCache } from '@/api/system/dict/type';
+import { listData, getData, delData, addData, updateData } from '@/api/system/dict/data';
 import { DictTypeForm, DictTypeQuery, DictTypeVO } from '@/api/system/dict/type/types';
+import { DictDataForm, DictDataQuery, DictDataVO } from '@/api/system/dict/data/types';
 
 const { proxy } = getCurrentInstance() as ComponentInternalInstance;
 
 const typeList = ref<DictTypeVO[]>([]);
-const loading = ref(true);
-const showSearch = ref(true);
-const ids = ref<Array<number | string>>([]);
-const single = ref(true);
-const multiple = ref(true);
-const total = ref(0);
+const typeLoading = ref(true);
+const showTypeSearch = ref(true);
+const typeIds = ref<Array<number | string>>([]);
+const typeSingle = ref(true);
+const typeMultiple = ref(true);
+const typeTotal = ref(0);
 const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
 
-const dictFormRef = ref<ElFormInstance>();
-const queryFormRef = ref<ElFormInstance>();
+const typeFormRef = ref<ElFormInstance>();
+const typeQueryFormRef = ref<ElFormInstance>();
+const typeTableRef = ref<ElTableInstance>();
 
-const dialog = reactive<DialogOption>({
+const typeDialog = reactive<DialogOption>({
   visible: false,
   title: ''
 });
 
-const initFormData: DictTypeForm = {
+const typeInitFormData: DictTypeForm = {
   dictId: undefined,
   dictName: '',
   dictType: '',
   remark: ''
 };
-const data = reactive<PageData<DictTypeForm, DictTypeQuery>>({
-  form: { ...initFormData },
+
+const typeState = reactive<PageData<DictTypeForm, DictTypeQuery>>({
+  form: { ...typeInitFormData },
   queryParams: {
     pageNum: 1,
     pageSize: 10,
@@ -157,96 +323,353 @@ const data = reactive<PageData<DictTypeForm, DictTypeQuery>>({
   }
 });
 
-const { queryParams, form, rules } = toRefs(data);
+const { queryParams: typeQueryParams, form: typeForm, rules: typeRules } = toRefs(typeState);
+
+const currentDict = ref<DictTypeVO | null>(null);
+const hasCurrentDict = computed(() => !!currentDict.value);
+const currentDictLabel = computed(() => {
+  if (!currentDict.value) return '请先选择字典';
+  return `${currentDict.value.dictName} / ${currentDict.value.dictType}`;
+});
+
+const dataList = ref<DictDataVO[]>([]);
+const dataLoading = ref(false);
+const showDataSearch = ref(true);
+const dataIds = ref<Array<string | number>>([]);
+const dataSingle = ref(true);
+const dataMultiple = ref(true);
+const dataTotal = ref(0);
+
+const dataFormRef = ref<ElFormInstance>();
+const dataQueryFormRef = ref<ElFormInstance>();
+
+const dataDialog = reactive<DialogOption>({
+  visible: false,
+  title: ''
+});
+
+const listClassOptions = ref<Array<{ value: string; label: string }>>([
+  { value: 'default', label: '默认' },
+  { value: 'primary', label: '主要' },
+  { value: 'success', label: '成功' },
+  { value: 'info', label: '信息' },
+  { value: 'warning', label: '警告' },
+  { value: 'danger', label: '危险' }
+]);
+
+const dataInitFormData: DictDataForm = {
+  dictCode: undefined,
+  dictLabel: '',
+  dictValue: '',
+  cssClass: '',
+  listClass: 'primary',
+  dictSort: 0,
+  remark: ''
+};
 
-/** 查询字典类型列表 */
-const getList = () => {
-  loading.value = true;
-  listType(proxy?.addDateRange(queryParams.value, dateRange.value)).then((res) => {
+const dataState = reactive<PageData<DictDataForm, DictDataQuery>>({
+  form: { ...dataInitFormData },
+  queryParams: {
+    pageNum: 1,
+    pageSize: 10,
+    dictName: '',
+    dictType: '',
+    dictLabel: ''
+  },
+  rules: {
+    dictLabel: [{ required: true, message: '数据标签不能为空', trigger: 'blur' }],
+    dictValue: [{ required: true, message: '数据键值不能为空', trigger: 'blur' }],
+    dictSort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }]
+  }
+});
+
+const { queryParams: dataQueryParams, form: dataForm, rules: dataRules } = toRefs(dataState);
+
+const getTypeList = () => {
+  typeLoading.value = true;
+  listType(proxy?.addDateRange(typeQueryParams.value, dateRange.value)).then((res) => {
     typeList.value = res.rows;
-    total.value = res.total;
-    loading.value = false;
+    typeTotal.value = res.total;
+    typeLoading.value = false;
+    ensureCurrentType();
   });
 };
-/** 取消按钮 */
-const cancel = () => {
-  reset();
-  dialog.visible = false;
+
+const ensureCurrentType = () => {
+  if (!typeList.value.length) {
+    currentDict.value = null;
+    dataQueryParams.value.dictType = '';
+    dataList.value = [];
+    dataTotal.value = 0;
+    return;
+  }
+
+  const current = currentDict.value && typeList.value.find((item) => item.dictId === currentDict.value?.dictId);
+  const nextRow = current || typeList.value[0];
+  setCurrentType(nextRow);
+};
+
+const setCurrentType = (row: DictTypeVO) => {
+  currentDict.value = row;
+  dataQueryParams.value.dictType = row.dictType;
+  dataQueryParams.value.pageNum = 1;
+  dataQueryParams.value.dictLabel = '';
+  getDataList();
+  nextTick(() => typeTableRef.value?.setCurrentRow(row));
+};
+
+const handleTypeRowClick = (row: DictTypeVO) => {
+  setCurrentType(row);
+};
+
+const cancelType = () => {
+  resetTypeForm();
+  typeDialog.visible = false;
 };
-/** 表单重置 */
-const reset = () => {
-  form.value = { ...initFormData };
-  dictFormRef.value?.resetFields();
+
+const resetTypeForm = () => {
+  typeForm.value = { ...typeInitFormData };
+  typeFormRef.value?.resetFields();
 };
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  queryParams.value.pageNum = 1;
-  getList();
+
+const handleTypeQuery = () => {
+  typeQueryParams.value.pageNum = 1;
+  getTypeList();
 };
-/** 重置按钮操作 */
-const resetQuery = () => {
+
+const handleTypeResetQuery = () => {
   dateRange.value = ['', ''];
-  queryFormRef.value?.resetFields();
-  handleQuery();
-};
-/** 新增按钮操作 */
-const handleAdd = () => {
-  reset();
-  dialog.visible = true;
-  dialog.title = '添加字典类型';
-};
-/** 多选框选中数据 */
-const handleSelectionChange = (selection: DictTypeVO[]) => {
-  ids.value = selection.map((item) => item.dictId);
-  single.value = selection.length != 1;
-  multiple.value = !selection.length;
-};
-/** 修改按钮操作 */
-const handleUpdate = async (row?: DictTypeVO) => {
-  reset();
-  const dictId = row?.dictId || ids.value[0];
+  typeQueryFormRef.value?.resetFields();
+  handleTypeQuery();
+};
+
+const handleTypeAdd = () => {
+  resetTypeForm();
+  typeDialog.visible = true;
+  typeDialog.title = '添加字典类型';
+};
+
+const handleTypeSelectionChange = (selection: DictTypeVO[]) => {
+  typeIds.value = selection.map((item) => item.dictId);
+  typeSingle.value = selection.length != 1;
+  typeMultiple.value = !selection.length;
+};
+
+const handleTypeUpdate = async (row?: DictTypeVO) => {
+  resetTypeForm();
+  const dictId = row?.dictId || typeIds.value[0];
   const res = await getType(dictId);
-  Object.assign(form.value, res.data);
-  dialog.visible = true;
-  dialog.title = '修改字典类型';
+  Object.assign(typeForm.value, res.data);
+  typeDialog.visible = true;
+  typeDialog.title = '修改字典类型';
 };
-/** 提交按钮 */
-const submitForm = () => {
-  dictFormRef.value?.validate(async (valid: boolean) => {
+
+const submitTypeForm = () => {
+  typeFormRef.value?.validate(async (valid: boolean) => {
     if (valid) {
-      form.value.dictId ? await updateType(form.value) : await addType(form.value);
+      typeForm.value.dictId ? await updateType(typeForm.value) : await addType(typeForm.value);
       proxy?.$modal.msgSuccess('操作成功');
-      dialog.visible = false;
-      getList();
+      typeDialog.visible = false;
+      getTypeList();
     }
   });
 };
-/** 删除按钮操作 */
-const handleDelete = async (row?: DictTypeVO) => {
-  const dictIds = row?.dictId || ids.value;
+
+const handleTypeDelete = async (row?: DictTypeVO) => {
+  const dictIds = row?.dictId || typeIds.value;
   await proxy?.$modal.confirm('是否确认删除字典编号为"' + dictIds + '"的数据项?');
   await delType(dictIds);
-  getList();
+  getTypeList();
   proxy?.$modal.msgSuccess('删除成功');
 };
-/** 导出按钮操作 */
-const handleExport = () => {
+
+const handleTypeExport = () => {
   proxy?.download(
     'system/dict/type/export',
     {
-      ...queryParams.value
+      ...typeQueryParams.value
     },
     `dict_${new Date().getTime()}.xlsx`
   );
 };
-/** 刷新缓存按钮操作 */
+
 const handleRefreshCache = async () => {
   await refreshCache();
   proxy?.$modal.msgSuccess('刷新成功');
   useDictStore().cleanDict();
 };
 
+const getDataList = async () => {
+  if (!currentDict.value) {
+    dataList.value = [];
+    dataTotal.value = 0;
+    dataLoading.value = false;
+    return;
+  }
+  dataLoading.value = true;
+  const res = await listData(dataQueryParams.value);
+  dataList.value = res.rows;
+  dataTotal.value = res.total;
+  dataLoading.value = false;
+};
+
+const cancelData = () => {
+  dataDialog.visible = false;
+  resetDataForm();
+};
+
+const resetDataForm = () => {
+  dataForm.value = { ...dataInitFormData };
+  dataFormRef.value?.resetFields();
+};
+
+const handleDataQuery = () => {
+  if (!currentDict.value) return;
+  dataQueryParams.value.pageNum = 1;
+  getDataList();
+};
+
+const handleDataResetQuery = () => {
+  dataQueryFormRef.value?.resetFields();
+  dataQueryParams.value.dictLabel = '';
+  handleDataQuery();
+};
+
+const handleDataAdd = () => {
+  if (!currentDict.value) {
+    proxy?.$modal.msgWarning('请先选择字典');
+    return;
+  }
+  resetDataForm();
+  dataForm.value.dictType = currentDict.value.dictType;
+  dataDialog.visible = true;
+  dataDialog.title = '添加字典数据';
+};
+
+const handleDataSelectionChange = (selection: DictDataVO[]) => {
+  dataIds.value = selection.map((item) => item.dictCode);
+  dataSingle.value = selection.length != 1;
+  dataMultiple.value = !selection.length;
+};
+
+const handleDataUpdate = async (row?: DictDataVO) => {
+  if (!currentDict.value) {
+    proxy?.$modal.msgWarning('请先选择字典');
+    return;
+  }
+  resetDataForm();
+  const dictCode = row?.dictCode || dataIds.value[0];
+  const res = await getData(dictCode);
+  Object.assign(dataForm.value, res.data);
+  dataDialog.visible = true;
+  dataDialog.title = '修改字典数据';
+};
+
+const submitDataForm = () => {
+  dataFormRef.value?.validate(async (valid: boolean) => {
+    if (valid) {
+      dataForm.value.dictCode ? await updateData(dataForm.value) : await addData(dataForm.value);
+      useDictStore().removeDict(dataQueryParams.value.dictType);
+      proxy?.$modal.msgSuccess('操作成功');
+      dataDialog.visible = false;
+      await getDataList();
+    }
+  });
+};
+
+const handleDataDelete = async (row?: DictDataVO) => {
+  if (!currentDict.value) {
+    proxy?.$modal.msgWarning('请先选择字典');
+    return;
+  }
+  const dictCodes = row?.dictCode || dataIds.value;
+  await proxy?.$modal.confirm('是否确认删除字典编码为"' + dictCodes + '"的数据项?');
+  await delData(dictCodes);
+  await getDataList();
+  proxy?.$modal.msgSuccess('删除成功');
+  useDictStore().removeDict(dataQueryParams.value.dictType);
+};
+
+const handleDataExport = () => {
+  if (!currentDict.value) {
+    proxy?.$modal.msgWarning('请先选择字典');
+    return;
+  }
+  proxy?.download(
+    'system/dict/data/export',
+    {
+      ...dataQueryParams.value
+    },
+    `dict_data_${new Date().getTime()}.xlsx`
+  );
+};
+
 onMounted(() => {
-  getList();
+  getTypeList();
 });
 </script>
+
+<style lang="scss" scoped>
+.dict-grid {
+  row-gap: 16px;
+}
+
+.dict-card__header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 8px;
+}
+
+.dict-card__title {
+  display: inline-flex;
+  align-items: baseline;
+  gap: 8px;
+  font-weight: 600;
+}
+
+.dict-card__subtitle {
+  font-size: 12px;
+  color: var(--el-text-color-secondary);
+}
+
+.dict-form-scroll {
+  max-height: 200px;
+  overflow: auto;
+  margin-bottom: 12px;
+  padding-right: 6px;
+  padding-bottom: 4px;
+}
+
+.dict-form-scroll :deep(.el-form) {
+  display: flex;
+  flex-wrap: wrap;
+  column-gap: 12px;
+  row-gap: 10px;
+}
+
+.dict-form-scroll :deep(.el-form-item) {
+  margin: 0;
+}
+
+.dict-actions {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 10px;
+  margin: 8px 0 12px;
+}
+
+.dict-actions :deep(.el-button) {
+  height: 32px;
+  padding: 0 14px;
+}
+
+.dict-actions :deep(.el-button + .el-button) {
+  margin-left: 0;
+}
+
+.dict-table-wrap {
+  overflow-x: auto;
+}
+
+</style>

+ 62 - 2
src/views/workflow/task/allTaskWaiting.vue

@@ -27,8 +27,12 @@
       <template #header>
         <el-row :gutter="10" class="mb8">
           <el-col :span="1.5" v-if="tab === 'waiting'">
-            <el-button type="primary" plain icon="Edit" :disabled="multiple" @click="handleUserOpen()">修改办理人 </el-button>
-            <el-button type="primary" plain icon="Bell" :disabled="multiple" @click="handleUrgeTaskOpen()">催办 </el-button>
+            <el-button class="todo-action-btn todo-action-btn--primary" type="primary" plain icon="Edit" :disabled="multiple" @click="handleUserOpen()"
+              >修改办理人
+            </el-button>
+            <el-button class="todo-action-btn todo-action-btn--warning" type="warning" plain icon="Bell" :disabled="multiple" @click="handleUrgeTaskOpen()"
+              >催办
+            </el-button>
           </el-col>
           <right-toolbar v-model:show-search="showSearch" @query-table="handleQuery"></right-toolbar>
         </el-row>
@@ -277,3 +281,59 @@ onMounted(() => {
   getWaitingList();
 });
 </script>
+
+<style lang="scss" scoped>
+.todo-action-btn {
+  height: 32px;
+  padding: 0 14px;
+  border-radius: var(--app-radius-md);
+  font-weight: 600;
+  --el-button-bg-color: var(--el-fill-color-lighter);
+  --el-button-border-color: var(--el-border-color-light);
+  --el-button-text-color: var(--el-text-color-regular);
+  --el-button-hover-bg-color: var(--el-fill-color-light);
+  --el-button-hover-border-color: var(--el-border-color);
+}
+
+.todo-action-btn--primary {
+  --el-button-text-color: var(--el-color-primary);
+  --el-button-border-color: var(--el-color-primary-light-5);
+  --el-button-bg-color: var(--el-color-primary-light-9);
+  --el-button-hover-bg-color: var(--el-color-primary-light-8);
+  --el-button-hover-border-color: var(--el-color-primary-light-5);
+}
+
+.todo-action-btn--warning {
+  --el-button-text-color: var(--el-color-warning);
+  --el-button-border-color: var(--el-color-warning-light-5);
+  --el-button-bg-color: var(--el-color-warning-light-9);
+  --el-button-hover-bg-color: var(--el-color-warning-light-8);
+  --el-button-hover-border-color: var(--el-color-warning-light-5);
+}
+
+:global(html.dark) {
+  .todo-action-btn {
+    --el-button-bg-color: rgba(148, 163, 184, 0.12);
+    --el-button-border-color: rgba(148, 163, 184, 0.3);
+    --el-button-text-color: var(--el-text-color-regular);
+    --el-button-hover-bg-color: rgba(148, 163, 184, 0.2);
+    --el-button-hover-border-color: rgba(148, 163, 184, 0.45);
+  }
+
+  .todo-action-btn--primary {
+    --el-button-bg-color: rgba(99, 113, 150, 0.14);
+    --el-button-border-color: rgba(99, 113, 150, 0.45);
+    --el-button-text-color: #c4cfdd;
+    --el-button-hover-bg-color: rgba(99, 113, 150, 0.22);
+    --el-button-hover-border-color: rgba(99, 113, 150, 0.65);
+  }
+
+  .todo-action-btn--warning {
+    --el-button-bg-color: rgba(214, 149, 59, 0.16);
+    --el-button-border-color: rgba(214, 149, 59, 0.55);
+    --el-button-text-color: #ffe6bb;
+    --el-button-hover-bg-color: rgba(214, 149, 59, 0.24);
+    --el-button-hover-border-color: rgba(214, 149, 59, 0.75);
+  }
+}
+</style>

+ 0 - 1
vite.config.ts

@@ -36,7 +36,6 @@ export default defineConfig(({ mode, command }) => {
         scss: {
           // additionalData: '@use "@/assets/styles/variables.module.scss as *";'
           // javascriptEnabled: true
-          api: 'modern-compiler'
         }
       },
       postcss: {