Quellcode durchsuchen

feat: 集成超级列表页

Gaokun Wang vor 1 Monat
Ursprung
Commit
31d83ed2ed

+ 4 - 4
package.json

@@ -24,10 +24,9 @@
   },
   "dependencies": {
     "@element-plus/icons-vue": "^2.3.1",
-    "@types/node-forge": "^1.3.12",
     "@vueuse/core": "^13.5.0",
     "axios": "^1.10.0",
-    "element-plus": "^2.10.3",
+    "element-plus": "^2.10.4",
     "nprogress": "^0.2.0",
     "pinia": "^3.0.3",
     "pinia-plugin-persistedstate": "^4.4.1",
@@ -38,8 +37,9 @@
   "devDependencies": {
     "@commitlint/cli": "^19.8.1",
     "@commitlint/config-conventional": "^19.8.1",
-    "@eslint/js": "^9.30.1",
+    "@eslint/js": "^9.31.0",
     "@types/node": "^24.0.13",
+    "@types/node-forge": "^1.3.13",
     "@types/nprogress": "^0.2.3",
     "@vitejs/plugin-vue": "^6.0.0",
     "@vitejs/plugin-vue-jsx": "^5.0.1",
@@ -48,7 +48,7 @@
     "@vue/tsconfig": "^0.7.0",
     "commitizen": "^4.3.1",
     "cz-git": "^1.12.0",
-    "eslint": "^9.30.1",
+    "eslint": "^9.31.0",
     "eslint-plugin-prettier": "^5.5.1",
     "eslint-plugin-vue": "^10.3.0",
     "husky": "^9.1.7",

+ 175 - 183
pnpm-lock.yaml

@@ -11,9 +11,6 @@ importers:
       '@element-plus/icons-vue':
         specifier: ^2.3.1
         version: 2.3.1(vue@3.5.17(typescript@5.8.3))
-      '@types/node-forge':
-        specifier: ^1.3.12
-        version: 1.3.12
       '@vueuse/core':
         specifier: ^13.5.0
         version: 13.5.0(vue@3.5.17(typescript@5.8.3))
@@ -21,8 +18,8 @@ importers:
         specifier: ^1.10.0
         version: 1.10.0
       element-plus:
-        specifier: ^2.10.3
-        version: 2.10.3(vue@3.5.17(typescript@5.8.3))
+        specifier: ^2.10.4
+        version: 2.10.4(vue@3.5.17(typescript@5.8.3))
       nprogress:
         specifier: ^0.2.0
         version: 0.2.0
@@ -37,7 +34,7 @@ importers:
         version: 3.5.17(typescript@5.8.3)
       vue-eslint-parser:
         specifier: ^10.2.0
-        version: 10.2.0(eslint@9.30.1(jiti@2.4.2))
+        version: 10.2.0(eslint@9.31.0(jiti@2.4.2))
       vue-router:
         specifier: ^4.5.1
         version: 4.5.1(vue@3.5.17(typescript@5.8.3))
@@ -49,11 +46,14 @@ importers:
         specifier: ^19.8.1
         version: 19.8.1
       '@eslint/js':
-        specifier: ^9.30.1
-        version: 9.30.1
+        specifier: ^9.31.0
+        version: 9.31.0
       '@types/node':
         specifier: ^24.0.13
         version: 24.0.13
+      '@types/node-forge':
+        specifier: ^1.3.13
+        version: 1.3.13
       '@types/nprogress':
         specifier: ^0.2.3
         version: 0.2.3
@@ -65,10 +65,10 @@ importers:
         version: 5.0.1(vite@7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(yaml@2.8.0))(vue@3.5.17(typescript@5.8.3))
       '@vue/eslint-config-prettier':
         specifier: ^10.2.0
-        version: 10.2.0(eslint@9.30.1(jiti@2.4.2))(prettier@3.6.2)
+        version: 10.2.0(eslint@9.31.0(jiti@2.4.2))(prettier@3.6.2)
       '@vue/eslint-config-typescript':
         specifier: ^14.6.0
-        version: 14.6.0(eslint-plugin-vue@10.3.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.30.1(jiti@2.4.2))))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
+        version: 14.6.0(eslint-plugin-vue@10.3.0(@typescript-eslint/parser@8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.31.0(jiti@2.4.2))))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
       '@vue/tsconfig':
         specifier: ^0.7.0
         version: 0.7.0(typescript@5.8.3)(vue@3.5.17(typescript@5.8.3))
@@ -79,14 +79,14 @@ importers:
         specifier: ^1.12.0
         version: 1.12.0
       eslint:
-        specifier: ^9.30.1
-        version: 9.30.1(jiti@2.4.2)
+        specifier: ^9.31.0
+        version: 9.31.0(jiti@2.4.2)
       eslint-plugin-prettier:
         specifier: ^5.5.1
-        version: 5.5.1(eslint-config-prettier@10.1.5(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2))(prettier@3.6.2)
+        version: 5.5.1(eslint-config-prettier@10.1.5(eslint@9.31.0(jiti@2.4.2)))(eslint@9.31.0(jiti@2.4.2))(prettier@3.6.2)
       eslint-plugin-vue:
         specifier: ^10.3.0
-        version: 10.3.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.30.1(jiti@2.4.2)))
+        version: 10.3.0(@typescript-eslint/parser@8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.31.0(jiti@2.4.2)))
       husky:
         specifier: ^9.1.7
         version: 9.1.7
@@ -131,7 +131,7 @@ importers:
         version: 5.8.3
       typescript-eslint:
         specifier: ^8.36.0
-        version: 8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
+        version: 8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
       unplugin-auto-import:
         specifier: ^19.3.0
         version: 19.3.0(@vueuse/core@13.5.0(vue@3.5.17(typescript@5.8.3)))
@@ -282,8 +282,8 @@ packages:
     resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==}
     engines: {node: '>=6.9.0'}
 
-  '@babel/types@7.28.0':
-    resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==}
+  '@babel/types@7.28.1':
+    resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==}
     engines: {node: '>=6.9.0'}
 
   '@commitlint/cli@19.8.1':
@@ -564,10 +564,6 @@ packages:
     resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@eslint/core@0.14.0':
-    resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==}
-    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
-
   '@eslint/core@0.15.1':
     resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -576,8 +572,8 @@ packages:
     resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@eslint/js@9.30.1':
-    resolution: {integrity: sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==}
+  '@eslint/js@9.31.0':
+    resolution: {integrity: sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   '@eslint/object-schema@2.1.6':
@@ -746,117 +742,117 @@ packages:
   '@rolldown/pluginutils@1.0.0-beta.19':
     resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==}
 
-  '@rolldown/pluginutils@1.0.0-beta.26':
-    resolution: {integrity: sha512-r/5po89voz/QRPDmoErL10+hVuTAuz1SHvokx+yWBlOIPB5C41jC7QhLqq9kaebx/+EHyoV3z22/qBfX81Ns8A==}
+  '@rolldown/pluginutils@1.0.0-beta.27':
+    resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
 
-  '@rollup/rollup-android-arm-eabi@4.44.2':
-    resolution: {integrity: sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==}
+  '@rollup/rollup-android-arm-eabi@4.45.0':
+    resolution: {integrity: sha512-2o/FgACbji4tW1dzXOqAV15Eu7DdgbKsF2QKcxfG4xbh5iwU7yr5RRP5/U+0asQliSYv5M4o7BevlGIoSL0LXg==}
     cpu: [arm]
     os: [android]
 
-  '@rollup/rollup-android-arm64@4.44.2':
-    resolution: {integrity: sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==}
+  '@rollup/rollup-android-arm64@4.45.0':
+    resolution: {integrity: sha512-PSZ0SvMOjEAxwZeTx32eI/j5xSYtDCRxGu5k9zvzoY77xUNssZM+WV6HYBLROpY5CkXsbQjvz40fBb7WPwDqtQ==}
     cpu: [arm64]
     os: [android]
 
-  '@rollup/rollup-darwin-arm64@4.44.2':
-    resolution: {integrity: sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==}
+  '@rollup/rollup-darwin-arm64@4.45.0':
+    resolution: {integrity: sha512-BA4yPIPssPB2aRAWzmqzQ3y2/KotkLyZukVB7j3psK/U3nVJdceo6qr9pLM2xN6iRP/wKfxEbOb1yrlZH6sYZg==}
     cpu: [arm64]
     os: [darwin]
 
-  '@rollup/rollup-darwin-x64@4.44.2':
-    resolution: {integrity: sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==}
+  '@rollup/rollup-darwin-x64@4.45.0':
+    resolution: {integrity: sha512-Pr2o0lvTwsiG4HCr43Zy9xXrHspyMvsvEw4FwKYqhli4FuLE5FjcZzuQ4cfPe0iUFCvSQG6lACI0xj74FDZKRA==}
     cpu: [x64]
     os: [darwin]
 
-  '@rollup/rollup-freebsd-arm64@4.44.2':
-    resolution: {integrity: sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==}
+  '@rollup/rollup-freebsd-arm64@4.45.0':
+    resolution: {integrity: sha512-lYE8LkE5h4a/+6VnnLiL14zWMPnx6wNbDG23GcYFpRW1V9hYWHAw9lBZ6ZUIrOaoK7NliF1sdwYGiVmziUF4vA==}
     cpu: [arm64]
     os: [freebsd]
 
-  '@rollup/rollup-freebsd-x64@4.44.2':
-    resolution: {integrity: sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==}
+  '@rollup/rollup-freebsd-x64@4.45.0':
+    resolution: {integrity: sha512-PVQWZK9sbzpvqC9Q0GlehNNSVHR+4m7+wET+7FgSnKG3ci5nAMgGmr9mGBXzAuE5SvguCKJ6mHL6vq1JaJ/gvw==}
     cpu: [x64]
     os: [freebsd]
 
-  '@rollup/rollup-linux-arm-gnueabihf@4.44.2':
-    resolution: {integrity: sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==}
+  '@rollup/rollup-linux-arm-gnueabihf@4.45.0':
+    resolution: {integrity: sha512-hLrmRl53prCcD+YXTfNvXd776HTxNh8wPAMllusQ+amcQmtgo3V5i/nkhPN6FakW+QVLoUUr2AsbtIRPFU3xIA==}
     cpu: [arm]
     os: [linux]
     libc: [glibc]
 
-  '@rollup/rollup-linux-arm-musleabihf@4.44.2':
-    resolution: {integrity: sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==}
+  '@rollup/rollup-linux-arm-musleabihf@4.45.0':
+    resolution: {integrity: sha512-XBKGSYcrkdiRRjl+8XvrUR3AosXU0NvF7VuqMsm7s5nRy+nt58ZMB19Jdp1RdqewLcaYnpk8zeVs/4MlLZEJxw==}
     cpu: [arm]
     os: [linux]
     libc: [musl]
 
-  '@rollup/rollup-linux-arm64-gnu@4.44.2':
-    resolution: {integrity: sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==}
+  '@rollup/rollup-linux-arm64-gnu@4.45.0':
+    resolution: {integrity: sha512-fRvZZPUiBz7NztBE/2QnCS5AtqLVhXmUOPj9IHlfGEXkapgImf4W9+FSkL8cWqoAjozyUzqFmSc4zh2ooaeF6g==}
     cpu: [arm64]
     os: [linux]
     libc: [glibc]
 
-  '@rollup/rollup-linux-arm64-musl@4.44.2':
-    resolution: {integrity: sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==}
+  '@rollup/rollup-linux-arm64-musl@4.45.0':
+    resolution: {integrity: sha512-Btv2WRZOcUGi8XU80XwIvzTg4U6+l6D0V6sZTrZx214nrwxw5nAi8hysaXj/mctyClWgesyuxbeLylCBNauimg==}
     cpu: [arm64]
     os: [linux]
     libc: [musl]
 
-  '@rollup/rollup-linux-loongarch64-gnu@4.44.2':
-    resolution: {integrity: sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==}
+  '@rollup/rollup-linux-loongarch64-gnu@4.45.0':
+    resolution: {integrity: sha512-Li0emNnwtUZdLwHjQPBxn4VWztcrw/h7mgLyHiEI5Z0MhpeFGlzaiBHpSNVOMB/xucjXTTcO+dhv469Djr16KA==}
     cpu: [loong64]
     os: [linux]
     libc: [glibc]
 
-  '@rollup/rollup-linux-powerpc64le-gnu@4.44.2':
-    resolution: {integrity: sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==}
+  '@rollup/rollup-linux-powerpc64le-gnu@4.45.0':
+    resolution: {integrity: sha512-sB8+pfkYx2kvpDCfd63d5ScYT0Fz1LO6jIb2zLZvmK9ob2D8DeVqrmBDE0iDK8KlBVmsTNzrjr3G1xV4eUZhSw==}
     cpu: [ppc64]
     os: [linux]
     libc: [glibc]
 
-  '@rollup/rollup-linux-riscv64-gnu@4.44.2':
-    resolution: {integrity: sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==}
+  '@rollup/rollup-linux-riscv64-gnu@4.45.0':
+    resolution: {integrity: sha512-5GQ6PFhh7E6jQm70p1aW05G2cap5zMOvO0se5JMecHeAdj5ZhWEHbJ4hiKpfi1nnnEdTauDXxPgXae/mqjow9w==}
     cpu: [riscv64]
     os: [linux]
     libc: [glibc]
 
-  '@rollup/rollup-linux-riscv64-musl@4.44.2':
-    resolution: {integrity: sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==}
+  '@rollup/rollup-linux-riscv64-musl@4.45.0':
+    resolution: {integrity: sha512-N/euLsBd1rekWcuduakTo/dJw6U6sBP3eUq+RXM9RNfPuWTvG2w/WObDkIvJ2KChy6oxZmOSC08Ak2OJA0UiAA==}
     cpu: [riscv64]
     os: [linux]
     libc: [musl]
 
-  '@rollup/rollup-linux-s390x-gnu@4.44.2':
-    resolution: {integrity: sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==}
+  '@rollup/rollup-linux-s390x-gnu@4.45.0':
+    resolution: {integrity: sha512-2l9sA7d7QdikL0xQwNMO3xURBUNEWyHVHfAsHsUdq+E/pgLTUcCE+gih5PCdmyHmfTDeXUWVhqL0WZzg0nua3g==}
     cpu: [s390x]
     os: [linux]
     libc: [glibc]
 
-  '@rollup/rollup-linux-x64-gnu@4.44.2':
-    resolution: {integrity: sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==}
+  '@rollup/rollup-linux-x64-gnu@4.45.0':
+    resolution: {integrity: sha512-XZdD3fEEQcwG2KrJDdEQu7NrHonPxxaV0/w2HpvINBdcqebz1aL+0vM2WFJq4DeiAVT6F5SUQas65HY5JDqoPw==}
     cpu: [x64]
     os: [linux]
     libc: [glibc]
 
-  '@rollup/rollup-linux-x64-musl@4.44.2':
-    resolution: {integrity: sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==}
+  '@rollup/rollup-linux-x64-musl@4.45.0':
+    resolution: {integrity: sha512-7ayfgvtmmWgKWBkCGg5+xTQ0r5V1owVm67zTrsEY1008L5ro7mCyGYORomARt/OquB9KY7LpxVBZes+oSniAAQ==}
     cpu: [x64]
     os: [linux]
     libc: [musl]
 
-  '@rollup/rollup-win32-arm64-msvc@4.44.2':
-    resolution: {integrity: sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==}
+  '@rollup/rollup-win32-arm64-msvc@4.45.0':
+    resolution: {integrity: sha512-B+IJgcBnE2bm93jEW5kHisqvPITs4ddLOROAcOc/diBgrEiQJJ6Qcjby75rFSmH5eMGrqJryUgJDhrfj942apQ==}
     cpu: [arm64]
     os: [win32]
 
-  '@rollup/rollup-win32-ia32-msvc@4.44.2':
-    resolution: {integrity: sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==}
+  '@rollup/rollup-win32-ia32-msvc@4.45.0':
+    resolution: {integrity: sha512-+CXwwG66g0/FpWOnP/v1HnrGVSOygK/osUbu3wPRy8ECXjoYKjRAyfxYpDQOfghC5qPJYLPH0oN4MCOjwgdMug==}
     cpu: [ia32]
     os: [win32]
 
-  '@rollup/rollup-win32-x64-msvc@4.44.2':
-    resolution: {integrity: sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==}
+  '@rollup/rollup-win32-x64-msvc@4.45.0':
+    resolution: {integrity: sha512-SRf1cytG7wqcHVLrBc9VtPK4pU5wxiB/lNIkNmW2ApKXIg+RpqwHfsaEK+e7eH4A1BpI6BX/aBWXxZCIrJg3uA==}
     cpu: [x64]
     os: [win32]
 
@@ -882,8 +878,8 @@ packages:
   '@types/lodash@4.17.20':
     resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
 
-  '@types/node-forge@1.3.12':
-    resolution: {integrity: sha512-a0ToKlRVnUw3aXKQq2F+krxZKq7B8LEQijzPn5RdFAMatARD2JX9o8FBpMXOOrjob0uc13aN+V/AXniOXW4d9A==}
+  '@types/node-forge@1.3.13':
+    resolution: {integrity: sha512-zePQJSW5QkwSHKRApqWCVKeKoSOt4xvEnLENZPjyvm9Ezdf/EyDeJM7jqLzOwjVICQQzvLZ63T55MKdJB5H6ww==}
 
   '@types/node@24.0.13':
     resolution: {integrity: sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==}
@@ -1683,8 +1679,8 @@ packages:
   electron-to-chromium@1.5.182:
     resolution: {integrity: sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==}
 
-  element-plus@2.10.3:
-    resolution: {integrity: sha512-OLpf0iekuvWJrz1+H9ybvem6TYTKSNk6L1QDA3tYq2YWbogKXJnWpHG1UAGKR1B7gx+vUH7M15VIH3EijE9Kgw==}
+  element-plus@2.10.4:
+    resolution: {integrity: sha512-UD4elWHrCnp1xlPhbXmVcaKFLCRaRAY6WWRwemGfGW3ceIjXm9fSYc9RNH3AiOEA6Ds1p9ZvhCs76CR9J8Vd+A==}
     peerDependencies:
       vue: ^3.2.0
 
@@ -1810,8 +1806,8 @@ packages:
     resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  eslint@9.30.1:
-    resolution: {integrity: sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==}
+  eslint@9.31.0:
+    resolution: {integrity: sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     hasBin: true
     peerDependencies:
@@ -3140,8 +3136,8 @@ packages:
   rfdc@1.4.1:
     resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
 
-  rollup@4.44.2:
-    resolution: {integrity: sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==}
+  rollup@4.45.0:
+    resolution: {integrity: sha512-WLjEcJRIo7i3WDDgOIJqVI2d+lAC3EwvOGy+Xfq6hs+GQuAA4Di/H72xmXkOhrIWFg2PFYSKZYfH0f4vfKXN4A==}
     engines: {node: '>=18.0.0', npm: '>=8.0.0'}
     hasBin: true
 
@@ -3876,7 +3872,7 @@ snapshots:
       '@babel/parser': 7.28.0
       '@babel/template': 7.27.2
       '@babel/traverse': 7.28.0
-      '@babel/types': 7.28.0
+      '@babel/types': 7.28.1
       convert-source-map: 2.0.0
       debug: 4.4.1
       gensync: 1.0.0-beta.2
@@ -3888,14 +3884,14 @@ snapshots:
   '@babel/generator@7.28.0':
     dependencies:
       '@babel/parser': 7.28.0
-      '@babel/types': 7.28.0
+      '@babel/types': 7.28.1
       '@jridgewell/gen-mapping': 0.3.12
       '@jridgewell/trace-mapping': 0.3.29
       jsesc: 3.1.0
 
   '@babel/helper-annotate-as-pure@7.27.3':
     dependencies:
-      '@babel/types': 7.28.0
+      '@babel/types': 7.28.1
 
   '@babel/helper-compilation-targets@7.27.2':
     dependencies:
@@ -3923,14 +3919,14 @@ snapshots:
   '@babel/helper-member-expression-to-functions@7.27.1':
     dependencies:
       '@babel/traverse': 7.28.0
-      '@babel/types': 7.28.0
+      '@babel/types': 7.28.1
     transitivePeerDependencies:
       - supports-color
 
   '@babel/helper-module-imports@7.27.1':
     dependencies:
       '@babel/traverse': 7.28.0
-      '@babel/types': 7.28.0
+      '@babel/types': 7.28.1
     transitivePeerDependencies:
       - supports-color
 
@@ -3945,7 +3941,7 @@ snapshots:
 
   '@babel/helper-optimise-call-expression@7.27.1':
     dependencies:
-      '@babel/types': 7.28.0
+      '@babel/types': 7.28.1
 
   '@babel/helper-plugin-utils@7.27.1': {}
 
@@ -3961,7 +3957,7 @@ snapshots:
   '@babel/helper-skip-transparent-expression-wrappers@7.27.1':
     dependencies:
       '@babel/traverse': 7.28.0
-      '@babel/types': 7.28.0
+      '@babel/types': 7.28.1
     transitivePeerDependencies:
       - supports-color
 
@@ -3974,11 +3970,11 @@ snapshots:
   '@babel/helpers@7.27.6':
     dependencies:
       '@babel/template': 7.27.2
-      '@babel/types': 7.28.0
+      '@babel/types': 7.28.1
 
   '@babel/parser@7.28.0':
     dependencies:
-      '@babel/types': 7.28.0
+      '@babel/types': 7.28.1
 
   '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.0)':
     dependencies:
@@ -4005,7 +4001,7 @@ snapshots:
     dependencies:
       '@babel/code-frame': 7.27.1
       '@babel/parser': 7.28.0
-      '@babel/types': 7.28.0
+      '@babel/types': 7.28.1
 
   '@babel/traverse@7.28.0':
     dependencies:
@@ -4014,12 +4010,12 @@ snapshots:
       '@babel/helper-globals': 7.28.0
       '@babel/parser': 7.28.0
       '@babel/template': 7.27.2
-      '@babel/types': 7.28.0
+      '@babel/types': 7.28.1
       debug: 4.4.1
     transitivePeerDependencies:
       - supports-color
 
-  '@babel/types@7.28.0':
+  '@babel/types@7.28.1':
     dependencies:
       '@babel/helper-string-parser': 7.27.1
       '@babel/helper-validator-identifier': 7.27.1
@@ -4235,9 +4231,9 @@ snapshots:
   '@esbuild/win32-x64@0.25.6':
     optional: true
 
-  '@eslint-community/eslint-utils@4.7.0(eslint@9.30.1(jiti@2.4.2))':
+  '@eslint-community/eslint-utils@4.7.0(eslint@9.31.0(jiti@2.4.2))':
     dependencies:
-      eslint: 9.30.1(jiti@2.4.2)
+      eslint: 9.31.0(jiti@2.4.2)
       eslint-visitor-keys: 3.4.3
 
   '@eslint-community/regexpp@4.12.1': {}
@@ -4252,10 +4248,6 @@ snapshots:
 
   '@eslint/config-helpers@0.3.0': {}
 
-  '@eslint/core@0.14.0':
-    dependencies:
-      '@types/json-schema': 7.0.15
-
   '@eslint/core@0.15.1':
     dependencies:
       '@types/json-schema': 7.0.15
@@ -4274,7 +4266,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@eslint/js@9.30.1': {}
+  '@eslint/js@9.31.0': {}
 
   '@eslint/object-schema@2.1.6': {}
 
@@ -4417,66 +4409,66 @@ snapshots:
 
   '@rolldown/pluginutils@1.0.0-beta.19': {}
 
-  '@rolldown/pluginutils@1.0.0-beta.26': {}
+  '@rolldown/pluginutils@1.0.0-beta.27': {}
 
-  '@rollup/rollup-android-arm-eabi@4.44.2':
+  '@rollup/rollup-android-arm-eabi@4.45.0':
     optional: true
 
-  '@rollup/rollup-android-arm64@4.44.2':
+  '@rollup/rollup-android-arm64@4.45.0':
     optional: true
 
-  '@rollup/rollup-darwin-arm64@4.44.2':
+  '@rollup/rollup-darwin-arm64@4.45.0':
     optional: true
 
-  '@rollup/rollup-darwin-x64@4.44.2':
+  '@rollup/rollup-darwin-x64@4.45.0':
     optional: true
 
-  '@rollup/rollup-freebsd-arm64@4.44.2':
+  '@rollup/rollup-freebsd-arm64@4.45.0':
     optional: true
 
-  '@rollup/rollup-freebsd-x64@4.44.2':
+  '@rollup/rollup-freebsd-x64@4.45.0':
     optional: true
 
-  '@rollup/rollup-linux-arm-gnueabihf@4.44.2':
+  '@rollup/rollup-linux-arm-gnueabihf@4.45.0':
     optional: true
 
-  '@rollup/rollup-linux-arm-musleabihf@4.44.2':
+  '@rollup/rollup-linux-arm-musleabihf@4.45.0':
     optional: true
 
-  '@rollup/rollup-linux-arm64-gnu@4.44.2':
+  '@rollup/rollup-linux-arm64-gnu@4.45.0':
     optional: true
 
-  '@rollup/rollup-linux-arm64-musl@4.44.2':
+  '@rollup/rollup-linux-arm64-musl@4.45.0':
     optional: true
 
-  '@rollup/rollup-linux-loongarch64-gnu@4.44.2':
+  '@rollup/rollup-linux-loongarch64-gnu@4.45.0':
     optional: true
 
-  '@rollup/rollup-linux-powerpc64le-gnu@4.44.2':
+  '@rollup/rollup-linux-powerpc64le-gnu@4.45.0':
     optional: true
 
-  '@rollup/rollup-linux-riscv64-gnu@4.44.2':
+  '@rollup/rollup-linux-riscv64-gnu@4.45.0':
     optional: true
 
-  '@rollup/rollup-linux-riscv64-musl@4.44.2':
+  '@rollup/rollup-linux-riscv64-musl@4.45.0':
     optional: true
 
-  '@rollup/rollup-linux-s390x-gnu@4.44.2':
+  '@rollup/rollup-linux-s390x-gnu@4.45.0':
     optional: true
 
-  '@rollup/rollup-linux-x64-gnu@4.44.2':
+  '@rollup/rollup-linux-x64-gnu@4.45.0':
     optional: true
 
-  '@rollup/rollup-linux-x64-musl@4.44.2':
+  '@rollup/rollup-linux-x64-musl@4.45.0':
     optional: true
 
-  '@rollup/rollup-win32-arm64-msvc@4.44.2':
+  '@rollup/rollup-win32-arm64-msvc@4.45.0':
     optional: true
 
-  '@rollup/rollup-win32-ia32-msvc@4.44.2':
+  '@rollup/rollup-win32-ia32-msvc@4.45.0':
     optional: true
 
-  '@rollup/rollup-win32-x64-msvc@4.44.2':
+  '@rollup/rollup-win32-x64-msvc@4.45.0':
     optional: true
 
   '@sxzz/popperjs-es@2.11.7': {}
@@ -4497,7 +4489,7 @@ snapshots:
 
   '@types/lodash@4.17.20': {}
 
-  '@types/node-forge@1.3.12':
+  '@types/node-forge@1.3.13':
     dependencies:
       '@types/node': 24.0.13
 
@@ -4515,15 +4507,15 @@ snapshots:
 
   '@types/web-bluetooth@0.0.21': {}
 
-  '@typescript-eslint/eslint-plugin@8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)':
+  '@typescript-eslint/eslint-plugin@8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)':
     dependencies:
       '@eslint-community/regexpp': 4.12.1
-      '@typescript-eslint/parser': 8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
+      '@typescript-eslint/parser': 8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
       '@typescript-eslint/scope-manager': 8.36.0
-      '@typescript-eslint/type-utils': 8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
-      '@typescript-eslint/utils': 8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
+      '@typescript-eslint/type-utils': 8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
+      '@typescript-eslint/utils': 8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
       '@typescript-eslint/visitor-keys': 8.36.0
-      eslint: 9.30.1(jiti@2.4.2)
+      eslint: 9.31.0(jiti@2.4.2)
       graphemer: 1.4.0
       ignore: 7.0.5
       natural-compare: 1.4.0
@@ -4532,14 +4524,14 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)':
+  '@typescript-eslint/parser@8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)':
     dependencies:
       '@typescript-eslint/scope-manager': 8.36.0
       '@typescript-eslint/types': 8.36.0
       '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.8.3)
       '@typescript-eslint/visitor-keys': 8.36.0
       debug: 4.4.1
-      eslint: 9.30.1(jiti@2.4.2)
+      eslint: 9.31.0(jiti@2.4.2)
       typescript: 5.8.3
     transitivePeerDependencies:
       - supports-color
@@ -4562,12 +4554,12 @@ snapshots:
     dependencies:
       typescript: 5.8.3
 
-  '@typescript-eslint/type-utils@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)':
+  '@typescript-eslint/type-utils@8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)':
     dependencies:
       '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.8.3)
-      '@typescript-eslint/utils': 8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
+      '@typescript-eslint/utils': 8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
       debug: 4.4.1
-      eslint: 9.30.1(jiti@2.4.2)
+      eslint: 9.31.0(jiti@2.4.2)
       ts-api-utils: 2.1.0(typescript@5.8.3)
       typescript: 5.8.3
     transitivePeerDependencies:
@@ -4591,13 +4583,13 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/utils@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)':
+  '@typescript-eslint/utils@8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)':
     dependencies:
-      '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2))
+      '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.4.2))
       '@typescript-eslint/scope-manager': 8.36.0
       '@typescript-eslint/types': 8.36.0
       '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.8.3)
-      eslint: 9.30.1(jiti@2.4.2)
+      eslint: 9.31.0(jiti@2.4.2)
       typescript: 5.8.3
     transitivePeerDependencies:
       - supports-color
@@ -4611,7 +4603,7 @@ snapshots:
     dependencies:
       '@babel/core': 7.28.0
       '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.0)
-      '@rolldown/pluginutils': 1.0.0-beta.26
+      '@rolldown/pluginutils': 1.0.0-beta.27
       '@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.28.0)
       vite: 7.0.4(@types/node@24.0.13)(jiti@2.4.2)(sass@1.89.2)(yaml@2.8.0)
       vue: 3.5.17(typescript@5.8.3)
@@ -4645,7 +4637,7 @@ snapshots:
       '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.0)
       '@babel/template': 7.27.2
       '@babel/traverse': 7.28.0
-      '@babel/types': 7.28.0
+      '@babel/types': 7.28.1
       '@vue/babel-helper-vue-transform-on': 1.4.0
       '@vue/babel-plugin-resolve-type': 1.4.0(@babel/core@7.28.0)
       '@vue/shared': 3.5.17
@@ -4720,23 +4712,23 @@ snapshots:
     dependencies:
       rfdc: 1.4.1
 
-  '@vue/eslint-config-prettier@10.2.0(eslint@9.30.1(jiti@2.4.2))(prettier@3.6.2)':
+  '@vue/eslint-config-prettier@10.2.0(eslint@9.31.0(jiti@2.4.2))(prettier@3.6.2)':
     dependencies:
-      eslint: 9.30.1(jiti@2.4.2)
-      eslint-config-prettier: 10.1.5(eslint@9.30.1(jiti@2.4.2))
-      eslint-plugin-prettier: 5.5.1(eslint-config-prettier@10.1.5(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2))(prettier@3.6.2)
+      eslint: 9.31.0(jiti@2.4.2)
+      eslint-config-prettier: 10.1.5(eslint@9.31.0(jiti@2.4.2))
+      eslint-plugin-prettier: 5.5.1(eslint-config-prettier@10.1.5(eslint@9.31.0(jiti@2.4.2)))(eslint@9.31.0(jiti@2.4.2))(prettier@3.6.2)
       prettier: 3.6.2
     transitivePeerDependencies:
       - '@types/eslint'
 
-  '@vue/eslint-config-typescript@14.6.0(eslint-plugin-vue@10.3.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.30.1(jiti@2.4.2))))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)':
+  '@vue/eslint-config-typescript@14.6.0(eslint-plugin-vue@10.3.0(@typescript-eslint/parser@8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.31.0(jiti@2.4.2))))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)':
     dependencies:
-      '@typescript-eslint/utils': 8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
-      eslint: 9.30.1(jiti@2.4.2)
-      eslint-plugin-vue: 10.3.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.30.1(jiti@2.4.2)))
+      '@typescript-eslint/utils': 8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
+      eslint: 9.31.0(jiti@2.4.2)
+      eslint-plugin-vue: 10.3.0(@typescript-eslint/parser@8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.31.0(jiti@2.4.2)))
       fast-glob: 3.3.3
-      typescript-eslint: 8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
-      vue-eslint-parser: 10.2.0(eslint@9.30.1(jiti@2.4.2))
+      typescript-eslint: 8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
+      vue-eslint-parser: 10.2.0(eslint@9.31.0(jiti@2.4.2))
     optionalDependencies:
       typescript: 5.8.3
     transitivePeerDependencies:
@@ -5433,7 +5425,7 @@ snapshots:
 
   electron-to-chromium@1.5.182: {}
 
-  element-plus@2.10.3(vue@3.5.17(typescript@5.8.3)):
+  element-plus@2.10.4(vue@3.5.17(typescript@5.8.3)):
     dependencies:
       '@ctrl/tinycolor': 3.6.1
       '@element-plus/icons-vue': 2.3.1(vue@3.5.17(typescript@5.8.3))
@@ -5591,31 +5583,31 @@ snapshots:
 
   escape-string-regexp@5.0.0: {}
 
-  eslint-config-prettier@10.1.5(eslint@9.30.1(jiti@2.4.2)):
+  eslint-config-prettier@10.1.5(eslint@9.31.0(jiti@2.4.2)):
     dependencies:
-      eslint: 9.30.1(jiti@2.4.2)
+      eslint: 9.31.0(jiti@2.4.2)
 
-  eslint-plugin-prettier@5.5.1(eslint-config-prettier@10.1.5(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2))(prettier@3.6.2):
+  eslint-plugin-prettier@5.5.1(eslint-config-prettier@10.1.5(eslint@9.31.0(jiti@2.4.2)))(eslint@9.31.0(jiti@2.4.2))(prettier@3.6.2):
     dependencies:
-      eslint: 9.30.1(jiti@2.4.2)
+      eslint: 9.31.0(jiti@2.4.2)
       prettier: 3.6.2
       prettier-linter-helpers: 1.0.0
       synckit: 0.11.8
     optionalDependencies:
-      eslint-config-prettier: 10.1.5(eslint@9.30.1(jiti@2.4.2))
+      eslint-config-prettier: 10.1.5(eslint@9.31.0(jiti@2.4.2))
 
-  eslint-plugin-vue@10.3.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.30.1(jiti@2.4.2))):
+  eslint-plugin-vue@10.3.0(@typescript-eslint/parser@8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(vue-eslint-parser@10.2.0(eslint@9.31.0(jiti@2.4.2))):
     dependencies:
-      '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2))
-      eslint: 9.30.1(jiti@2.4.2)
+      '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.4.2))
+      eslint: 9.31.0(jiti@2.4.2)
       natural-compare: 1.4.0
       nth-check: 2.1.1
       postcss-selector-parser: 6.1.2
       semver: 7.7.2
-      vue-eslint-parser: 10.2.0(eslint@9.30.1(jiti@2.4.2))
+      vue-eslint-parser: 10.2.0(eslint@9.31.0(jiti@2.4.2))
       xml-name-validator: 4.0.0
     optionalDependencies:
-      '@typescript-eslint/parser': 8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
+      '@typescript-eslint/parser': 8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
 
   eslint-scope@8.4.0:
     dependencies:
@@ -5626,15 +5618,15 @@ snapshots:
 
   eslint-visitor-keys@4.2.1: {}
 
-  eslint@9.30.1(jiti@2.4.2):
+  eslint@9.31.0(jiti@2.4.2):
     dependencies:
-      '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2))
+      '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.4.2))
       '@eslint-community/regexpp': 4.12.1
       '@eslint/config-array': 0.21.0
       '@eslint/config-helpers': 0.3.0
-      '@eslint/core': 0.14.0
+      '@eslint/core': 0.15.1
       '@eslint/eslintrc': 3.3.1
-      '@eslint/js': 9.30.1
+      '@eslint/js': 9.31.0
       '@eslint/plugin-kit': 0.3.3
       '@humanfs/node': 0.16.6
       '@humanwhocodes/module-importer': 1.0.1
@@ -6966,30 +6958,30 @@ snapshots:
 
   rfdc@1.4.1: {}
 
-  rollup@4.44.2:
+  rollup@4.45.0:
     dependencies:
       '@types/estree': 1.0.8
     optionalDependencies:
-      '@rollup/rollup-android-arm-eabi': 4.44.2
-      '@rollup/rollup-android-arm64': 4.44.2
-      '@rollup/rollup-darwin-arm64': 4.44.2
-      '@rollup/rollup-darwin-x64': 4.44.2
-      '@rollup/rollup-freebsd-arm64': 4.44.2
-      '@rollup/rollup-freebsd-x64': 4.44.2
-      '@rollup/rollup-linux-arm-gnueabihf': 4.44.2
-      '@rollup/rollup-linux-arm-musleabihf': 4.44.2
-      '@rollup/rollup-linux-arm64-gnu': 4.44.2
-      '@rollup/rollup-linux-arm64-musl': 4.44.2
-      '@rollup/rollup-linux-loongarch64-gnu': 4.44.2
-      '@rollup/rollup-linux-powerpc64le-gnu': 4.44.2
-      '@rollup/rollup-linux-riscv64-gnu': 4.44.2
-      '@rollup/rollup-linux-riscv64-musl': 4.44.2
-      '@rollup/rollup-linux-s390x-gnu': 4.44.2
-      '@rollup/rollup-linux-x64-gnu': 4.44.2
-      '@rollup/rollup-linux-x64-musl': 4.44.2
-      '@rollup/rollup-win32-arm64-msvc': 4.44.2
-      '@rollup/rollup-win32-ia32-msvc': 4.44.2
-      '@rollup/rollup-win32-x64-msvc': 4.44.2
+      '@rollup/rollup-android-arm-eabi': 4.45.0
+      '@rollup/rollup-android-arm64': 4.45.0
+      '@rollup/rollup-darwin-arm64': 4.45.0
+      '@rollup/rollup-darwin-x64': 4.45.0
+      '@rollup/rollup-freebsd-arm64': 4.45.0
+      '@rollup/rollup-freebsd-x64': 4.45.0
+      '@rollup/rollup-linux-arm-gnueabihf': 4.45.0
+      '@rollup/rollup-linux-arm-musleabihf': 4.45.0
+      '@rollup/rollup-linux-arm64-gnu': 4.45.0
+      '@rollup/rollup-linux-arm64-musl': 4.45.0
+      '@rollup/rollup-linux-loongarch64-gnu': 4.45.0
+      '@rollup/rollup-linux-powerpc64le-gnu': 4.45.0
+      '@rollup/rollup-linux-riscv64-gnu': 4.45.0
+      '@rollup/rollup-linux-riscv64-musl': 4.45.0
+      '@rollup/rollup-linux-s390x-gnu': 4.45.0
+      '@rollup/rollup-linux-x64-gnu': 4.45.0
+      '@rollup/rollup-linux-x64-musl': 4.45.0
+      '@rollup/rollup-win32-arm64-msvc': 4.45.0
+      '@rollup/rollup-win32-ia32-msvc': 4.45.0
+      '@rollup/rollup-win32-x64-msvc': 4.45.0
       fsevents: 2.3.3
 
   run-async@2.4.1: {}
@@ -7509,12 +7501,12 @@ snapshots:
       typed-array-buffer: 1.0.3
       typed-array-byte-offset: 1.0.4
 
-  typescript-eslint@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3):
+  typescript-eslint@8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3):
     dependencies:
-      '@typescript-eslint/eslint-plugin': 8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
-      '@typescript-eslint/parser': 8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
-      '@typescript-eslint/utils': 8.36.0(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)
-      eslint: 9.30.1(jiti@2.4.2)
+      '@typescript-eslint/eslint-plugin': 8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
+      '@typescript-eslint/parser': 8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
+      '@typescript-eslint/utils': 8.36.0(eslint@9.31.0(jiti@2.4.2))(typescript@5.8.3)
+      eslint: 9.31.0(jiti@2.4.2)
       typescript: 5.8.3
     transitivePeerDependencies:
       - supports-color
@@ -7664,7 +7656,7 @@ snapshots:
       fdir: 6.4.6(picomatch@4.0.2)
       picomatch: 4.0.2
       postcss: 8.5.6
-      rollup: 4.44.2
+      rollup: 4.45.0
       tinyglobby: 0.2.14
     optionalDependencies:
       '@types/node': 24.0.13
@@ -7679,10 +7671,10 @@ snapshots:
     dependencies:
       vue: 3.5.17(typescript@5.8.3)
 
-  vue-eslint-parser@10.2.0(eslint@9.30.1(jiti@2.4.2)):
+  vue-eslint-parser@10.2.0(eslint@9.31.0(jiti@2.4.2)):
     dependencies:
       debug: 4.4.1
-      eslint: 9.30.1(jiti@2.4.2)
+      eslint: 9.31.0(jiti@2.4.2)
       eslint-scope: 8.4.0
       eslint-visitor-keys: 4.2.1
       espree: 10.4.0

+ 9 - 2
src/App.vue

@@ -1,7 +1,14 @@
 <template>
-  <el-config-provider>
+  <el-config-provider :locale="locale">
     <router-view />
   </el-config-provider>
 </template>
-<script setup lang="ts"></script>
+<script setup lang="ts">
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
+
+// element 国际化-默认中文
+const locale = computed(() => {
+  return zhCn
+})
+</script>
 <style scoped></style>

+ 37 - 0
src/api/interface/system/role.ts

@@ -0,0 +1,37 @@
+export type RoleQuery = PageQuery & {
+  roleName?: string
+}
+
+export type RoleForm = {
+  id?: number
+  roleName: string
+  remark: string
+}
+
+export type RoleInfo = {
+  id: number
+  roleName: string
+  remark: string
+  delFlag: string
+  createTime: string
+  updateTime: string
+  isLock?: string
+  permissions?: string
+}
+
+export type RoleMenu = {
+  menuLists: RoleMenuTree[]
+  selectIds: string[]
+}
+
+export type RoleMenuTree = {
+  id: string
+  pid: string
+  title: string
+  children: RoleMenuTree[]
+}
+
+export type RoleMenuForm = {
+  menuIds: string[]
+  roleId: number
+}

+ 14 - 0
src/api/module/system/role.ts

@@ -0,0 +1,14 @@
+import { RoleInfo, RoleQuery } from '@/api/interface/system/role'
+import http from '@/axios'
+
+class RoleApi {
+  /**
+   * 获取角色列表
+   * @param params
+   * @returns {*}
+   */
+  static pageRoleApi = (params: RoleQuery): Promise<ResultData<any>> => {
+    return http.get<ResPage<RoleInfo>>({ url: '/system/role/page', params })
+  }
+}
+export default RoleApi

+ 141 - 4
src/assets/styles/custom.scss

@@ -3,8 +3,8 @@
   box-sizing: border-box;
   padding: 12px;
   overflow-x: hidden;
-  background-color: var(--card-bg-color);
-  border: 1px solid var(--card-border-color);
+  background-color: var(--bg-color);
+  border: 1px solid var(--border-color);
   border-radius: 4px;
   box-shadow: 0 0 12px rgb(0 0 0 / 5%);
 }
@@ -23,8 +23,145 @@
     margin-bottom: 0 !important;
   }
 }
-
-/* content-box (常用内容盒子) */
 .content-box {
   height: 100%;
 }
+
+/* proTable */
+.table-box,
+.table-main {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  width: 100%;
+  height: 100%;
+
+  // table-search 表格搜索样式
+  .table-search {
+    padding: 18px 18px 0;
+    margin-bottom: 4px;
+    .el-form {
+      .el-form-item__content > * {
+        width: 100%;
+      }
+      .el-form-item {
+        margin-bottom: 12px;
+      }
+
+      // 去除时间选择器上下 padding
+      .el-range-editor.el-input__wrapper {
+        padding: 0 10px;
+      }
+    }
+    .operation {
+      display: flex;
+      align-items: center;
+      justify-content: flex-end;
+      margin-bottom: 18px;
+    }
+  }
+
+  // 表格 header 样式
+  .table-header {
+    .header-button-lf {
+      float: left;
+    }
+    .header-button-ri {
+      float: right;
+    }
+    .el-button {
+      margin-bottom: 15px;
+    }
+  }
+
+  // el-table 表格样式
+  .el-table {
+    flex: 1;
+
+    // 修复 safari 浏览器表格错位 https://github.com/HalseySpicy/Geeker-Admin/issues/83
+    table {
+      width: 100%;
+    }
+    .el-table__header th {
+      height: 45px;
+      font-size: 15px;
+      font-weight: bold;
+      color: var(--el-text-color-primary);
+      background: var(--el-fill-color-light);
+    }
+    .el-table__row {
+      height: 45px;
+      font-size: 14px;
+      .move {
+        cursor: move;
+        .el-icon {
+          cursor: move;
+        }
+      }
+    }
+
+    // 设置 el-table 中 header 文字不换行,并省略
+    .el-table__header .el-table__cell > .cell {
+      white-space: nowrap;
+    }
+
+    // 解决表格数据为空时样式不居中问题(仅在element-plus中)
+    .el-table__empty-block {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      .table-empty {
+        line-height: 30px;
+      }
+    }
+    &.is-scrolling-none {
+      th.el-table-fixed-column--left,
+      th.el-table-fixed-column--right {
+        background-color: var(--el-fill-color-light);
+      }
+    }
+
+    // table 中 image 图片样式
+    .table-image {
+      width: 50px;
+      height: 50px;
+      border-radius: 50%;
+    }
+  }
+
+  // 表格 pagination 样式
+  .el-pagination {
+    display: flex;
+    justify-content: flex-end;
+    margin-top: 20px;
+  }
+}
+
+/* main-box (树形表格 DeptTree 页面会使用,左右布局 flex) */
+.main-box {
+  display: flex;
+  width: 100%;
+  height: 100%;
+  .table-box {
+    // 这里减去的是 DeptTree 组件宽度
+    width: calc(100% - 230px);
+  }
+}
+
+/* mask image */
+.mask-image {
+  padding-right: 50px;
+  mask-image: linear-gradient(90deg, #000000 0%, #000000 calc(100% - 50px), transparent);
+}
+
+/* 当前页面最大化 css */
+.main-maximize {
+  .aside-split,
+  .el-aside,
+  .el-header,
+  .el-footer,
+  .tabs-box {
+    display: none !important;
+  }
+}

+ 3 - 164
src/assets/styles/element.scss

@@ -9,170 +9,12 @@
   border: 1px solid;
 }
 
-/* 当前页面最大化 css */
-.main-maximize {
-  .aside-split,
-  .el-aside,
-  .el-header,
-  .el-footer,
-  .tabs-box {
-    display: none !important;
-  }
-}
-
-/* mask image */
-.mask-image {
-  padding-right: 50px;
-  mask-image: linear-gradient(90deg, #000000 0%, #000000 calc(100% - 50px), transparent);
-}
-
-/* main-box (树形表格 DeptTree 页面会使用,左右布局 flex) */
-.main-box {
-  display: flex;
-  width: 100%;
-  height: 100%;
-  .table-box {
-    // 这里减去的是 DeptTree 组件宽度
-    width: calc(100% - 230px);
-  }
-}
-
-/* proTable */
-.table-box,
-.table-main {
-  display: flex;
-  flex: 1;
-  flex-direction: column;
-  width: 100%;
-  height: 100%;
-
-  // table-search 表格搜索样式
-  .table-search {
-    padding: 18px 18px 0;
-    margin-bottom: 10px;
-    .el-form {
-      .el-form-item__content > * {
-        width: 100%;
-      }
-
-      // 去除时间选择器上下 padding
-      .el-range-editor.el-input__wrapper {
-        padding: 0 10px;
-      }
-    }
-    .operation {
-      display: flex;
-      align-items: center;
-      justify-content: flex-end;
-      margin-bottom: 18px;
-    }
-  }
-
-  // 表格 header 样式
-  .table-header {
-    .header-button-lf {
-      float: left;
-    }
-    .header-button-ri {
-      float: right;
-    }
-    .el-button {
-      margin-bottom: 15px;
-    }
-  }
-
-  // el-table 表格样式
-  .el-table {
-    flex: 1;
-
-    // 修复 safari 浏览器表格错位 https://github.com/HalseySpicy/Geeker-Admin/issues/83
-    table {
-      width: 100%;
-    }
-    .el-table__header th {
-      height: 45px;
-      font-size: 15px;
-      font-weight: bold;
-      color: var(--el-text-color-primary);
-      background: var(--el-fill-color-light);
-    }
-    .el-table__row {
-      height: 45px;
-      font-size: 14px;
-      .move {
-        cursor: move;
-        .el-icon {
-          cursor: move;
-        }
-      }
-    }
-
-    // 设置 el-table 中 header 文字不换行,并省略
-    .el-table__header .el-table__cell > .cell {
-      white-space: nowrap;
-    }
-
-    // 解决表格数据为空时样式不居中问题(仅在element-plus中)
-    .el-table__empty-block {
-      position: absolute;
-      top: 50%;
-      left: 50%;
-      transform: translate(-50%, -50%);
-      .table-empty {
-        line-height: 30px;
-      }
-    }
-    &.is-scrolling-none {
-      th.el-table-fixed-column--left,
-      th.el-table-fixed-column--right {
-        background-color: var(--el-fill-color-light);
-      }
-    }
-
-    // table 中 image 图片样式
-    .table-image {
-      width: 50px;
-      height: 50px;
-      border-radius: 50%;
-    }
-  }
-
-  // 表格 pagination 样式
-  .el-pagination {
-    display: flex;
-    justify-content: flex-end;
-    margin-top: 20px;
-  }
-}
-
-/* el-table 组件大小 */
-.el-table--small {
-  .el-table__header th {
-    height: 40px !important;
-    font-size: 14px !important;
-  }
-  .el-table__row {
-    height: 40px !important;
-    font-size: 13px !important;
-  }
-}
-.el-table--large {
-  .el-table__header th {
-    height: 50px !important;
-    font-size: 16px !important;
-  }
-  .el-table__row {
-    height: 50px !important;
-    font-size: 15px !important;
-  }
-}
-
 /* el-drawer */
 .el-drawer {
   .el-drawer__header {
     padding: 16px 20px;
     margin-bottom: 0;
-    border-bottom: 1px solid var(--el-border-color-lighter);
+    border-bottom: 1px solid var(--border-color);
     span {
       font-size: 17px;
       line-height: 17px;
@@ -180,7 +22,7 @@
     }
   }
   .el-drawer__footer {
-    border-top: 1px solid var(--el-border-color-lighter);
+    border-top: 1px solid var(--border-color);
   }
 
   // select 样式
@@ -206,7 +48,7 @@
   .el-dialog__header {
     padding: 15px 20px;
     margin-bottom: 20px;
-    border-bottom: 1px solid var(--el-border-color-lighter);
+    border-bottom: 1px solid var(--border-color);
     .el-dialog__title {
       font-size: 17px;
     }
@@ -217,6 +59,3 @@
     display: none;
   }
 }
-.form__label {
-  font-weight: 700;
-}

+ 63 - 0
src/components/Grid/GridItem.vue

@@ -0,0 +1,63 @@
+<template>
+  <div v-show="isShow" :style="style">
+    <slot />
+  </div>
+</template>
+<script setup lang="ts" name="GridItem">
+import type { BreakPoint, Responsive } from '@/components/Grid/interface'
+type Props = {
+  offset?: number
+  span?: number
+  suffix?: boolean
+  xs?: Responsive
+  sm?: Responsive
+  md?: Responsive
+  lg?: Responsive
+  xl?: Responsive
+}
+const props = withDefaults(defineProps<Props>(), {
+  offset: 0,
+  span: 1,
+  suffix: false,
+  xs: undefined,
+  sm: undefined,
+  md: undefined,
+  lg: undefined,
+  xl: undefined
+})
+
+const attrs = useAttrs() as { index: string }
+const isShow = ref(true)
+
+// 注入断点
+const breakPoint = inject<Ref<BreakPoint>>('breakPoint', ref('xl'))
+const shouldHiddenIndex = inject('shouldHiddenIndex', ref(-1))
+watch(
+  () => [shouldHiddenIndex.value, breakPoint.value],
+  n => {
+    if (attrs.index) {
+      isShow.value = !(n[0] !== -1 && parseInt(attrs.index) >= Number(n[0]))
+    }
+  },
+  { immediate: true }
+)
+
+const gap = inject('gap', 0)
+const cols = inject('cols', ref(4))
+const style = computed(() => {
+  const span = props[breakPoint.value]?.span ?? props.span
+  const offset = props[breakPoint.value]?.offset ?? props.offset
+  if (props.suffix) {
+    return {
+      gridColumnStart: cols.value - span - offset + 1,
+      gridColumnEnd: `span ${span + offset}`,
+      marginLeft: offset !== 0 ? `calc(((100% + ${gap}px) / ${span + offset}) * ${offset})` : 'unset'
+    }
+  } else {
+    return {
+      gridColumn: `span ${span + offset > cols.value ? cols.value : span + offset}/span ${span + offset > cols.value ? cols.value : span + offset}`,
+      marginLeft: offset !== 0 ? `calc(((100% + ${gap}px) / ${span + offset}) * ${offset})` : 'unset'
+    }
+  }
+})
+</script>

+ 156 - 0
src/components/Grid/index.vue

@@ -0,0 +1,156 @@
+<template>
+  <div :style="style">
+    <slot />
+  </div>
+</template>
+
+<script setup lang="ts" name="Grid">
+import type { BreakPoint } from '@/components/Grid/interface'
+import { VNodeArrayChildren } from 'vue'
+
+type Props = {
+  cols?: number | Record<BreakPoint, number>
+  collapsed?: boolean
+  collapsedRows?: number
+  gap?: [number, number] | number
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  cols: () => ({ xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }),
+  collapsed: false,
+  collapsedRows: 1,
+  gap: 0
+})
+
+onBeforeMount(() => props.collapsed && findIndex())
+onMounted(() => {
+  resize({ target: { innerWidth: window.innerWidth } } as unknown as Event)
+  window.addEventListener('resize', resize)
+})
+onActivated(() => {
+  resize({ target: { innerWidth: window.innerWidth } } as unknown as Event)
+  window.addEventListener('resize', resize)
+})
+onUnmounted(() => {
+  window.removeEventListener('resize', resize)
+})
+onDeactivated(() => {
+  window.removeEventListener('resize', resize)
+})
+
+// 监听屏幕变化
+const resize = (e: Event) => {
+  const width = (e.target as Window).innerWidth
+  switch (true) {
+    case width < 768:
+      breakPoint.value = 'xs'
+      break
+    case width >= 768 && width < 992:
+      breakPoint.value = 'sm'
+      break
+    case width >= 992 && width < 1200:
+      breakPoint.value = 'md'
+      break
+    case width >= 1200 && width < 1920:
+      breakPoint.value = 'lg'
+      break
+    case width >= 1920:
+      breakPoint.value = 'xl'
+      break
+  }
+}
+
+// 注入 gap 间距
+provide('gap', Array.isArray(props.gap) ? props.gap[0] : props.gap)
+
+// 注入响应式断点
+const breakPoint = ref<BreakPoint>('xl')
+provide('breakPoint', breakPoint)
+
+// 注入要开始折叠的 index
+const hiddenIndex = ref(-1)
+provide('shouldHiddenIndex', hiddenIndex)
+
+// 注入 cols
+const gridCols = computed(() => {
+  if (typeof props.cols === 'object') return props.cols[breakPoint.value] ?? props.cols
+  return props.cols
+})
+provide('cols', gridCols)
+
+// 寻找需要开始折叠的字段 index
+const slots = (() => {
+  const s = useSlots()
+  return typeof s.default === 'function' ? s.default() : []
+})()
+
+const findIndex = () => {
+  const fields: VNodeArrayChildren = []
+  let suffix: VNode | null = null
+  slots.forEach((slot: any) => {
+    // suffix
+    if (typeof slot.type === 'object' && slot.type.name === 'GridItem' && slot.props?.suffix !== undefined) suffix = slot
+    // slot children
+    if (typeof slot.type === 'symbol' && Array.isArray(slot.children)) fields.push(...slot.children)
+  })
+
+  // 计算 suffix 所占用的列
+  let suffixCols = 0
+  if (suffix) {
+    suffixCols =
+      ((suffix as VNode).props?.[breakPoint.value]?.span ?? (suffix as VNode).props?.span ?? 1) +
+      ((suffix as VNode).props?.[breakPoint.value]?.offset ?? (suffix as VNode).props?.offset ?? 0)
+  }
+
+  let total = 0
+  let found = false
+  const colsNum = Number(gridCols.value) || 1
+  for (let index = 0; index < fields.length; index++) {
+    const current = fields[index] as VNode
+    const span = current.props?.[breakPoint.value]?.span ?? current.props?.span ?? 1
+    const offset = current.props?.[breakPoint.value]?.offset ?? current.props?.offset ?? 0
+    total += Number(span) + Number(offset)
+    if (total > props.collapsedRows * colsNum - suffixCols) {
+      hiddenIndex.value = index
+      found = true
+      break
+    }
+  }
+  if (!found) hiddenIndex.value = -1
+}
+
+// 断点变化时 执行 findIndex
+watch(
+  () => breakPoint.value,
+  () => {
+    if (props.collapsed) findIndex()
+  }
+)
+
+// 监听 collapsed
+watch(
+  () => props.collapsed,
+  value => {
+    if (value) return findIndex()
+    hiddenIndex.value = -1
+  }
+)
+
+// 设置间距
+const gridGap = computed(() => {
+  if (typeof props.gap === 'number') return `${props.gap}px`
+  if (Array.isArray(props.gap)) return `${props.gap[1]}px ${props.gap[0]}px`
+  return 'unset'
+})
+
+// 设置 style
+const style = computed(() => {
+  return {
+    display: 'grid',
+    gridGap: gridGap.value,
+    gridTemplateColumns: `repeat(${gridCols.value}, minmax(0, 1fr))`
+  }
+})
+
+defineExpose({ breakPoint })
+</script>

+ 6 - 0
src/components/Grid/interface/index.ts

@@ -0,0 +1,6 @@
+export type BreakPoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl'
+
+export type Responsive = {
+  span?: number
+  offset?: number
+}

+ 0 - 41
src/components/HelloWorld.vue

@@ -1,41 +0,0 @@
-<script setup lang="ts">
-import { ref } from 'vue'
-
-defineProps<{ msg: string }>()
-
-const count = ref(0)
-</script>
-
-<template>
-  <h1>{{ msg }}</h1>
-
-  <div class="card">
-    <button type="button" @click="count++">count is {{ count }}</button>
-    <p>
-      Edit
-      <code>components/HelloWorld.vue</code> to test HMR
-    </p>
-  </div>
-
-  <p>
-    Check out
-    <a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
-      >create-vue</a
-    >, the official Vue + Vite starter
-  </p>
-  <p>
-    Learn more about IDE Support for Vue in the
-    <a
-      href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
-      target="_blank"
-      >Vue Docs Scaling up Guide</a
-    >.
-  </p>
-  <p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
-</template>
-
-<style scoped>
-.read-the-docs {
-  color: #888;
-}
-</style>

+ 48 - 0
src/components/ProTable/ColSetting.vue

@@ -0,0 +1,48 @@
+<template>
+  <!-- 列设置 -->
+  <el-drawer v-model="drawerVisible" title="列设置" size="450px">
+    <div class="table-main">
+      <el-table :data="colSetting" :border="true" row-key="prop" default-expand-all :tree-props="{ children: '_children' }">
+        <el-table-column prop="label" align="center" label="列名" />
+        <el-table-column v-slot="scope" prop="isShow" align="center" label="显示">
+          <el-switch v-model="scope.row.isShow" />
+        </el-table-column>
+        <el-table-column v-slot="scope" prop="sortable" align="center" label="排序">
+          <el-switch v-model="scope.row.sortable" />
+        </el-table-column>
+        <template #empty>
+          <div class="table-empty">
+            <img src="@/assets/images/notData.png" alt="notData" />
+            <div>暂无可配置列</div>
+          </div>
+        </template>
+      </el-table>
+    </div>
+  </el-drawer>
+</template>
+
+<script setup lang="ts" name="ColSetting">
+import type { ColumnProps } from '@/components/ProTable/interface'
+
+type Props = {
+  colSetting: ColumnProps[]
+}
+
+defineProps<Props>()
+
+const drawerVisible = ref(false)
+
+const openColSetting = () => {
+  drawerVisible.value = true
+}
+
+defineExpose({
+  openColSetting
+})
+</script>
+
+<style scoped lang="scss">
+.cursor-move {
+  cursor: move;
+}
+</style>

+ 28 - 0
src/components/ProTable/Pagination.vue

@@ -0,0 +1,28 @@
+<template>
+  <!-- 分页组件 -->
+  <el-pagination
+    :background="true"
+    :current-page="pageable.pageNum"
+    :page-size="pageable.pageSize"
+    :page-sizes="[10, 25, 50, 100]"
+    :total="pageable.total"
+    layout="total, sizes, prev, pager, next, jumper"
+    @size-change="handleSizeChange"
+    @current-change="handleCurrentChange" />
+</template>
+
+<script setup lang="ts" name="Pagination">
+interface Pageable {
+  pageNum: number
+  pageSize: number
+  total: number
+}
+
+interface PaginationProps {
+  pageable: Pageable
+  handleSizeChange: (size: number) => void
+  handleCurrentChange: (currentPage: number) => void
+}
+
+defineProps<PaginationProps>()
+</script>

+ 52 - 0
src/components/ProTable/TableColumn.vue

@@ -0,0 +1,52 @@
+<template>
+  <RenderTableColumn v-bind="column" />
+</template>
+
+<script setup lang="tsx" name="TableColumn">
+import { filterEnum, formatValue, handleProp, handleRowAccordingToProp } from '@/utils'
+import type { ColumnProps, RenderScope, HeaderRenderScope } from '@/components/ProTable/interface'
+import { Fragment } from 'vue'
+
+defineProps<{ column: ColumnProps }>()
+
+const slots = useSlots()
+
+const enumMap = inject('enumMap', ref(new Map()))
+
+// 渲染表格数据
+const renderCellData = (item: ColumnProps, scope: RenderScope<any>) => {
+  return enumMap.value.get(item.prop) && item.isFilterEnum
+    ? filterEnum(handleRowAccordingToProp(scope.row, item.prop!), enumMap.value.get(item.prop), item.fieldNames)
+    : formatValue(handleRowAccordingToProp(scope.row, item.prop!))
+}
+
+// 获取 tag 类型
+const getTagType = (item: ColumnProps, scope: RenderScope<any>) => {
+  return filterEnum(handleRowAccordingToProp(scope.row, item.prop!), enumMap.value.get(item.prop), item.fieldNames, 'tag') || 'primary'
+}
+
+const RenderTableColumn = (item: ColumnProps) => {
+  return (
+    <Fragment>
+      {item.isShow && (
+        <el-table-column {...item} align={item.align ?? 'center'} showOverflowTooltip={item.showOverflowTooltip ?? item.prop !== 'operation'}>
+          {{
+            default: (scope: RenderScope<any>) => {
+              if (item._children) return item._children.map(child => RenderTableColumn(child))
+              if (item.render) return item.render(scope)
+              if (slots[handleProp(item.prop!)]) return slots[handleProp(item.prop!)]!(scope)
+              if (item.tag) return <el-tag type={getTagType(item, scope)}>{renderCellData(item, scope)}</el-tag>
+              return renderCellData(item, scope)
+            },
+            header: (scope: HeaderRenderScope<any>) => {
+              if (item.headerRender) return item.headerRender(scope)
+              if (slots[`${handleProp(item.prop!)}Header`]) return slots[`${handleProp(item.prop!)}Header`]!(scope)
+              return item.label
+            }
+          }}
+        </el-table-column>
+      )}
+    </Fragment>
+  )
+}
+</script>

+ 264 - 0
src/components/ProTable/index.vue

@@ -0,0 +1,264 @@
+<template>
+  <!-- 查询表单 card -->
+  <SearchForm
+    v-show="isShowSearch"
+    :search="_search"
+    :reset="_reset"
+    :columns="searchColumnsData"
+    :search-param="searchParam"
+    :search-col="searchCol" />
+
+  <!-- 表格内容 card -->
+  <div class="card table-main">
+    <!-- 表格头部 操作按钮 -->
+    <div class="table-header">
+      <div class="header-button-lf">
+        <slot name="tableHeader" :selected-list-ids="selectedListIds" :selected-list="selectedList" :is-selected="isSelected" />
+      </div>
+      <div v-if="toolButton" class="header-button-ri">
+        <slot name="toolButton">
+          <el-button v-if="showToolButton('refresh')" icon="Refresh" circle @click="getTableList" />
+          <el-button v-if="showToolButton('setting') && columns.length" icon="Operation" circle @click="openColSetting" />
+          <el-button v-if="showToolButton('search') && searchColumnsData.length" icon="Search" circle @click="isShowSearch = !isShowSearch" />
+        </slot>
+      </div>
+    </div>
+    <!-- 表格主体 -->
+    <el-table
+      v-if="reload"
+      :class="randomTableClass"
+      ref="tableRef"
+      v-bind="$attrs"
+      :data="processTableData"
+      :border="border"
+      :row-key="rowKey"
+      @selection-change="selectionChange"
+      v-loading="loading">
+      <!-- 默认插槽 -->
+      <slot />
+      <template v-for="item in tableColumns" :key="item">
+        <!-- selection || radio || index || expand || sort -->
+        <el-table-column
+          v-if="item.type && columnTypes.includes(item.type)"
+          v-bind="item"
+          :align="item.align ?? 'center'"
+          :reserve-selection="item.type === 'selection'">
+          <template #default="scope">
+            <!-- expand -->
+            <template v-if="item.type == 'expand'">
+              <component :is="item.render" v-bind="scope" v-if="item.render" />
+              <slot v-else :name="item.type" v-bind="scope" />
+            </template>
+            <!-- radio -->
+            <el-radio v-if="item.type == 'radio'" v-model="radio" :value="scope.row[rowKey]">
+              <i />
+            </el-radio>
+            <!-- sort -->
+            <el-tag v-if="item.type == 'sort'" type="primary" class="move">
+              <el-icon>
+                <DCaret />
+              </el-icon>
+            </el-tag>
+          </template>
+        </el-table-column>
+        <!-- other -->
+        <TableColumn v-if="!item.type && item.prop && item.isShow" :column="item">
+          <template v-for="slot in Object.keys($slots)" #[slot]="scope">
+            <slot :name="slot" v-bind="scope" />
+          </template>
+        </TableColumn>
+      </template>
+      <!-- 插入表格最后一行之后的插槽 -->
+      <template #append>
+        <slot name="append" />
+      </template>
+      <!-- 无数据 -->
+      <template #empty>
+        <div class="table-empty">
+          <slot name="empty">
+            <img src="@/assets/images/notData.png" alt="notData" />
+            <div>暂无数据</div>
+          </slot>
+        </div>
+      </template>
+    </el-table>
+    <!-- 分页组件 -->
+    <slot name="pagination">
+      <Pagination v-if="pagination" :pageable="pageable" :handle-size-change="handleSizeChange" :handle-current-change="handleCurrentChange" />
+    </slot>
+  </div>
+  <!-- 列设置 -->
+  <ColSetting v-if="toolButton" ref="colRef" v-model:col-setting="colSetting" />
+</template>
+
+<script setup lang="ts" name="ProTable">
+import { useTable, useSelection } from '@/hooks'
+import { generateUUID, handleProp } from '@/utils'
+import type { ColumnProps, ProTableProps, TypeProps } from '@/components/ProTable/interface'
+
+// 接受父组件参数,配置默认值
+const props = withDefaults(defineProps<ProTableProps>(), {
+  columns: () => [],
+  searchColumns: () => [],
+  requestAuto: true,
+  pagination: true,
+  initParam: {},
+  border: true,
+  toolButton: true,
+  rowKey: 'id',
+  searchCol: () => ({ xs: 1, sm: 2, md: 2, lg: 3, xl: 4 })
+})
+
+const randomTableClass = 'random-' + generateUUID()
+
+// 是否显示搜索模块
+const isShowSearch = ref(true)
+
+// 表格 DOM 元素
+const tableRef = ref()
+
+// column 列类型
+const columnTypes: TypeProps[] = ['selection', 'radio', 'index', 'expand', 'sort']
+
+// 控制 ToolButton 显示
+const showToolButton = (key: 'refresh' | 'setting' | 'search') => {
+  return Array.isArray(props.toolButton) ? props.toolButton.includes(key) : props.toolButton
+}
+
+// 单选值
+const radio = ref('')
+
+// 表格多选 Hooks
+const { selectionChange, selectedList, selectedListIds, isSelected } = useSelection(props.rowKey)
+
+const reload = ref(true)
+const refresh = () => {
+  reload.value = false
+  nextTick(() => (reload.value = true))
+}
+
+// 表格操作 Hooks
+const { tableData, pageable, searchParam, searchInitParam, getTableList, search, reset, handleSizeChange, handleCurrentChange, loading } = useTable(
+  props.requestApi,
+  props.initParam,
+  props.pagination,
+  props.dataCallback,
+  props.requestError,
+  props.loadingTime
+)
+
+// 清空选中数据列表
+const clearSelection = () => tableRef.value?.clearSelection()
+
+// 初始化表格数据 && 拖拽排序
+onMounted(() => {
+  if (props.requestAuto) getTableList()
+  if (props.data) pageable.value.total = props.data.length
+})
+
+// 处理表格数据
+const processTableData = computed(() => {
+  if (!props.data) return tableData.value
+  if (!props.pagination) return props.data
+  return props.data.slice((pageable.value.pageNum - 1) * pageable.value.pageSize, pageable.value.pageSize * pageable.value.pageNum)
+})
+
+// 监听页面 initParam 改化,重新获取表格数据
+watch(() => props.initParam, getTableList, { deep: true })
+
+// 接收 columns 并设置为响应式
+const tableColumns = reactive<ColumnProps[]>(props.columns)
+
+// 定义 enumMap 存储 enum 值(避免异步请求无法格式化单元格内容 || 无法填充搜索下拉选择)
+const enumMap = ref(new Map<string, { [key: string]: any }[]>())
+const setEnumMap = async (col: ColumnProps) => {
+  if (!col.enum) return
+  // 如果当前 enumMap 存在相同的值 return
+  if (enumMap.value.has(col.prop!) && (typeof col.enum === 'function' || enumMap.value.get(col.prop!) === col.enum)) return
+  // 当前 enum 为静态数据,则直接存储到 enumMap
+  if (typeof col.enum !== 'function') return enumMap.value.set(col.prop!, unref(col.enum!))
+
+  // 为了防止接口执行慢,而存储慢,导致重复请求,所以预先存储为[],接口返回后再二次存储
+  enumMap.value.set(col.prop!, [])
+
+  // 当前 enum 为后台数据需要请求数据,则调用该请求接口,并存储到 enumMap
+  const { data } = await col.enum()
+  enumMap.value.set(col.prop!, data)
+}
+
+provide('enumMap', enumMap)
+
+// 扁平化 columns
+const flatColumnsFunc = (columns: ColumnProps[], flatArr: ColumnProps[] = []) => {
+  columns.forEach(async col => {
+    if (col._children?.length) flatArr.push(...flatColumnsFunc(col._children))
+    flatArr.push(col)
+
+    // 给每一项 column 添加 isShow && isFilterEnum 默认属性
+    col.isShow = col.isShow ?? true
+    col.isFilterEnum = col.isFilterEnum ?? true
+
+    // 设置 enumMap
+    setEnumMap(col)
+  })
+  return flatArr.filter(item => !item._children?.length)
+}
+flatColumnsFunc(tableColumns)
+
+// 过滤需要搜索的配置项
+const searchColumnsData = props.searchColumns
+
+// 设置搜索表单排序默认值 && 设置搜索表单项的默认值
+searchColumnsData.forEach((column, index) => {
+  column.order = column.order ?? index + 2
+  const key = column.key ?? handleProp(column.prop!)
+  const defaultValue = column.defaultValue
+  if (defaultValue !== undefined && defaultValue !== null) {
+    searchInitParam.value[key] = defaultValue
+    searchParam.value[key] = defaultValue
+  }
+})
+
+// 列设置 ==> 过滤掉不需要设置的列
+const colRef = ref()
+const colSetting = tableColumns.filter(item => !columnTypes.includes(item.type!) && item.prop !== 'operation' && item.isShow)
+const openColSetting = () => colRef.value.openColSetting()
+
+// 定义 emit 事件
+const emit = defineEmits<{
+  search: []
+  reset: []
+  dargSort: [{ newIndex?: number; oldIndex?: number }]
+}>()
+
+const _search = () => {
+  search()
+  emit('search')
+}
+
+const _reset = () => {
+  reset()
+  emit('reset')
+}
+
+// 暴露给父组件的参数和方法(外部需要什么,都可以从这里暴露出去)
+defineExpose({
+  element: tableRef,
+  tableData: processTableData,
+  radio,
+  pageable,
+  searchParam,
+  searchInitParam,
+  getTableList,
+  search,
+  reset,
+  handleSizeChange,
+  handleCurrentChange,
+  clearSelection,
+  enumMap,
+  isSelected,
+  selectedList,
+  selectedListIds,
+  refresh
+})
+</script>

+ 106 - 0
src/components/ProTable/interface/index.ts

@@ -0,0 +1,106 @@
+import type { BreakPoint, Responsive } from '@/components/Grid/interface'
+import type { TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults'
+import ProTable from '@/components/ProTable/index.vue'
+import { DefaultRow } from 'element-plus/es/components/table/src/table/defaults.mjs'
+
+export interface ProTableProps {
+  columns: ColumnProps[] // 列配置项  ==> 必传
+  searchColumns?: SearchProps[] // 搜索列配置项  ==> 必传
+  data?: any[] // 静态 table data 数据,若存在则不会使用 requestApi 返回的 data ==> 非必传
+  requestApi?: (params: any) => Promise<any> // 请求表格数据的 api ==> 非必传
+  requestAuto?: boolean // 是否自动执行请求 api ==> 非必传(默认为true)
+  requestError?: (params: any) => void // 表格 api 请求错误监听 ==> 非必传
+  dataCallback?: (data: any) => any // 返回数据的回调函数,可以对数据进行处理 ==> 非必传
+  title?: string // 表格标题 ==> 非必传
+  pagination?: boolean // 是否需要分页组件 ==> 非必传(默认为true)
+  initParam?: any // 初始化请求参数 ==> 非必传(默认为{})
+  border?: boolean // 是否带有纵向边框 ==> 非必传(默认为true)
+  toolButton?: ('refresh' | 'setting' | 'search')[] | boolean // 是否显示表格功能按钮 ==> 非必传(默认为true)
+  rowKey?: string // 行数据的 Key,用来优化 Table 的渲染,当表格数据多选时,所指定的 id ==> 非必传(默认为 id)
+  searchCol?: number | Record<BreakPoint, number> // 表格搜索项 每列占比配置 ==> 非必传 { xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }
+  loadingTime?: number // 表格数据加载时间 ==> 非必传(默认为200)
+}
+
+export interface EnumProps {
+  label?: string // 选项框显示的文字
+  value?: string | number | boolean | any[] // 选项框值
+  disabled?: boolean // 是否禁用此选项
+  tagType?: string // 当 tag 为 true 时,此选择会指定 tag 显示类型
+  children?: EnumProps[] // 为树形选择时,可以通过 children 属性指定子选项
+  [key: string]: any
+}
+
+export type TypeProps = 'index' | 'selection' | 'radio' | 'expand' | 'sort'
+
+export type SearchType =
+  | 'input'
+  | 'input-number'
+  | 'select'
+  | 'select-v2'
+  | 'tree-select'
+  | 'cascader'
+  | 'date-picker'
+  | 'time-picker'
+  | 'time-select'
+  | 'switch'
+  | 'slider'
+  | 'checkbox'
+
+export type SearchRenderScope = {
+  searchParam: { [key: string]: any }
+  placeholder: string
+  clearable: boolean
+  options: EnumProps[]
+  data: EnumProps[]
+}
+
+export interface SearchProps extends Partial<Record<BreakPoint, Responsive>> {
+  el?: SearchType // 当前项搜索框的类型
+  label?: string // 当前项搜索框的 label
+  props?: any // 搜索项参数,根据 element plus 官方文档来传递,该属性所有值会透传到组件
+  key?: string // 当搜索项 key 不为 prop 属性时,可通过 key 指定
+  tooltip?: string // 搜索提示
+  order?: number // 搜索项排序(从大到小)
+  span?: number // 搜索项所占用的列数,默认为 1 列
+  offset?: number // 搜索字段左侧偏移列数
+  defaultValue?: string | number | boolean | any[] | Ref<any> // 搜索项默认值
+  render?: (scope: SearchRenderScope) => VNode // 自定义搜索内容渲染(tsx语法)
+  prop?: string
+  fieldNames?: FieldNamesProps // 指定 label && value && children 的 key 值
+  enum?: EnumProps[] | Ref<EnumProps[]> | ((params?: any) => Promise<any>) // 枚举字典
+}
+
+export type FieldNamesProps = {
+  label: string
+  value: string
+  tagType?: string
+  children?: string
+}
+
+export type RenderScope<T extends DefaultRow> = {
+  row: T
+  $index: number
+  column: TableColumnCtx<T>
+  [key: string]: any
+}
+
+export type HeaderRenderScope<T extends DefaultRow> = {
+  $index: number
+  column: TableColumnCtx<T>
+  [key: string]: any
+}
+
+export interface ColumnProps<T extends DefaultRow = any>
+  extends Partial<Omit<TableColumnCtx<T>, 'type' | 'children' | 'renderCell' | 'renderHeader'>> {
+  type?: TypeProps // 列类型
+  tag?: boolean | Ref<boolean> // 是否是标签展示
+  isShow?: boolean | Ref<boolean> // 是否显示在表格当中
+  enum?: EnumProps[] | Ref<EnumProps[]> | ((params?: any) => Promise<any>) // 枚举字典
+  isFilterEnum?: boolean | Ref<boolean> // 当前单元格值是否根据 enum 格式化(示例:enum 只作为搜索项数据)
+  fieldNames?: FieldNamesProps // 指定 label && value && children 的 key 值
+  headerRender?: (scope: HeaderRenderScope<T>) => VNode // 自定义表头内容渲染(tsx语法)
+  render?: (scope: RenderScope<T>) => VNode | string // 自定义单元格内容渲染(tsx语法)
+  _children?: ColumnProps<T>[] // 多级表头
+}
+
+export type ProTableInstance = Omit<InstanceType<typeof ProTable>, keyof ComponentPublicInstance | keyof ProTableProps>

+ 87 - 0
src/components/SearchForm/SearchFormItem.vue

@@ -0,0 +1,87 @@
+<template>
+  <component
+    :is="column.render ?? `el-${column.el}`"
+    v-bind="{ ...handleSearchProps, ...placeholder, searchParam: _searchParam, clearable }"
+    v-model.trim="_searchParam[column.key ?? handleProp(column.prop!)]"
+    :data="column.el === 'tree-select' ? columnEnum : []"
+    :options="['cascader', 'select-v2'].includes(column.el!) ? columnEnum : []">
+    <template v-if="column.el === 'cascader'" #default="{ data }">
+      <span>{{ data[fieldNames.label] }}</span>
+    </template>
+    <template v-if="column.el === 'select'">
+      <component :is="`el-option`" v-for="(col, index) in columnEnum" :key="index" :label="col[fieldNames.label]" :value="col[fieldNames.value]" />
+    </template>
+    <slot v-else />
+  </component>
+</template>
+
+<script setup lang="ts" name="SearchFormItem">
+import { handleProp } from '@/utils'
+import type { SearchProps } from '@/components/ProTable/interface'
+interface SearchFormItem {
+  column: SearchProps
+  searchParam: { [key: string]: any }
+}
+const props = defineProps<SearchFormItem>()
+
+// Re receive SearchParam
+const _searchParam = computed(() => props.searchParam)
+
+// 判断 fieldNames 设置 label && value && children 的 key 值
+const fieldNames = computed(() => {
+  return {
+    label: props.column.fieldNames?.label ?? 'label',
+    value: props.column.fieldNames?.value ?? 'value',
+    children: props.column.fieldNames?.children ?? 'children'
+  }
+})
+
+// 接收 enumMap (el 为 select-v2 需单独处理 enumData)
+const enumMap = inject('enumMap', ref(new Map()))
+const columnEnum = computed(() => {
+  let enumData = enumMap.value.get(props.column.prop) || props.column.enum
+  if (!enumData) return []
+  if (props.column.el === 'select-v2' && props.column.fieldNames) {
+    enumData = enumData.map((item: { [key: string]: any }) => {
+      return { ...item, label: item[fieldNames.value.label], value: item[fieldNames.value.value] }
+    })
+  }
+  return enumData
+})
+
+// 处理透传的 searchProps (el 为 tree-select、cascader 的时候需要给下默认 label && value && children)
+const handleSearchProps = computed(() => {
+  const label = fieldNames.value.label
+  const value = fieldNames.value.value
+  const children = fieldNames.value.children
+  const searchEl = props.column.el
+  let searchProps = props.column.props ?? {}
+  if (searchEl === 'tree-select') {
+    searchProps = {
+      ...searchProps,
+      props: { ...searchProps.props, label, children },
+      nodeKey: value
+    }
+  }
+  if (searchEl === 'cascader') {
+    searchProps = { ...searchProps, props: { ...searchProps.props, label, value, children } }
+  }
+  return searchProps
+})
+
+// 处理默认 placeholder
+const placeholder = computed(() => {
+  const search = props.column
+  if (['datetimerange', 'daterange', 'monthrange'].includes(search?.props?.type) || search?.props?.isRange) {
+    return { rangeSeparator: '至', startPlaceholder: '开始时间', endPlaceholder: '结束时间' }
+  }
+  const placeholder = search?.props?.placeholder ?? (search?.el?.includes('input') ? '请输入' : '请选择')
+  return { placeholder }
+})
+
+// 是否有清除按钮 (当搜索项有默认值时,清除按钮不显示)
+const clearable = computed(() => {
+  const search = props.column
+  return search?.props?.clearable ?? (search?.defaultValue === null || search?.defaultValue === undefined)
+})
+</script>

+ 86 - 0
src/components/SearchForm/index.vue

@@ -0,0 +1,86 @@
+<template>
+  <div v-if="columns.length" class="card table-search">
+    <el-form ref="formRef" :model="searchParam">
+      <Grid ref="gridRef" :collapsed="collapsed" :gap="[20, 0]" :cols="searchCol">
+        <GridItem v-for="(item, index) in columns" :key="item.prop" v-bind="getResponsive(item)" :index="index">
+          <el-form-item>
+            <template #label>
+              <el-space :size="4">
+                <span>{{ item.label }}</span>
+                <el-tooltip v-if="item.tooltip" effect="dark" :content="item.tooltip" placement="top">
+                  <i :class="'iconfont icon-yiwen'" />
+                </el-tooltip>
+              </el-space>
+              <span>:</span>
+            </template>
+            <SearchFormItem :column="item" :search-param="searchParam" />
+          </el-form-item>
+        </GridItem>
+        <GridItem suffix>
+          <div class="operation">
+            <el-button type="primary" icon="Search" @click="search"> 搜索 </el-button>
+            <el-button icon="Delete" @click="reset"> 重置 </el-button>
+            <el-button v-if="showCollapse" type="primary" link class="search-isOpen" @click="collapsed = !collapsed">
+              {{ collapsed ? '展开' : '合并' }}
+              <el-icon class="el-icon--right">
+                <component :is="collapsed ? 'ArrowDown' : 'ArrowUp'" />
+              </el-icon>
+            </el-button>
+          </div>
+        </GridItem>
+      </Grid>
+    </el-form>
+  </div>
+</template>
+<script setup lang="ts" name="SearchForm">
+import type { SearchProps } from '@/components/ProTable/interface'
+import type { BreakPoint } from '@/components/Grid/interface'
+
+interface ProTableProps {
+  columns?: SearchProps[] // 搜索配置列
+  searchParam?: { [key: string]: any } // 搜索参数
+  searchCol: number | Record<BreakPoint, number>
+  search: (params: any) => void // 搜索方法
+  reset: (params: any) => void // 重置方法
+}
+
+const props = withDefaults(defineProps<ProTableProps>(), {
+  columns: () => [],
+  searchParam: () => ({})
+})
+
+// 获取响应式设置
+const getResponsive = (item: SearchProps) => {
+  return {
+    span: item.span,
+    offset: item.offset ?? 0,
+    xs: item.xs,
+    sm: item.sm,
+    md: item.md,
+    lg: item.lg,
+    xl: item.xl
+  }
+}
+
+// 是否默认折叠搜索项
+const collapsed = ref(true)
+
+// 获取响应式断点
+const gridRef = ref()
+const breakPoint = computed<BreakPoint>(() => gridRef.value?.breakPoint)
+
+// 判断是否显示 展开/合并 按钮
+const showCollapse = computed(() => {
+  let show = false
+  props.columns.reduce((prev, current) => {
+    prev += (current[breakPoint.value]?.span ?? current?.span ?? 1) + (current[breakPoint.value]?.offset ?? current?.offset ?? 0)
+    if (typeof props.searchCol !== 'number') {
+      if (prev >= props.searchCol[breakPoint.value]) show = true
+    } else {
+      if (prev >= props.searchCol) show = true
+    }
+    return prev
+  }, 0)
+  return show
+})
+</script>

+ 2 - 0
src/hooks/index.ts

@@ -0,0 +1,2 @@
+export * from './useTable'
+export * from './useSelection'

+ 24 - 0
src/hooks/interface/index.ts

@@ -0,0 +1,24 @@
+export type Pageable = {
+  pageNum: number
+  pageSize: number
+  total: number
+}
+export type StateProps = {
+  tableData: any[]
+  pageable: Pageable
+  searchParam: {
+    [key: string]: any
+  }
+  searchInitParam: {
+    [key: string]: any
+  }
+  totalParam: {
+    [key: string]: any
+  }
+  icon?: {
+    [key: string]: any
+  }
+  loading?: boolean
+}
+
+export type HandleDataMessageType = '' | 'success' | 'warning' | 'info' | 'error'

+ 32 - 0
src/hooks/useSelection.ts

@@ -0,0 +1,32 @@
+/**
+ * @description 表格多选数据操作
+ * @param {String} rowKey 当表格可以多选时,所指定的 id
+ * */
+export const useSelection = (rowKey: string = 'id') => {
+  const isSelected = ref(false)
+  const selectedList = ref<{ [key: string | number]: any }[]>([])
+
+  // 当前选中的所有 ids 数组
+  const selectedListIds = computed(() => {
+    const ids: (string | number)[] = []
+    selectedList.value.forEach(item => ids.push(item[rowKey]))
+    return ids
+  })
+
+  /**
+   * @description 多选操作
+   * @param {Array} rowArr 当前选择的所有数据
+   * @return void
+   */
+  const selectionChange = (rowArr: { [key: string]: any }[]) => {
+    isSelected.value = !!rowArr.length
+    selectedList.value = rowArr
+  }
+
+  return {
+    isSelected,
+    selectedList,
+    selectedListIds,
+    selectionChange
+  }
+}

+ 177 - 0
src/hooks/useTable.ts

@@ -0,0 +1,177 @@
+import type { Pageable, StateProps } from '@/hooks/interface'
+
+/**
+ * @description table 页面操作方法封装
+ * @param {Function} api 获取表格数据 api 方法 (必传)
+ * @param {Object} initParam 获取数据初始化参数 (非必传,默认为{})
+ * @param {Boolean} isPageable 是否有分页 (非必传,默认为true)
+ * @param {Function} dataCallBack 对后台返回的数据进行处理的方法 (非必传)
+ * @param {Function} requestError 对请求数据错误处理的方法 (非必传)
+ * @param loadingTime loading 延迟时间 (非必传,默认为 0 ms)
+ * */
+export const useTable = (
+  api?: (params: any) => Promise<any>,
+  initParam: object = {},
+  isPageable: boolean = true,
+  dataCallBack?: (data: any) => any,
+  requestError?: (params: any) => void,
+  loadingTime?: number
+) => {
+  const state = reactive<StateProps>({
+    // 表格数据
+    tableData: [],
+    // 分页数据
+    pageable: {
+      // 当前页数
+      pageNum: 1,
+      // 每页显示条数
+      pageSize: 10,
+      // 总条数
+      total: 0
+    },
+    // 查询参数(只包括查询)
+    searchParam: {},
+    // 初始化默认的查询参数
+    searchInitParam: {},
+    // 总参数(包含分页和查询参数)
+    totalParam: {},
+    loading: false
+  })
+
+  /**
+   * @description 分页查询参数(只包括分页和表格字段排序,其他排序方式可自行配置)
+   * */
+  const pageParam = computed({
+    get: () => {
+      return {
+        pageNum: state.pageable.pageNum,
+        pageSize: state.pageable.pageSize
+      }
+    },
+    set: newVal => {
+      console.log('我是分页更新之后的值', newVal)
+    }
+  })
+
+  /**
+   * @description 获取表格数据
+   * @return void
+   * */
+  const getTableList = async () => {
+    if (!api) return
+    state.loading = true
+    try {
+      await delayLoading(loadingTime)
+      // 先把初始化参数和分页参数放到总参数里面
+      Object.assign(state.totalParam, initParam, isPageable ? pageParam.value : {})
+      let { data } = await api({ ...state.searchInitParam, ...state.totalParam })
+      if (dataCallBack) data = dataCallBack(data)
+      if (typeof data === 'object' && Object.keys(data).length === 0) {
+        state.tableData = isPageable ? data.list : []
+      } else {
+        state.tableData = isPageable ? data.list : data
+      }
+      state.loading = false
+      // 解构后台返回的分页数据 (如果有分页更新分页信息)
+      if (isPageable) {
+        const { pageNum, pageSize, total } = data
+        updatePageable({ pageNum, pageSize, total })
+      }
+    } catch (error) {
+      state.loading = false
+      if (requestError) requestError(error)
+    }
+  }
+
+  /**
+   * @description 更新查询参数
+   * @return void
+   * */
+  const updatedTotalParam = () => {
+    state.totalParam = {}
+    // 处理查询参数,可以给查询参数加自定义前缀操作
+    const nowSearchParam: StateProps['searchParam'] = {}
+    // 防止手动清空输入框携带参数(这里可以自定义查询参数前缀)
+    for (const key in state.searchParam) {
+      // * 某些情况下参数为 false/0 也应该携带参数
+      if (state.searchParam[key] || state.searchParam[key] === false || state.searchParam[key] === 0) {
+        nowSearchParam[key] = state.searchParam[key]
+      }
+    }
+    Object.assign(state.totalParam, nowSearchParam, isPageable ? pageParam.value : {})
+  }
+
+  /**
+   * @description 更新分页信息
+   * @param {Object} pageable 后台返回的分页数据
+   * @return void
+   * */
+  const updatePageable = (pageable: Pageable) => {
+    Object.assign(state.pageable, pageable)
+  }
+
+  /**
+   * @description 表格数据查询
+   * @return void
+   * */
+  const search = () => {
+    state.pageable.pageNum = 1
+    updatedTotalParam()
+    getTableList()
+  }
+
+  /**
+   * @description 表格数据重置
+   * @return void
+   * */
+  const reset = () => {
+    state.pageable.pageNum = 1
+    state.searchParam = {}
+    // 重置搜索表单的时,如果有默认搜索参数,则重置默认的搜索参数
+    state.searchParam = { ...state.searchInitParam }
+    updatedTotalParam()
+    getTableList()
+  }
+
+  /**
+   * @description 每页条数改变
+   * @param {Number} val 当前条数
+   * @return void
+   * */
+  const handleSizeChange = (val: number) => {
+    state.pageable.pageNum = 1
+    state.pageable.pageSize = val
+    getTableList()
+  }
+
+  /**
+   * @description 当前页改变
+   * @param {Number} val 当前页
+   * @return void
+   * */
+  const handleCurrentChange = (val: number) => {
+    state.pageable.pageNum = val
+    getTableList()
+  }
+
+  const delayLoading = async (loadingTime: number | undefined) => {
+    const defaultLoadingTime = 0 // 默认的 loading 延迟时间为0秒
+    let actualLoadingTime = loadingTime
+    // 判断 loadingTime 是否为特殊参数
+    if (typeof loadingTime === 'undefined' || loadingTime === null || loadingTime === -1) {
+      actualLoadingTime = defaultLoadingTime
+    }
+    // 等待 loading 延迟时间
+    await new Promise(resolve => setTimeout(resolve, actualLoadingTime))
+  }
+
+  return {
+    ...toRefs(state),
+    getTableList,
+    search,
+    reset,
+    handleSizeChange,
+    handleCurrentChange,
+    updatedTotalParam
+  }
+}

+ 1 - 0
src/layouts/components/AppMain/index.vue

@@ -7,3 +7,4 @@
     </transition>
   </router-view>
 </template>
+<script lang="ts" name="AppMain" setup></script>

+ 1 - 0
src/layouts/components/AppMenu/index.vue

@@ -29,3 +29,4 @@
     <el-menu-item index="5">系统配置</el-menu-item>
   </el-menu>
 </template>
+<script lang="ts" name="AppMenu" setup></script>

+ 1 - 1
src/layouts/components/AppTools/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="tool-bar-ri">
+  <div class="tool-bar">
     <span class="username" style="margin-right: 8px">{{ userStore.user.userName }}</span>
     <el-avatar :icon="UserFilled" />
   </div>

+ 1 - 1
src/layouts/components/index.scss

@@ -13,7 +13,7 @@
     white-space: nowrap;
   }
 }
-.tool-bar-ri {
+.tool-bar {
   display: flex;
   align-items: center;
   justify-content: center;

+ 1 - 1
src/layouts/container/AppTransverse/index.vue

@@ -15,5 +15,5 @@
 </template>
 <script lang="tsx" setup name="LayoutTransverse"></script>
 <style lang="scss" scoped>
-@use './index';
+@use '../index';
 </style>

+ 0 - 0
src/layouts/container/AppTransverse/index.scss → src/layouts/container/index.scss


+ 20 - 0
src/types/auto-components.d.ts

@@ -15,10 +15,12 @@ declare module 'vue' {
     AppMenu: typeof import('./../layouts/components/AppMenu/index.vue')['default']
     AppTools: typeof import('./../layouts/components/AppTools/index.vue')['default']
     AppTransverse: typeof import('./../layouts/container/AppTransverse/index.vue')['default']
+    ColSetting: typeof import('./../components/ProTable/ColSetting.vue')['default']
     ElAvatar: typeof import('element-plus/es')['ElAvatar']
     ElButton: typeof import('element-plus/es')['ElButton']
     ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
     ElContainer: typeof import('element-plus/es')['ElContainer']
+    ElDrawer: typeof import('element-plus/es')['ElDrawer']
     ElFooter: typeof import('element-plus/es')['ElFooter']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
@@ -28,10 +30,28 @@ declare module 'vue' {
     ElMain: typeof import('element-plus/es')['ElMain']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
+    ElPagination: typeof import('element-plus/es')['ElPagination']
+    ElRadio: typeof import('element-plus/es')['ElRadio']
+    ElSpace: typeof import('element-plus/es')['ElSpace']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
+    ElSwitch: typeof import('element-plus/es')['ElSwitch']
+    ElTable: typeof import('element-plus/es')['ElTable']
+    ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
+    ElTag: typeof import('element-plus/es')['ElTag']
+    ElTooltip: typeof import('element-plus/es')['ElTooltip']
+    Grid: typeof import('./../components/Grid/index.vue')['default']
+    GridItem: typeof import('./../components/Grid/GridItem.vue')['default']
     HelloWorld: typeof import('./../components/HelloWorld.vue')['default']
     LoginForm: typeof import('./../views/login/components/LoginForm.vue')['default']
+    Pagination: typeof import('./../components/ProTable/Pagination.vue')['default']
+    ProTable: typeof import('./../components/ProTable/index.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
+    SearchForm: typeof import('./../components/SearchForm/index.vue')['default']
+    SearchFormItem: typeof import('./../components/SearchForm/SearchFormItem.vue')['default']
+    TableColumn: typeof import('./../components/ProTable/TableColumn.vue')['default']
+  }
+  export interface GlobalDirectives {
+    vLoading: typeof import('element-plus/es')['ElLoadingDirective']
   }
 }

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

@@ -36,10 +36,9 @@ declare interface Result {
 declare interface BaseEntity {
   /** 乐观锁 */
   version?: number
-  createBy?: any
-  createDept?: any
+  createBy?: string
   createTime?: string
-  updateBy?: any
+  updateBy?: string
   updateTime?: any
 }
 

+ 53 - 15
src/views/home/index.vue

@@ -1,19 +1,57 @@
 <template>
-  <div class="content-box">
-    <div class="text" style="height: 300px">首页</div>
-    <div class="text" style="height: 300px">首页</div>
-    <div class="text" style="height: 300px">首页</div>
-    <div class="text" style="height: 300px">首页</div>
-    <div class="text" style="height: 300px">首页</div>
-    <div class="text" style="height: 300px">首页</div>
-    <div class="text" style="height: 300px">首页</div>
-    <div class="text" style="height: 300px">首页</div>
-    <div class="text" style="height: 300px">首页</div>
-    <div class="text" style="height: 300px">首页</div>
-    <div class="text" style="height: 300px">首页</div>
-    <div class="text" style="height: 300px">首页</div>
+  <div class="table-box">
+    <ProTable
+      ref="proTableRef"
+      title="角色列表"
+      row-key="roleId"
+      :indent="20"
+      :columns="columns"
+      :search-columns="searchColumns"
+      :request-api="getTableList">
+      <!-- 表格 header 按钮 -->
+      <template #tableHeader="scope">
+        <el-button type="primary" icon="CirclePlus"> 新增角色 </el-button>
+        <el-button type="danger" icon="Delete" plain :disabled="!scope.isSelected"> 批量删除角色 </el-button>
+      </template>
+
+      <template #operation="{ row }">
+        <el-button type="primary" link icon="Lock"> 权限 </el-button>
+        <el-button type="primary" link icon="EditPen"> 编辑 </el-button>
+        <el-button v-if="row.id !== 1" type="primary" link icon="Delete"> 删除 </el-button>
+      </template>
+    </ProTable>
   </div>
 </template>
-<script lang="tsx" setup>
-import { Position } from '@element-plus/icons-vue'
+<script lang="tsx" setup name="Home">
+import { RoleInfo, RoleQuery } from '@/api/interface/system/role'
+import RoleApi from '@/api/module/system/role'
+import { ColumnProps, SearchProps } from '@/components/ProTable/interface'
+
+// 表格配置项
+const columns: ColumnProps<RoleInfo>[] = [
+  { type: 'selection', width: 60, selectable: row => row.isLock == '1' },
+  { prop: 'roleId', label: '编号', width: 80 },
+  { prop: 'name', label: '角色名称' },
+  { prop: 'code', tag: true, label: '权限标识' },
+  { prop: 'remark', label: '备注' },
+  { prop: 'createByName', label: '创建人' },
+  { prop: 'createTime', label: '创建时间' },
+  { prop: 'updateTime', label: '修改时间' },
+  { prop: 'operation', label: '操作', width: 250, fixed: 'right' }
+]
+
+// 表格配置项
+const searchColumns: SearchProps[] = [
+  { prop: 'name', label: '角色名称', el: 'input' },
+  { prop: 'name', label: '角色名称', el: 'input' },
+  { prop: 'name', label: '角色名称', el: 'input' },
+  { prop: 'name', label: '角色名称', el: 'input' },
+  { prop: 'name', label: '角色名称', el: 'input' },
+  { prop: 'name', label: '角色名称', el: 'input' },
+  { prop: 'name', label: '角色名称', el: 'input' },
+  { prop: 'code', label: '标识', el: 'input' }
+]
+
+// 获取table列表
+const getTableList = (params: RoleQuery) => RoleApi.pageRoleApi(params)
 </script>

+ 13 - 1
vite.config.ts

@@ -56,7 +56,19 @@ export default defineConfig(({ mode, command }: ConfigEnv): UserConfig => {
         'element-plus/es/components/message/style/css',
         'element-plus/es/components/menu/style/css',
         'element-plus/es/components/sub-menu/style/css',
-        'element-plus/es/components/menu-item/style/css'
+        'element-plus/es/components/menu-item/style/css',
+        'element-plus/es/components/loading/style/css',
+        'element-plus/es/components/table/style/css',
+        'element-plus/es/components/table-column/style/css',
+        'element-plus/es/components/tag/style/css',
+        'element-plus/es/components/icon/style/css',
+        'element-plus/es/components/radio/style/css',
+        'element-plus/es/components/avatar/style/css',
+        'element-plus/es/components/drawer/style/css',
+        'element-plus/es/components/switch/style/css',
+        'element-plus/es/components/pagination/style/css',
+        'element-plus/es/components/space/style/css',
+        'element-plus/es/components/tooltip/style/css'
       ]
     }
   }