refactor: 使用 Element Plus 替换 TDesign
- 将 TDesign 组件替换为 Element Plus组件 - 更新样式和布局以适应 Element Plus - 修改图标使用 Element Plus 的图标库 -调整表格、对话框等组件的用法 - 更新消息提示插件
This commit is contained in:
parent
7504b2a9de
commit
2b41ed27ac
|
@ -8,9 +8,9 @@
|
||||||
"name": "deploy-helper-front",
|
"name": "deploy-helper-front",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"tdesign-icons-vue-next": "^0.2.6",
|
"element-plus": "^2.6.1",
|
||||||
"tdesign-vue-next": "^1.10.4",
|
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.12",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0"
|
||||||
},
|
},
|
||||||
|
@ -53,15 +53,6 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
|
||||||
"version": "7.28.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.2.tgz",
|
|
||||||
"integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.28.2",
|
"version": "7.28.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.2.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.2.tgz",
|
||||||
|
@ -75,6 +66,24 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@ctrl/tinycolor": {
|
||||||
|
"version": "3.6.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
|
||||||
|
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@element-plus/icons-vue": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.8",
|
"version": "0.25.8",
|
||||||
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
|
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
|
||||||
|
@ -517,6 +526,31 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.7.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.3.tgz",
|
||||||
|
"integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.7.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.3.tgz",
|
||||||
|
"integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.3",
|
||||||
|
"@floating-ui/utils": "^0.2.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.10",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||||
|
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.4",
|
"version": "1.5.4",
|
||||||
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
|
||||||
|
@ -852,22 +886,10 @@
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/sortablejs": {
|
"node_modules/@types/web-bluetooth": {
|
||||||
"version": "1.15.8",
|
"version": "0.0.16",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.8.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
|
||||||
"integrity": "sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==",
|
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/tinycolor2": {
|
|
||||||
"version": "1.4.6",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@types/tinycolor2/-/tinycolor2-1.4.6.tgz",
|
|
||||||
"integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/validator": {
|
|
||||||
"version": "13.15.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@types/validator/-/validator-13.15.2.tgz",
|
|
||||||
"integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==",
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-vue": {
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
|
@ -993,6 +1015,100 @@
|
||||||
"integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==",
|
"integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@vueuse/core": {
|
||||||
|
"version": "9.13.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz",
|
||||||
|
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/web-bluetooth": "^0.0.16",
|
||||||
|
"@vueuse/metadata": "9.13.0",
|
||||||
|
"@vueuse/shared": "9.13.0",
|
||||||
|
"vue-demi": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/core/node_modules/vue-demi": {
|
||||||
|
"version": "0.14.10",
|
||||||
|
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||||
|
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/metadata": {
|
||||||
|
"version": "9.13.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz",
|
||||||
|
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/shared": {
|
||||||
|
"version": "9.13.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz",
|
||||||
|
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vue-demi": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/shared/node_modules/vue-demi": {
|
||||||
|
"version": "0.14.10",
|
||||||
|
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
|
||||||
|
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||||
|
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@vue/composition-api": "^1.0.0-rc.1",
|
||||||
|
"vue": "^3.0.0-0 || ^2.6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@vue/composition-api": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/async-validator": {
|
||||||
|
"version": "4.2.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
|
||||||
|
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
@ -1070,6 +1186,32 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/element-plus": {
|
||||||
|
"version": "2.10.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.10.5.tgz",
|
||||||
|
"integrity": "sha512-O9wTDu3Tm51ACVByWrThtBhH4Ygefg1HGY5pyAaxnoIrj8uMN0GtZ4IREwR3Yw/6sM2HyxjrsGI/D46iUVP97A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ctrl/tinycolor": "^3.4.1",
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@floating-ui/dom": "^1.0.1",
|
||||||
|
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
|
||||||
|
"@types/lodash": "^4.14.182",
|
||||||
|
"@types/lodash-es": "^4.17.6",
|
||||||
|
"@vueuse/core": "^9.1.0",
|
||||||
|
"async-validator": "^4.2.5",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"lodash-es": "^4.17.21",
|
||||||
|
"lodash-unified": "^1.0.2",
|
||||||
|
"memoize-one": "^6.0.0",
|
||||||
|
"normalize-wheel-es": "^1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/entities": {
|
"node_modules/entities": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
|
||||||
|
@ -1169,6 +1311,12 @@
|
||||||
"@esbuild/win32-x64": "0.25.8"
|
"@esbuild/win32-x64": "0.25.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/escape-html": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/estree-walker": {
|
"node_modules/estree-walker": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
|
@ -1338,12 +1486,29 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash-es": {
|
"node_modules/lodash-es": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
|
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash-unified": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/lodash-es": "*",
|
||||||
|
"lodash": "*",
|
||||||
|
"lodash-es": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.17",
|
"version": "0.30.17",
|
||||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz",
|
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.17.tgz",
|
||||||
|
@ -1362,6 +1527,12 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/memoize-one": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mime-db": {
|
"node_modules/mime-db": {
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
@ -1383,12 +1554,6 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mitt": {
|
|
||||||
"version": "3.0.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
|
|
||||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.11",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
|
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
@ -1407,6 +1572,12 @@
|
||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/normalize-wheel-es": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
@ -1500,12 +1671,6 @@
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sortablejs": {
|
|
||||||
"version": "1.15.6",
|
|
||||||
"resolved": "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.6.tgz",
|
|
||||||
"integrity": "sha512-aNfiuwMEpfBM/CN6LY0ibyhxPfPbyFeBTYJKCvzkJ2GkUpazIt3H+QIPAMHwqQ7tMKaHz1Qj+rJJCqljnf4p3A==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
@ -1515,61 +1680,6 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tdesign-icons-vue-next": {
|
|
||||||
"version": "0.2.6",
|
|
||||||
"resolved": "https://registry.npmmirror.com/tdesign-icons-vue-next/-/tdesign-icons-vue-next-0.2.6.tgz",
|
|
||||||
"integrity": "sha512-Se/3N6AAfbpEyJfuBUCMcBl26FCfwUYCf9OwEFp1dw0HhMeCd6LENoZx0o43/V69NpcUGp+0Ya+xVuNKiP8M+Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.16.3"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tdesign-vue-next": {
|
|
||||||
"version": "1.15.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/tdesign-vue-next/-/tdesign-vue-next-1.15.2.tgz",
|
|
||||||
"integrity": "sha512-EGuI3zJZD5JA6BB+s/+PeFZ08lqcTJtFCdI4O+1QJ/ru4Rie6fTgkrCL+IR9R3NQNyG4Di5T2SziAbSH1v9j7Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.22.6",
|
|
||||||
"@popperjs/core": "^2.11.8",
|
|
||||||
"@types/lodash-es": "^4.17.12",
|
|
||||||
"@types/sortablejs": "^1.15.1",
|
|
||||||
"@types/tinycolor2": "^1.4.3",
|
|
||||||
"@types/validator": "^13.7.17",
|
|
||||||
"dayjs": "^1.11.10",
|
|
||||||
"lodash-es": "^4.17.21",
|
|
||||||
"mitt": "^3.0.1",
|
|
||||||
"sortablejs": "^1.15.0",
|
|
||||||
"tdesign-icons-vue-next": "^0.3.6",
|
|
||||||
"tinycolor2": "^1.6.0",
|
|
||||||
"validator": "^13.9.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": ">=3.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tdesign-vue-next/node_modules/tdesign-icons-vue-next": {
|
|
||||||
"version": "0.3.6",
|
|
||||||
"resolved": "https://registry.npmmirror.com/tdesign-icons-vue-next/-/tdesign-icons-vue-next-0.3.6.tgz",
|
|
||||||
"integrity": "sha512-X9u90dBv8tPhfpguUyx+BzF8CU2ef2L4RXOO7MYOj1ufHCHwBXTF8L3GPfq6KZd/2u4vMLYAA8lGURn4PZZICw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.16.3"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tinycolor2": {
|
|
||||||
"version": "1.6.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.6.0.tgz",
|
|
||||||
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.14",
|
"version": "0.2.14",
|
||||||
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.14.tgz",
|
||||||
|
@ -1594,15 +1704,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/validator": {
|
|
||||||
"version": "13.15.15",
|
|
||||||
"resolved": "https://registry.npmmirror.com/validator/-/validator-13.15.15.tgz",
|
|
||||||
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.0.6.tgz",
|
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.0.6.tgz",
|
||||||
|
|
|
@ -11,8 +11,8 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vue": "^3.5.12",
|
"vue": "^3.5.12",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
"tdesign-vue-next": "^1.10.4",
|
"element-plus": "^2.6.1",
|
||||||
"tdesign-icons-vue-next": "^0.2.6",
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
"axios": "^1.7.9"
|
"axios": "^1.7.9"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { MessagePlugin } from 'tdesign-vue-next'
|
import { ElMessage } from 'element-plus'
|
||||||
import auth from '@/utils/auth'
|
import auth from '@/utils/auth'
|
||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
|
@ -41,23 +41,23 @@ api.interceptors.response.use(
|
||||||
case 401:
|
case 401:
|
||||||
// 未授权,清除token并跳转到登录页
|
// 未授权,清除token并跳转到登录页
|
||||||
auth.logout()
|
auth.logout()
|
||||||
MessagePlugin.error('登录已过期,请重新登录')
|
ElMessage.error('登录已过期,请重新登录')
|
||||||
window.location.href = '/login'
|
window.location.href = '/login'
|
||||||
break
|
break
|
||||||
case 403:
|
case 403:
|
||||||
MessagePlugin.error('没有权限访问该资源')
|
ElMessage.error('没有权限访问该资源')
|
||||||
break
|
break
|
||||||
case 404:
|
case 404:
|
||||||
MessagePlugin.error('请求的资源不存在')
|
ElMessage.error('请求的资源不存在')
|
||||||
break
|
break
|
||||||
case 500:
|
case 500:
|
||||||
MessagePlugin.error('服务器内部错误')
|
ElMessage.error('服务器内部错误')
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
MessagePlugin.error(data?.message || '请求失败')
|
ElMessage.error(data?.message || '请求失败')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
MessagePlugin.error('网络连接失败,请检查网络设置')
|
ElMessage.error('网络连接失败,请检查网络设置')
|
||||||
}
|
}
|
||||||
|
|
||||||
return Promise.reject(error)
|
return Promise.reject(error)
|
||||||
|
|
|
@ -1,69 +1,84 @@
|
||||||
<template>
|
<template>
|
||||||
<t-layout style="height: 100vh;">
|
<el-container style="height: 100vh;">
|
||||||
<t-header class="header">
|
<el-header class="header">
|
||||||
<div class="header-container">
|
<div class="header-container">
|
||||||
<t-head-menu
|
<el-menu
|
||||||
:value="activeMenu"
|
:default-active="activeMenu"
|
||||||
theme="light"
|
mode="horizontal"
|
||||||
@change="handleMenuChange"
|
@select="handleMenuChange"
|
||||||
|
class="header-menu"
|
||||||
>
|
>
|
||||||
<template #logo>
|
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<span class="logo-text">DeployHelper</span>
|
<span class="logo-text">DeployHelper</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 首页菜单项 - 未登录也能看到 -->
|
<!-- 首页菜单项 - 未登录也能看到 -->
|
||||||
<t-menu-item value="home">首页</t-menu-item>
|
<el-menu-item index="home">首页</el-menu-item>
|
||||||
|
|
||||||
<!-- 其他菜单项 - 只有登录后才显示 -->
|
<!-- 其他菜单项 - 只有登录后才显示 -->
|
||||||
<template v-if="isLoggedIn">
|
<template v-if="isLoggedIn">
|
||||||
<t-menu-item value="deploy">部署管理</t-menu-item>
|
<el-menu-item index="deploy">部署管理</el-menu-item>
|
||||||
<t-menu-item value="files">文件管理</t-menu-item>
|
<el-menu-item index="files">文件管理</el-menu-item>
|
||||||
<t-menu-item value="logs">日志管理</t-menu-item>
|
<el-menu-item index="logs">日志管理</el-menu-item>
|
||||||
<t-menu-item value="users">用户管理</t-menu-item>
|
<el-menu-item index="users">用户管理</el-menu-item>
|
||||||
</template>
|
</template>
|
||||||
</t-head-menu>
|
</el-menu>
|
||||||
|
|
||||||
<!-- 右侧操作区域 -->
|
<!-- 右侧操作区域 -->
|
||||||
<div class="header-actions">
|
<div class="header-actions">
|
||||||
<!-- 已登录:显示用户信息下拉菜单 -->
|
<!-- 已登录:显示用户信息下拉菜单 -->
|
||||||
<t-dropdown
|
<el-dropdown
|
||||||
v-if="isLoggedIn"
|
v-if="isLoggedIn"
|
||||||
:options="userMenuOptions"
|
@command="handleUserMenuClick"
|
||||||
@click="handleUserMenuClick"
|
|
||||||
>
|
>
|
||||||
<t-button variant="text" class="user-button">
|
<el-button type="text" class="user-button">
|
||||||
<t-icon name="user" />
|
<el-icon><User /></el-icon>
|
||||||
<span class="username">{{ userInfo?.username || '用户' }}</span>
|
<span class="username">{{ userInfo?.username || '用户' }}</span>
|
||||||
<t-icon name="chevron-down" />
|
<el-icon><ArrowDown /></el-icon>
|
||||||
</t-button>
|
</el-button>
|
||||||
</t-dropdown>
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="profile">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
个人信息
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="change-password">
|
||||||
|
<el-icon><Lock /></el-icon>
|
||||||
|
修改密码
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="logout" divided>
|
||||||
|
<el-icon><SwitchButton /></el-icon>
|
||||||
|
退出登录
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
|
||||||
<!-- 未登录:显示登录按钮 -->
|
<!-- 未登录:显示登录按钮 -->
|
||||||
<t-button
|
<el-button
|
||||||
v-else
|
v-else
|
||||||
theme="primary"
|
type="primary"
|
||||||
size="medium"
|
size="default"
|
||||||
@click="goToLogin"
|
@click="goToLogin"
|
||||||
>
|
>
|
||||||
<t-icon name="user" />
|
<el-icon><User /></el-icon>
|
||||||
登录
|
登录
|
||||||
</t-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</t-header>
|
</el-header>
|
||||||
|
|
||||||
<t-content class="content">
|
<el-main class="content">
|
||||||
<slot />
|
<slot />
|
||||||
</t-content>
|
</el-main>
|
||||||
</t-layout>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { MessagePlugin } from 'tdesign-vue-next'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { User, ArrowDown, Lock, SwitchButton } from '@element-plus/icons-vue'
|
||||||
import { userApi } from '@/api/user'
|
import { userApi } from '@/api/user'
|
||||||
import auth from '@/utils/auth'
|
import auth from '@/utils/auth'
|
||||||
|
|
||||||
|
@ -75,25 +90,6 @@ const activeMenu = ref('home')
|
||||||
const isLoggedIn = computed(() => auth.isLoggedIn())
|
const isLoggedIn = computed(() => auth.isLoggedIn())
|
||||||
const userInfo = computed(() => auth.getUserInfo())
|
const userInfo = computed(() => auth.getUserInfo())
|
||||||
|
|
||||||
// 用户菜单选项
|
|
||||||
const userMenuOptions = [
|
|
||||||
{
|
|
||||||
content: '个人信息',
|
|
||||||
value: 'profile',
|
|
||||||
icon: 'user'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
content: '修改密码',
|
|
||||||
value: 'change-password',
|
|
||||||
icon: 'lock-on'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
content: '退出登录',
|
|
||||||
value: 'logout',
|
|
||||||
icon: 'logout'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// 处理菜单切换
|
// 处理菜单切换
|
||||||
const handleMenuChange = (value) => {
|
const handleMenuChange = (value) => {
|
||||||
activeMenu.value = value
|
activeMenu.value = value
|
||||||
|
@ -113,18 +109,18 @@ const goToLogin = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理用户菜单点击
|
// 处理用户菜单点击
|
||||||
const handleUserMenuClick = async (data) => {
|
const handleUserMenuClick = async (command) => {
|
||||||
switch (data.value) {
|
switch (command) {
|
||||||
case 'logout':
|
case 'logout':
|
||||||
await handleLogout()
|
await handleLogout()
|
||||||
break
|
break
|
||||||
case 'profile':
|
case 'profile':
|
||||||
// TODO: 跳转到个人信息页面
|
// TODO: 跳转到个人信息页面
|
||||||
MessagePlugin.info('功能开发中')
|
ElMessage.info('功能开发中')
|
||||||
break
|
break
|
||||||
case 'change-password':
|
case 'change-password':
|
||||||
// TODO: 跳转到修改密码页面
|
// TODO: 跳转到修改密码页面
|
||||||
MessagePlugin.info('功能开发中')
|
ElMessage.info('功能开发中')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,7 +134,7 @@ const handleLogout = async () => {
|
||||||
} finally {
|
} finally {
|
||||||
// 清除本地认证信息
|
// 清除本地认证信息
|
||||||
auth.logout()
|
auth.logout()
|
||||||
MessagePlugin.success('已退出登录')
|
ElMessage.success('已退出登录')
|
||||||
|
|
||||||
// 跳转到首页
|
// 跳转到首页
|
||||||
router.push('/home')
|
router.push('/home')
|
||||||
|
@ -159,6 +155,7 @@ onMounted(() => {
|
||||||
.header {
|
.header {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-bottom: 1px solid #e7e7e7;
|
border-bottom: 1px solid #e7e7e7;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-container {
|
.header-container {
|
||||||
|
@ -169,16 +166,22 @@ onMounted(() => {
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-menu {
|
||||||
|
flex: 1;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
|
margin-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-text {
|
.logo-text {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #0052d9;
|
color: #409eff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-icon">
|
<div class="empty-icon">
|
||||||
<t-icon :name="icon" size="64px" />
|
<el-icon :size="64" :color="'#d9d9d9'">
|
||||||
|
<component :is="iconComponent" />
|
||||||
|
</el-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="empty-content">
|
<div class="empty-content">
|
||||||
<h3 class="empty-title">{{ title }}</h3>
|
<h3 class="empty-title">{{ title }}</h3>
|
||||||
|
@ -14,10 +16,21 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
defineProps({
|
import { computed } from 'vue'
|
||||||
|
import {
|
||||||
|
Inbox,
|
||||||
|
Document,
|
||||||
|
Folder,
|
||||||
|
User,
|
||||||
|
Setting,
|
||||||
|
Warning,
|
||||||
|
InfoFilled
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'inbox'
|
default: 'Inbox'
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -28,6 +41,21 @@ defineProps({
|
||||||
default: ''
|
default: ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 图标映射
|
||||||
|
const iconMap = {
|
||||||
|
'Inbox': Inbox,
|
||||||
|
'Document': Document,
|
||||||
|
'Folder': Folder,
|
||||||
|
'User': User,
|
||||||
|
'Setting': Setting,
|
||||||
|
'Warning': Warning,
|
||||||
|
'InfoFilled': InfoFilled
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconComponent = computed(() => {
|
||||||
|
return iconMap[props.icon] || Inbox
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="loading-spinner" :class="{ 'full-screen': fullScreen }">
|
<div class="loading-spinner" :class="{ 'full-screen': fullScreen }">
|
||||||
<div class="spinner-content">
|
<div class="spinner-content">
|
||||||
<t-loading :size="size" />
|
<el-loading
|
||||||
<p v-if="text" class="loading-text">{{ text }}</p>
|
:size="size"
|
||||||
|
:text="text"
|
||||||
|
:background="fullScreen ? 'rgba(255, 255, 255, 0.8)' : 'transparent'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -15,7 +18,7 @@ defineProps({
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'medium'
|
default: 'default'
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -38,7 +41,6 @@ defineProps({
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba(255, 255, 255, 0.8);
|
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,10 +50,4 @@ defineProps({
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-text {
|
|
||||||
margin: 0;
|
|
||||||
color: #8c8c8c;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
|
@ -10,12 +10,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="showBreadcrumb" class="page-breadcrumb">
|
<div v-if="showBreadcrumb" class="page-breadcrumb">
|
||||||
<t-breadcrumb>
|
<el-breadcrumb separator="/">
|
||||||
<t-breadcrumb-item v-for="item in breadcrumbItems" :key="item.path">
|
<el-breadcrumb-item v-for="item in breadcrumbItems" :key="item.path">
|
||||||
<router-link v-if="item.path" :to="item.path">{{ item.title }}</router-link>
|
<router-link v-if="item.path" :to="item.path">{{ item.title }}</router-link>
|
||||||
<span v-else>{{ item.title }}</span>
|
<span v-else>{{ item.title }}</span>
|
||||||
</t-breadcrumb-item>
|
</el-breadcrumb-item>
|
||||||
</t-breadcrumb>
|
</el-breadcrumb>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,53 +1,54 @@
|
||||||
<template>
|
<template>
|
||||||
<t-card class="project-card" hover shadow>
|
<el-card class="project-card" shadow="hover">
|
||||||
<div class="project-header">
|
<div class="project-header">
|
||||||
<div class="project-info">
|
<div class="project-info">
|
||||||
<h3 class="project-name">{{ project.name || '未命名项目' }}</h3>
|
<h3 class="project-name">{{ project.name || '未命名项目' }}</h3>
|
||||||
<p class="project-description">{{ project.description || '暂无描述' }}</p>
|
<p class="project-description">{{ project.description || '暂无描述' }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="project-status">
|
<div class="project-status">
|
||||||
<t-tag
|
<el-tag
|
||||||
:theme="getStatusTheme(project.status)"
|
:type="getStatusType(project.status)"
|
||||||
:variant="project.status === 'success' ? 'light' : 'outline'"
|
:effect="project.status === 'success' ? 'light' : 'plain'"
|
||||||
>
|
>
|
||||||
{{ getStatusText(project.status) }}
|
{{ getStatusText(project.status) }}
|
||||||
</t-tag>
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="project-details">
|
<div class="project-details">
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<t-icon name="folder" size="16px" />
|
<el-icon><Folder /></el-icon>
|
||||||
<span>{{ project.path || '/' }}</span>
|
<span>{{ project.path || '/' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item">
|
<div class="detail-item">
|
||||||
<t-icon name="time" size="16px" />
|
<el-icon><Clock /></el-icon>
|
||||||
<span>{{ formatTime(project.updatedAt || project.createdAt) }}</span>
|
<span>{{ formatTime(project.updatedAt || project.createdAt) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-item" v-if="project.version">
|
<div class="detail-item" v-if="project.version">
|
||||||
<t-icon name="tag" size="16px" />
|
<el-icon><PriceTag /></el-icon>
|
||||||
<span>v{{ project.version }}</span>
|
<span>v{{ project.version }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="project-actions">
|
<div class="project-actions">
|
||||||
<t-button size="small" variant="outline" @click="$emit('view', project)">
|
<el-button size="small" @click="$emit('view', project)">
|
||||||
查看详情
|
查看详情
|
||||||
</t-button>
|
</el-button>
|
||||||
<t-button
|
<el-button
|
||||||
size="small"
|
size="small"
|
||||||
theme="primary"
|
type="primary"
|
||||||
@click="$emit('deploy', project)"
|
@click="$emit('deploy', project)"
|
||||||
:loading="deploying"
|
:loading="deploying"
|
||||||
>
|
>
|
||||||
{{ deploying ? '部署中...' : '部署' }}
|
{{ deploying ? '部署中...' : '部署' }}
|
||||||
</t-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</t-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { Folder, Clock, PriceTag } from '@element-plus/icons-vue'
|
||||||
import { formatDistanceToNow } from '@/utils/format'
|
import { formatDistanceToNow } from '@/utils/format'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
@ -61,15 +62,15 @@ const emit = defineEmits(['view', 'deploy'])
|
||||||
|
|
||||||
const deploying = ref(false)
|
const deploying = ref(false)
|
||||||
|
|
||||||
// 获取状态主题色
|
// 获取状态类型
|
||||||
const getStatusTheme = (status) => {
|
const getStatusType = (status) => {
|
||||||
const themes = {
|
const types = {
|
||||||
success: 'success',
|
success: 'success',
|
||||||
running: 'warning',
|
running: 'warning',
|
||||||
failed: 'danger',
|
failed: 'danger',
|
||||||
pending: 'default'
|
pending: 'info'
|
||||||
}
|
}
|
||||||
return themes[status] || 'default'
|
return types[status] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取状态文本
|
// 获取状态文本
|
||||||
|
@ -171,8 +172,4 @@ const formatTime = (time) => {
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.t-card__body) {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
14
src/main.js
14
src/main.js
|
@ -2,13 +2,19 @@ import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
// 引入 TDesign 样式和组件
|
// 引入 Element Plus 样式和组件
|
||||||
import TDesign from 'tdesign-vue-next'
|
import ElementPlus from 'element-plus'
|
||||||
import 'tdesign-vue-next/es/style/index.css'
|
import 'element-plus/dist/index.css'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(TDesign)
|
// 注册所有图标
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.use(ElementPlus)
|
||||||
app.use(router)
|
app.use(router)
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
|
@ -1,63 +1,111 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="deploy">
|
<div class="deploy">
|
||||||
<t-card title="部署管理">
|
<el-card>
|
||||||
<template #actions>
|
<template #header>
|
||||||
<t-button theme="primary" @click="handleCreate">
|
<div class="card-header">
|
||||||
<t-icon name="add" />
|
<span>部署管理</span>
|
||||||
|
<el-button type="primary" @click="handleCreate">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
创建部署
|
创建部署
|
||||||
</t-button>
|
</el-button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<t-table
|
<el-table
|
||||||
:data="deployList"
|
:data="deployList"
|
||||||
:columns="columns"
|
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
row-key="id"
|
style="width: 100%"
|
||||||
:pagination="pagination"
|
>
|
||||||
@page-change="handlePageChange"
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="name" label="部署名称" />
|
||||||
|
<el-table-column prop="environment" label="环境" />
|
||||||
|
<el-table-column prop="status" label="状态">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getStatusType(row.status)">
|
||||||
|
{{ row.status }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="user" label="操作用户" />
|
||||||
|
<el-table-column prop="createTime" label="创建时间" />
|
||||||
|
<el-table-column label="操作" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" size="small" text>查看</el-button>
|
||||||
|
<el-button type="danger" size="small" text>删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.current"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
/>
|
/>
|
||||||
</t-card>
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<!-- 创建部署对话框 -->
|
<!-- 创建部署对话框 -->
|
||||||
<t-dialog
|
<el-dialog
|
||||||
v-model:visible="createVisible"
|
v-model="createVisible"
|
||||||
title="创建部署"
|
title="创建部署"
|
||||||
width="600px"
|
width="600px"
|
||||||
@confirm="handleSubmit"
|
|
||||||
>
|
>
|
||||||
<t-form ref="formRef" :data="formData" :rules="rules" label-width="100px">
|
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||||
<t-form-item label="部署名称" name="name">
|
<el-form-item label="部署名称" prop="name">
|
||||||
<t-input v-model="formData.name" placeholder="请输入部署名称" />
|
<el-input v-model="formData.name" placeholder="请输入部署名称" />
|
||||||
</t-form-item>
|
</el-form-item>
|
||||||
<t-form-item label="部署环境" name="environment">
|
<el-form-item label="部署环境" prop="environment">
|
||||||
<t-select v-model="formData.environment" placeholder="请选择部署环境">
|
<el-select v-model="formData.environment" placeholder="请选择部署环境" style="width: 100%">
|
||||||
<t-option value="dev" label="开发环境" />
|
<el-option value="dev" label="开发环境" />
|
||||||
<t-option value="test" label="测试环境" />
|
<el-option value="test" label="测试环境" />
|
||||||
<t-option value="prod" label="生产环境" />
|
<el-option value="prod" label="生产环境" />
|
||||||
</t-select>
|
</el-select>
|
||||||
</t-form-item>
|
</el-form-item>
|
||||||
<t-form-item label="部署文件" name="file">
|
<el-form-item label="部署文件" prop="file">
|
||||||
<t-upload
|
<el-upload
|
||||||
v-model="formData.file"
|
v-model:file-list="formData.file"
|
||||||
theme="file-input"
|
action="#"
|
||||||
placeholder="请选择部署文件"
|
:auto-upload="false"
|
||||||
/>
|
:limit="1"
|
||||||
</t-form-item>
|
>
|
||||||
<t-form-item label="描述" name="description">
|
<el-button type="primary">选择文件</el-button>
|
||||||
<t-textarea
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
请选择要部署的文件
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="描述" prop="description">
|
||||||
|
<el-input
|
||||||
v-model="formData.description"
|
v-model="formData.description"
|
||||||
|
type="textarea"
|
||||||
placeholder="请输入部署描述"
|
placeholder="请输入部署描述"
|
||||||
:maxlength="200"
|
:maxlength="200"
|
||||||
|
:rows="3"
|
||||||
|
show-word-limit
|
||||||
/>
|
/>
|
||||||
</t-form-item>
|
</el-form-item>
|
||||||
</t-form>
|
</el-form>
|
||||||
</t-dialog>
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="createVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { MessagePlugin } from 'tdesign-vue-next'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Plus } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const createVisible = ref(false)
|
const createVisible = ref(false)
|
||||||
|
@ -82,61 +130,10 @@ const deployList = ref([
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
colKey: 'id',
|
|
||||||
title: 'ID',
|
|
||||||
width: 80
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'name',
|
|
||||||
title: '部署名称'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'environment',
|
|
||||||
title: '环境'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'status',
|
|
||||||
title: '状态',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (!row || !row.status) return '-'
|
|
||||||
|
|
||||||
const statusMap = {
|
|
||||||
'成功': 'success',
|
|
||||||
'失败': 'danger',
|
|
||||||
'进行中': 'warning'
|
|
||||||
}
|
|
||||||
return `<t-tag theme="${statusMap[row.status] || 'default'}">${row.status}</t-tag>`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'user',
|
|
||||||
title: '操作用户'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'createTime',
|
|
||||||
title: '创建时间'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'action',
|
|
||||||
title: '操作',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (!row || !row.id) return '-'
|
|
||||||
return `
|
|
||||||
<t-button theme="primary" variant="text" size="small">查看</t-button>
|
|
||||||
<t-button theme="danger" variant="text" size="small">删除</t-button>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 2,
|
total: 2
|
||||||
showJumper: true,
|
|
||||||
showSizer: true
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
|
@ -147,9 +144,19 @@ const formData = reactive({
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
name: [{ required: true, message: '请输入部署名称' }],
|
name: [{ required: true, message: '请输入部署名称', trigger: 'blur' }],
|
||||||
environment: [{ required: true, message: '请选择部署环境' }],
|
environment: [{ required: true, message: '请选择部署环境', trigger: 'change' }],
|
||||||
file: [{ required: true, message: '请选择部署文件' }]
|
file: [{ required: true, message: '请选择部署文件', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态类型
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const types = {
|
||||||
|
'成功': 'success',
|
||||||
|
'失败': 'danger',
|
||||||
|
'进行中': 'warning'
|
||||||
|
}
|
||||||
|
return types[status] || 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
|
@ -157,9 +164,11 @@ const handleCreate = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const result = await formRef.value.validate()
|
if (!formRef.value) return
|
||||||
if (result === true) {
|
|
||||||
MessagePlugin.success('部署创建成功')
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
ElMessage.success('部署创建成功')
|
||||||
createVisible.value = false
|
createVisible.value = false
|
||||||
// 重置表单
|
// 重置表单
|
||||||
Object.keys(formData).forEach(key => {
|
Object.keys(formData).forEach(key => {
|
||||||
|
@ -169,12 +178,19 @@ const handleSubmit = async () => {
|
||||||
formData[key] = ''
|
formData[key] = ''
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('表单验证失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePageChange = (pageInfo) => {
|
const handleSizeChange = (size) => {
|
||||||
pagination.current = pageInfo.current
|
pagination.pageSize = size
|
||||||
pagination.pageSize = pageInfo.pageSize
|
// 重新加载数据
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (page) => {
|
||||||
|
pagination.current = page
|
||||||
|
// 重新加载数据
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -187,4 +203,22 @@ onMounted(() => {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,61 +1,111 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="files">
|
<div class="files">
|
||||||
<t-card title="文件管理">
|
<el-card>
|
||||||
<template #actions>
|
<template #header>
|
||||||
<t-space>
|
<div class="card-header">
|
||||||
<t-button theme="primary" @click="handleUpload">
|
<span>文件管理</span>
|
||||||
<t-icon name="upload" />
|
<div class="header-actions">
|
||||||
|
<el-button type="primary" @click="handleUpload">
|
||||||
|
<el-icon><Upload /></el-icon>
|
||||||
上传文件
|
上传文件
|
||||||
</t-button>
|
</el-button>
|
||||||
<t-button variant="outline" @click="handleRefresh">
|
<el-button @click="handleRefresh">
|
||||||
<t-icon name="refresh" />
|
<el-icon><Refresh /></el-icon>
|
||||||
刷新
|
刷新
|
||||||
</t-button>
|
</el-button>
|
||||||
</t-space>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<t-table
|
<el-table
|
||||||
:data="fileList"
|
:data="fileList"
|
||||||
:columns="columns"
|
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
row-key="id"
|
style="width: 100%"
|
||||||
:pagination="pagination"
|
>
|
||||||
@page-change="handlePageChange"
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="filename" label="文件名">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="file-cell">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
<span>{{ row.filename }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="size" label="文件大小" />
|
||||||
|
<el-table-column prop="type" label="文件类型">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getFileType(row.type)">
|
||||||
|
{{ row.type }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="uploader" label="上传者" />
|
||||||
|
<el-table-column prop="uploadTime" label="上传时间" />
|
||||||
|
<el-table-column prop="status" label="状态">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag type="success">{{ row.status }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" size="small" text>下载</el-button>
|
||||||
|
<el-button type="danger" size="small" text>删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.current"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
/>
|
/>
|
||||||
</t-card>
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<!-- 上传文件对话框 -->
|
<!-- 上传文件对话框 -->
|
||||||
<t-dialog
|
<el-dialog
|
||||||
v-model:visible="uploadVisible"
|
v-model="uploadVisible"
|
||||||
title="上传文件"
|
title="上传文件"
|
||||||
width="600px"
|
width="600px"
|
||||||
@confirm="handleUploadSubmit"
|
|
||||||
>
|
>
|
||||||
<t-upload
|
<el-upload
|
||||||
v-model="uploadFiles"
|
v-model:file-list="uploadFiles"
|
||||||
theme="file-flow"
|
action="#"
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
multiple
|
multiple
|
||||||
:max="5"
|
:limit="5"
|
||||||
accept=".zip,.tar,.gz,.jar,.war"
|
accept=".zip,.tar,.gz,.jar,.war"
|
||||||
|
drag
|
||||||
>
|
>
|
||||||
<t-upload-dragger>
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
<div class="upload-dragger-content">
|
<div class="el-upload__text">
|
||||||
<t-icon name="cloud-upload" size="48px" />
|
将文件拖到此处,或<em>点击上传</em>
|
||||||
<div class="upload-text">
|
|
||||||
<div class="upload-tip">点击上传或将文件拖拽到此区域</div>
|
|
||||||
<div class="upload-sub-tip">支持 .zip、.tar、.gz、.jar、.war 格式</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
支持 .zip、.tar、.gz、.jar、.war 格式,最多上传5个文件
|
||||||
</div>
|
</div>
|
||||||
</t-upload-dragger>
|
</template>
|
||||||
</t-upload>
|
</el-upload>
|
||||||
</t-dialog>
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="uploadVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleUploadSubmit">确定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { MessagePlugin } from 'tdesign-vue-next'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Upload, Refresh, Document } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const uploadVisible = ref(false)
|
const uploadVisible = ref(false)
|
||||||
|
@ -91,79 +141,22 @@ const fileList = ref([
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
const columns = [
|
const pagination = reactive({
|
||||||
{
|
current: 1,
|
||||||
colKey: 'id',
|
pageSize: 10,
|
||||||
title: 'ID',
|
total: 3
|
||||||
width: 80
|
})
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'filename',
|
|
||||||
title: '文件名',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (!row || !row.filename) return '-'
|
|
||||||
|
|
||||||
return `<div class="file-cell">
|
|
||||||
<t-icon name="file" style="margin-right: 8px;" />
|
|
||||||
${row.filename}
|
|
||||||
</div>`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'size',
|
|
||||||
title: '文件大小'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'type',
|
|
||||||
title: '文件类型',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (!row || !row.type) return '-'
|
|
||||||
|
|
||||||
|
// 获取文件类型标签样式
|
||||||
|
const getFileType = (type) => {
|
||||||
const typeMap = {
|
const typeMap = {
|
||||||
'ZIP': 'primary',
|
'ZIP': 'primary',
|
||||||
'JAR': 'success',
|
'JAR': 'success',
|
||||||
'TAR.GZ': 'warning',
|
'TAR.GZ': 'warning',
|
||||||
'WAR': 'danger'
|
'WAR': 'danger'
|
||||||
}
|
}
|
||||||
return `<t-tag theme="${typeMap[row.type] || 'default'}">${row.type}</t-tag>`
|
return typeMap[type] || 'info'
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'uploader',
|
|
||||||
title: '上传者'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'uploadTime',
|
|
||||||
title: '上传时间'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'status',
|
|
||||||
title: '状态',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (!row || !row.status) return '-'
|
|
||||||
return `<t-tag theme="success">${row.status}</t-tag>`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'action',
|
|
||||||
title: '操作',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (!row || !row.id) return '-'
|
|
||||||
return `
|
|
||||||
<t-button theme="primary" variant="text" size="small">下载</t-button>
|
|
||||||
<t-button theme="danger" variant="text" size="small">删除</t-button>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const pagination = reactive({
|
|
||||||
current: 1,
|
|
||||||
pageSize: 10,
|
|
||||||
total: 3,
|
|
||||||
showJumper: true,
|
|
||||||
showSizer: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const handleUpload = () => {
|
const handleUpload = () => {
|
||||||
uploadVisible.value = true
|
uploadVisible.value = true
|
||||||
|
@ -173,24 +166,29 @@ const handleRefresh = () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
MessagePlugin.success('刷新成功')
|
ElMessage.success('刷新成功')
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUploadSubmit = () => {
|
const handleUploadSubmit = () => {
|
||||||
if (uploadFiles.value.length === 0) {
|
if (uploadFiles.value.length === 0) {
|
||||||
MessagePlugin.warning('请选择要上传的文件')
|
ElMessage.warning('请选择要上传的文件')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
MessagePlugin.success('文件上传成功')
|
ElMessage.success('文件上传成功')
|
||||||
uploadVisible.value = false
|
uploadVisible.value = false
|
||||||
uploadFiles.value = []
|
uploadFiles.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePageChange = (pageInfo) => {
|
const handleSizeChange = (size) => {
|
||||||
pagination.current = pageInfo.current
|
pagination.pageSize = size
|
||||||
pagination.pageSize = pageInfo.pageSize
|
// 重新加载数据
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (page) => {
|
||||||
|
pagination.current = page
|
||||||
|
// 重新加载数据
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -204,28 +202,32 @@ onMounted(() => {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-cell {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-dragger-content {
|
.header-actions {
|
||||||
text-align: center;
|
display: flex;
|
||||||
padding: 40px 20px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-text {
|
.file-cell {
|
||||||
margin-top: 16px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-tip {
|
.pagination-container {
|
||||||
font-size: 16px;
|
margin-top: 20px;
|
||||||
color: #262626;
|
display: flex;
|
||||||
margin-bottom: 8px;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-sub-tip {
|
.dialog-footer {
|
||||||
font-size: 14px;
|
display: flex;
|
||||||
color: #8c8c8c;
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -8,61 +8,65 @@
|
||||||
<p class="page-subtitle">管理和部署您的项目</p>
|
<p class="page-subtitle">管理和部署您的项目</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions" v-if="isLoggedIn">
|
<div class="header-actions" v-if="isLoggedIn">
|
||||||
<t-button theme="primary" @click="goToDeploy">
|
<el-button type="primary" @click="goToDeploy">
|
||||||
<t-icon name="add" />
|
<el-icon><Plus /></el-icon>
|
||||||
新建项目
|
新建项目
|
||||||
</t-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 未登录状态 -->
|
<!-- 未登录状态 -->
|
||||||
<div v-if="!isLoggedIn" class="welcome-section">
|
<div v-if="!isLoggedIn" class="welcome-section">
|
||||||
<t-card class="welcome-card">
|
<el-card class="welcome-card">
|
||||||
<div class="welcome-content">
|
<div class="welcome-content">
|
||||||
<h2 class="welcome-title">欢迎使用 DeployHelper</h2>
|
<h2 class="welcome-title">欢迎使用 DeployHelper</h2>
|
||||||
<p class="welcome-subtitle">一个简单高效的部署管理系统</p>
|
<p class="welcome-subtitle">一个简单高效的部署管理系统</p>
|
||||||
<div class="welcome-features">
|
<div class="welcome-features">
|
||||||
<div class="feature-item">
|
<div class="feature-item">
|
||||||
<t-icon name="rocket" size="24px" color="#0052d9" />
|
<el-icon size="24" color="#409eff"><Promotion /></el-icon>
|
||||||
<span>快速部署</span>
|
<span>快速部署</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-item">
|
<div class="feature-item">
|
||||||
<t-icon name="folder" size="24px" color="#00a870" />
|
<el-icon size="24" color="#67c23a"><Folder /></el-icon>
|
||||||
<span>文件管理</span>
|
<span>文件管理</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-item">
|
<div class="feature-item">
|
||||||
<t-icon name="chart" size="24px" color="#ed7b2f" />
|
<el-icon size="24" color="#e6a23c"><DataAnalysis /></el-icon>
|
||||||
<span>日志监控</span>
|
<span>日志监控</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature-item">
|
<div class="feature-item">
|
||||||
<t-icon name="user" size="24px" color="#d54941" />
|
<el-icon size="24" color="#f56c6c"><User /></el-icon>
|
||||||
<span>用户管理</span>
|
<span>用户管理</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="login-hint">登录后查看您的项目</p>
|
<p class="login-hint">登录后查看您的项目</p>
|
||||||
</div>
|
</div>
|
||||||
</t-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 已登录状态 - 项目列表 -->
|
<!-- 已登录状态 - 项目列表 -->
|
||||||
<div v-else class="projects-section">
|
<div v-else class="projects-section">
|
||||||
<!-- 加载状态 -->
|
<!-- 加载状态 -->
|
||||||
<div v-if="loading" class="loading-container">
|
<div v-if="loading" class="loading-container">
|
||||||
<t-loading size="large" text="加载项目中..." />
|
<el-loading size="large" text="加载项目中..." />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 空状态 -->
|
<!-- 空状态 -->
|
||||||
<div v-else-if="projects.length === 0" class="empty-state">
|
<div v-else-if="projects.length === 0" class="empty-state">
|
||||||
<t-empty
|
<el-empty
|
||||||
icon="rocket"
|
description="还没有项目"
|
||||||
title="还没有项目"
|
|
||||||
description="创建您的第一个项目开始部署之旅"
|
|
||||||
>
|
>
|
||||||
<t-button theme="primary" @click="goToDeploy">
|
<template #image>
|
||||||
|
<el-icon size="64" color="#d9d9d9"><Promotion /></el-icon>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<p>创建您的第一个项目开始部署之旅</p>
|
||||||
|
</template>
|
||||||
|
<el-button type="primary" @click="goToDeploy">
|
||||||
创建项目
|
创建项目
|
||||||
</t-button>
|
</el-button>
|
||||||
</t-empty>
|
</el-empty>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 项目网格 -->
|
<!-- 项目网格 -->
|
||||||
|
@ -82,7 +86,8 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { MessagePlugin } from 'tdesign-vue-next'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Plus, Promotion, Folder, DataAnalysis, User } from '@element-plus/icons-vue'
|
||||||
import ProjectCard from '@/components/ProjectCard.vue'
|
import ProjectCard from '@/components/ProjectCard.vue'
|
||||||
import { deployApi } from '@/api/deploy'
|
import { deployApi } from '@/api/deploy'
|
||||||
import auth from '@/utils/auth'
|
import auth from '@/utils/auth'
|
||||||
|
@ -110,11 +115,11 @@ const fetchProjects = async () => {
|
||||||
if (response.data.code === 0) {
|
if (response.data.code === 0) {
|
||||||
projects.value = response.data.data?.list || []
|
projects.value = response.data.data?.list || []
|
||||||
} else {
|
} else {
|
||||||
MessagePlugin.error(response.data.message || '获取项目列表失败')
|
ElMessage.error(response.data.message || '获取项目列表失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取项目列表错误:', error)
|
console.error('获取项目列表错误:', error)
|
||||||
MessagePlugin.error('获取项目列表失败,请检查网络连接')
|
ElMessage.error('获取项目列表失败,请检查网络连接')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
@ -128,13 +133,13 @@ const handleViewProject = (project) => {
|
||||||
// 部署项目
|
// 部署项目
|
||||||
const handleDeployProject = async (project) => {
|
const handleDeployProject = async (project) => {
|
||||||
try {
|
try {
|
||||||
MessagePlugin.info('部署功能开发中...')
|
ElMessage.info('部署功能开发中...')
|
||||||
// TODO: 实现部署逻辑
|
// TODO: 实现部署逻辑
|
||||||
// await deployApi.executeDeploy(project.id)
|
// await deployApi.executeDeploy(project.id)
|
||||||
// MessagePlugin.success('部署成功')
|
// ElMessage.success('部署成功')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('部署错误:', error)
|
console.error('部署错误:', error)
|
||||||
MessagePlugin.error('部署失败')
|
ElMessage.error('部署失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,7 +212,7 @@ onMounted(() => {
|
||||||
.welcome-title {
|
.welcome-title {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #0052d9;
|
color: #409eff;
|
||||||
margin: 0 0 16px 0;
|
margin: 0 0 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,41 +8,45 @@
|
||||||
|
|
||||||
<div class="login-form">
|
<div class="login-form">
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<t-input
|
<el-input
|
||||||
v-model="formData.account"
|
v-model="formData.account"
|
||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
<template #prefix-icon>
|
<template #prefix>
|
||||||
<t-icon name="user" />
|
<el-icon>
|
||||||
|
<User/>
|
||||||
|
</el-icon>
|
||||||
</template>
|
</template>
|
||||||
</t-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<t-input
|
<el-input
|
||||||
v-model="formData.password"
|
v-model="formData.password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="请输入密码"
|
placeholder="请输入密码"
|
||||||
size="large"
|
size="large"
|
||||||
@keyup.enter="handleSubmit"
|
@keyup.enter="handleSubmit"
|
||||||
>
|
>
|
||||||
<template #prefix-icon>
|
<template #prefix>
|
||||||
<t-icon name="lock-on" />
|
<el-icon>
|
||||||
|
<Lock/>
|
||||||
|
</el-icon>
|
||||||
</template>
|
</template>
|
||||||
</t-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<t-button
|
<el-button
|
||||||
theme="primary"
|
type="primary"
|
||||||
size="large"
|
size="large"
|
||||||
block
|
style="width: 100%"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
>
|
>
|
||||||
登录
|
登录
|
||||||
</t-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -50,11 +54,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive } from 'vue'
|
import {ref, reactive} from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import {useRouter} from 'vue-router'
|
||||||
import { MessagePlugin } from 'tdesign-vue-next'
|
import {ElMessage} from 'element-plus'
|
||||||
|
import {User, Lock} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
import { userApi } from '@/api/user'
|
import {userApi} from '@/api/user'
|
||||||
import auth from '@/utils/auth'
|
import auth from '@/utils/auth'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -69,11 +74,11 @@ const formData = reactive({
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
// 简单的表单验证
|
// 简单的表单验证
|
||||||
if (!formData.account.trim()) {
|
if (!formData.account.trim()) {
|
||||||
MessagePlugin.error('请输入用户名')
|
ElMessage.error('请输入用户名')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!formData.password.trim()) {
|
if (!formData.password.trim()) {
|
||||||
MessagePlugin.error('请输入密码')
|
ElMessage.error('请输入密码')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,22 +88,22 @@ const handleSubmit = async () => {
|
||||||
const response = await userApi.login(formData)
|
const response = await userApi.login(formData)
|
||||||
|
|
||||||
if (response.data.code === 0) {
|
if (response.data.code === 0) {
|
||||||
const { token, userInfo } = response.data.data
|
const {token, userInfo} = response.data.data
|
||||||
|
|
||||||
// 保存认证信息
|
// 保存认证信息
|
||||||
auth.setToken(token)
|
auth.setToken(token)
|
||||||
auth.setUserInfo(userInfo)
|
auth.setUserInfo(userInfo)
|
||||||
|
|
||||||
MessagePlugin.success('登录成功')
|
ElMessage.success('登录成功')
|
||||||
|
|
||||||
// 跳转到首页
|
// 跳转到首页
|
||||||
router.push('/home')
|
router.push('/home')
|
||||||
} else {
|
} else {
|
||||||
MessagePlugin.error(response.data.message || '登录失败')
|
ElMessage.error(response.data.message || '登录失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登录错误:', error)
|
console.error('登录错误:', error)
|
||||||
MessagePlugin.error('登录失败,请检查网络连接')
|
ElMessage.error('登录失败,请检查网络连接')
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
@ -131,7 +136,7 @@ const handleSubmit = async () => {
|
||||||
.login-title {
|
.login-title {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #0052d9;
|
color: #409eff;
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,11 +158,11 @@ const handleSubmit = async () => {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.t-input) {
|
:deep(.el-input) {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.t-button) {
|
:deep(.el-button) {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
|
@ -1,69 +1,110 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="logs">
|
<div class="logs">
|
||||||
<t-card title="日志管理">
|
<el-card>
|
||||||
<template #actions>
|
<template #header>
|
||||||
<t-space>
|
<div class="card-header">
|
||||||
<t-input
|
<span>日志管理</span>
|
||||||
|
<div class="header-actions">
|
||||||
|
<el-input
|
||||||
v-model="searchKeyword"
|
v-model="searchKeyword"
|
||||||
placeholder="搜索日志内容"
|
placeholder="搜索日志内容"
|
||||||
style="width: 200px;"
|
style="width: 200px;"
|
||||||
@enter="handleSearch"
|
@keyup.enter="handleSearch"
|
||||||
>
|
>
|
||||||
<template #suffix-icon>
|
<template #suffix>
|
||||||
<t-icon name="search" />
|
<el-icon><Search /></el-icon>
|
||||||
</template>
|
</template>
|
||||||
</t-input>
|
</el-input>
|
||||||
<t-select
|
<el-select
|
||||||
v-model="logLevel"
|
v-model="logLevel"
|
||||||
placeholder="日志级别"
|
placeholder="日志级别"
|
||||||
style="width: 120px;"
|
style="width: 120px;"
|
||||||
@change="handleFilter"
|
@change="handleFilter"
|
||||||
>
|
>
|
||||||
<t-option value="" label="全部" />
|
<el-option value="" label="全部" />
|
||||||
<t-option value="INFO" label="INFO" />
|
<el-option value="INFO" label="INFO" />
|
||||||
<t-option value="WARN" label="WARN" />
|
<el-option value="WARN" label="WARN" />
|
||||||
<t-option value="ERROR" label="ERROR" />
|
<el-option value="ERROR" label="ERROR" />
|
||||||
<t-option value="DEBUG" label="DEBUG" />
|
<el-option value="DEBUG" label="DEBUG" />
|
||||||
</t-select>
|
</el-select>
|
||||||
<t-button variant="outline" @click="handleRefresh">
|
<el-button @click="handleRefresh">
|
||||||
<t-icon name="refresh" />
|
<el-icon><Refresh /></el-icon>
|
||||||
刷新
|
刷新
|
||||||
</t-button>
|
</el-button>
|
||||||
</t-space>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<t-table
|
<el-table
|
||||||
:data="logList"
|
:data="logList"
|
||||||
:columns="columns"
|
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
row-key="id"
|
style="width: 100%"
|
||||||
:pagination="pagination"
|
>
|
||||||
@page-change="handlePageChange"
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="level" label="级别" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getLevelType(row.level)">
|
||||||
|
{{ row.level }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="module" label="模块" />
|
||||||
|
<el-table-column prop="message" label="消息" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="user" label="用户" />
|
||||||
|
<el-table-column prop="timestamp" label="时间" />
|
||||||
|
<el-table-column label="操作" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" size="small" text @click="showDetail(row)">
|
||||||
|
查看详情
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.current"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
/>
|
/>
|
||||||
</t-card>
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<!-- 日志详情对话框 -->
|
<!-- 日志详情对话框 -->
|
||||||
<t-dialog
|
<el-dialog
|
||||||
v-model:visible="detailVisible"
|
v-model="detailVisible"
|
||||||
title="日志详情"
|
title="日志详情"
|
||||||
width="800px"
|
width="800px"
|
||||||
:footer="false"
|
|
||||||
>
|
>
|
||||||
<div class="log-detail" v-if="currentLog">
|
<div class="log-detail" v-if="currentLog">
|
||||||
<t-descriptions :data="logDetailData" />
|
<el-descriptions :column="2" border>
|
||||||
<t-divider />
|
<el-descriptions-item label="日志ID">{{ currentLog.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="级别">
|
||||||
|
<el-tag :type="getLevelType(currentLog.level)">{{ currentLog.level }}</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="模块">{{ currentLog.module }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户">{{ currentLog.user }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="时间" :span="2">{{ currentLog.timestamp }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="消息" :span="2">{{ currentLog.message }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<el-divider />
|
||||||
<div class="log-content">
|
<div class="log-content">
|
||||||
<h4>日志内容</h4>
|
<h4>日志内容</h4>
|
||||||
<pre class="log-text">{{ currentLog.content }}</pre>
|
<pre class="log-text">{{ currentLog.content }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</t-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { MessagePlugin } from 'tdesign-vue-next'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Search, Refresh } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const detailVisible = ref(false)
|
const detailVisible = ref(false)
|
||||||
|
@ -110,90 +151,35 @@ const logList = ref([
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
colKey: 'id',
|
|
||||||
title: 'ID',
|
|
||||||
width: 80
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'level',
|
|
||||||
title: '级别',
|
|
||||||
width: 100,
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (!row || !row.level) return '-'
|
|
||||||
|
|
||||||
const levelMap = {
|
|
||||||
'INFO': { theme: 'primary', color: '#0052d9' },
|
|
||||||
'WARN': { theme: 'warning', color: '#ed7b2f' },
|
|
||||||
'ERROR': { theme: 'danger', color: '#e34d59' },
|
|
||||||
'DEBUG': { theme: 'default', color: '#8c8c8c' }
|
|
||||||
}
|
|
||||||
const config = levelMap[row.level] || levelMap['DEBUG']
|
|
||||||
return `<t-tag theme="${config.theme}">${row.level}</t-tag>`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'module',
|
|
||||||
title: '模块'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'message',
|
|
||||||
title: '消息',
|
|
||||||
ellipsis: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'user',
|
|
||||||
title: '用户'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'timestamp',
|
|
||||||
title: '时间'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'action',
|
|
||||||
title: '操作',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (!row || !row.id) return '-'
|
|
||||||
return `<t-button theme="primary" variant="text" size="small" onclick="showDetail(${row.id})">查看详情</t-button>`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 4,
|
total: 4
|
||||||
showJumper: true,
|
|
||||||
showSizer: true
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const logDetailData = computed(() => {
|
// 获取日志级别类型
|
||||||
if (!currentLog.value) return []
|
const getLevelType = (level) => {
|
||||||
|
const levelMap = {
|
||||||
|
'INFO': 'primary',
|
||||||
|
'WARN': 'warning',
|
||||||
|
'ERROR': 'danger',
|
||||||
|
'DEBUG': 'info'
|
||||||
|
}
|
||||||
|
return levelMap[level] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
const showDetail = (log) => {
|
||||||
{ label: '日志ID', value: currentLog.value.id },
|
currentLog.value = log
|
||||||
{ label: '级别', value: currentLog.value.level },
|
|
||||||
{ label: '模块', value: currentLog.value.module },
|
|
||||||
{ label: '用户', value: currentLog.value.user },
|
|
||||||
{ label: '时间', value: currentLog.value.timestamp },
|
|
||||||
{ label: '消息', value: currentLog.value.message }
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
// 全局函数,供表格调用
|
|
||||||
window.showDetail = (id) => {
|
|
||||||
currentLog.value = logList.value.find(log => log.id === id)
|
|
||||||
detailVisible.value = true
|
detailVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
MessagePlugin.info(`搜索关键词:${searchKeyword.value}`)
|
ElMessage.info(`搜索关键词:${searchKeyword.value}`)
|
||||||
// 实际项目中这里会调用 API 进行搜索
|
// 实际项目中这里会调用 API 进行搜索
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFilter = () => {
|
const handleFilter = () => {
|
||||||
MessagePlugin.info(`筛选日志级别:${logLevel.value || '全部'}`)
|
ElMessage.info(`筛选日志级别:${logLevel.value || '全部'}`)
|
||||||
// 实际项目中这里会调用 API 进行筛选
|
// 实际项目中这里会调用 API 进行筛选
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,13 +187,18 @@ const handleRefresh = () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
MessagePlugin.success('刷新成功')
|
ElMessage.success('刷新成功')
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePageChange = (pageInfo) => {
|
const handleSizeChange = (size) => {
|
||||||
pagination.current = pageInfo.current
|
pagination.pageSize = size
|
||||||
pagination.pageSize = pageInfo.pageSize
|
// 重新加载数据
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (page) => {
|
||||||
|
pagination.current = page
|
||||||
|
// 重新加载数据
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -221,6 +212,24 @@ onMounted(() => {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
.log-detail {
|
.log-detail {
|
||||||
max-height: 600px;
|
max-height: 600px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
|
@ -1,107 +1,163 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="users">
|
<div class="users">
|
||||||
<t-card title="用户管理">
|
<el-card>
|
||||||
<template #actions>
|
<template #header>
|
||||||
<t-space>
|
<div class="card-header">
|
||||||
<t-button theme="primary" @click="handleCreate">
|
<span>用户管理</span>
|
||||||
<t-icon name="user-add" />
|
<div class="header-actions">
|
||||||
|
<el-button type="primary" @click="handleCreate">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
添加用户
|
添加用户
|
||||||
</t-button>
|
</el-button>
|
||||||
<t-button variant="outline" @click="handleRefresh">
|
<el-button @click="handleRefresh">
|
||||||
<t-icon name="refresh" />
|
<el-icon><Refresh /></el-icon>
|
||||||
刷新
|
刷新
|
||||||
</t-button>
|
</el-button>
|
||||||
</t-space>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<t-table
|
<el-table
|
||||||
:data="userList"
|
:data="userList"
|
||||||
:columns="columns"
|
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
row-key="id"
|
style="width: 100%"
|
||||||
:pagination="pagination"
|
>
|
||||||
@page-change="handlePageChange"
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="username" label="用户名" />
|
||||||
|
<el-table-column prop="realName" label="真实姓名" />
|
||||||
|
<el-table-column prop="email" label="邮箱" />
|
||||||
|
<el-table-column prop="phone" label="手机号" />
|
||||||
|
<el-table-column prop="role" label="角色">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="getRoleType(row.role)">
|
||||||
|
{{ getRoleLabel(row.role) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="status" label="状态">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 'active' ? 'success' : 'info'">
|
||||||
|
{{ row.status === 'active' ? '启用' : '禁用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="lastLoginTime" label="最后登录" />
|
||||||
|
<el-table-column label="操作" width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" size="small" text @click="editUser(row)">
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button type="warning" size="small" text @click="resetUserPassword(row)">
|
||||||
|
重置密码
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" size="small" text @click="deleteUser(row)">
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.current"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:total="pagination.total"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
/>
|
/>
|
||||||
</t-card>
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
<!-- 添加/编辑用户对话框 -->
|
<!-- 添加/编辑用户对话框 -->
|
||||||
<t-dialog
|
<el-dialog
|
||||||
v-model:visible="formVisible"
|
v-model="formVisible"
|
||||||
:title="isEdit ? '编辑用户' : '添加用户'"
|
:title="isEdit ? '编辑用户' : '添加用户'"
|
||||||
width="600px"
|
width="600px"
|
||||||
@confirm="handleSubmit"
|
|
||||||
@cancel="handleCancel"
|
|
||||||
>
|
>
|
||||||
<t-form ref="formRef" :data="formData" :rules="rules" label-width="100px">
|
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||||
<t-form-item label="用户名" name="username">
|
<el-form-item label="用户名" prop="username">
|
||||||
<t-input
|
<el-input
|
||||||
v-model="formData.username"
|
v-model="formData.username"
|
||||||
placeholder="请输入用户名"
|
placeholder="请输入用户名"
|
||||||
:disabled="isEdit"
|
:disabled="isEdit"
|
||||||
/>
|
/>
|
||||||
</t-form-item>
|
</el-form-item>
|
||||||
<t-form-item label="真实姓名" name="realName">
|
<el-form-item label="真实姓名" prop="realName">
|
||||||
<t-input v-model="formData.realName" placeholder="请输入真实姓名" />
|
<el-input v-model="formData.realName" placeholder="请输入真实姓名" />
|
||||||
</t-form-item>
|
</el-form-item>
|
||||||
<t-form-item label="邮箱" name="email">
|
<el-form-item label="邮箱" prop="email">
|
||||||
<t-input v-model="formData.email" placeholder="请输入邮箱地址" />
|
<el-input v-model="formData.email" placeholder="请输入邮箱地址" />
|
||||||
</t-form-item>
|
</el-form-item>
|
||||||
<t-form-item label="手机号" name="phone">
|
<el-form-item label="手机号" prop="phone">
|
||||||
<t-input v-model="formData.phone" placeholder="请输入手机号" />
|
<el-input v-model="formData.phone" placeholder="请输入手机号" />
|
||||||
</t-form-item>
|
</el-form-item>
|
||||||
<t-form-item label="角色" name="role">
|
<el-form-item label="角色" prop="role">
|
||||||
<t-select v-model="formData.role" placeholder="请选择用户角色">
|
<el-select v-model="formData.role" placeholder="请选择用户角色" style="width: 100%">
|
||||||
<t-option value="admin" label="管理员" />
|
<el-option value="admin" label="管理员" />
|
||||||
<t-option value="user" label="普通用户" />
|
<el-option value="user" label="普通用户" />
|
||||||
<t-option value="viewer" label="只读用户" />
|
<el-option value="viewer" label="只读用户" />
|
||||||
</t-select>
|
</el-select>
|
||||||
</t-form-item>
|
</el-form-item>
|
||||||
<t-form-item label="状态" name="status">
|
<el-form-item label="状态" prop="status">
|
||||||
<t-radio-group v-model="formData.status">
|
<el-radio-group v-model="formData.status">
|
||||||
<t-radio value="active">启用</t-radio>
|
<el-radio value="active">启用</el-radio>
|
||||||
<t-radio value="inactive">禁用</t-radio>
|
<el-radio value="inactive">禁用</el-radio>
|
||||||
</t-radio-group>
|
</el-radio-group>
|
||||||
</t-form-item>
|
</el-form-item>
|
||||||
<t-form-item v-if="!isEdit" label="密码" name="password">
|
<el-form-item v-if="!isEdit" label="密码" prop="password">
|
||||||
<t-input
|
<el-input
|
||||||
v-model="formData.password"
|
v-model="formData.password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="请输入初始密码"
|
placeholder="请输入初始密码"
|
||||||
/>
|
/>
|
||||||
</t-form-item>
|
</el-form-item>
|
||||||
</t-form>
|
</el-form>
|
||||||
</t-dialog>
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="formVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 重置密码对话框 -->
|
<!-- 重置密码对话框 -->
|
||||||
<t-dialog
|
<el-dialog
|
||||||
v-model:visible="resetPasswordVisible"
|
v-model="resetPasswordVisible"
|
||||||
title="重置密码"
|
title="重置密码"
|
||||||
width="400px"
|
width="400px"
|
||||||
@confirm="handleResetPassword"
|
|
||||||
>
|
>
|
||||||
<t-form ref="passwordFormRef" :data="passwordData" :rules="passwordRules" label-width="100px">
|
<el-form ref="passwordFormRef" :model="passwordData" :rules="passwordRules" label-width="100px">
|
||||||
<t-form-item label="新密码" name="newPassword">
|
<el-form-item label="新密码" prop="newPassword">
|
||||||
<t-input
|
<el-input
|
||||||
v-model="passwordData.newPassword"
|
v-model="passwordData.newPassword"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="请输入新密码"
|
placeholder="请输入新密码"
|
||||||
/>
|
/>
|
||||||
</t-form-item>
|
</el-form-item>
|
||||||
<t-form-item label="确认密码" name="confirmPassword">
|
<el-form-item label="确认密码" prop="confirmPassword">
|
||||||
<t-input
|
<el-input
|
||||||
v-model="passwordData.confirmPassword"
|
v-model="passwordData.confirmPassword"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="请再次输入新密码"
|
placeholder="请再次输入新密码"
|
||||||
/>
|
/>
|
||||||
</t-form-item>
|
</el-form-item>
|
||||||
</t-form>
|
</el-form>
|
||||||
</t-dialog>
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="resetPasswordVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleResetPassword">确定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { User, Refresh } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const formVisible = ref(false)
|
const formVisible = ref(false)
|
||||||
|
@ -147,79 +203,10 @@ const userList = ref([
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
colKey: 'id',
|
|
||||||
title: 'ID',
|
|
||||||
width: 80
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'username',
|
|
||||||
title: '用户名'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'realName',
|
|
||||||
title: '真实姓名'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'email',
|
|
||||||
title: '邮箱'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'phone',
|
|
||||||
title: '手机号'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'role',
|
|
||||||
title: '角色',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (!row || !row.role) return '-'
|
|
||||||
|
|
||||||
const roleMap = {
|
|
||||||
'admin': { label: '管理员', theme: 'danger' },
|
|
||||||
'user': { label: '普通用户', theme: 'primary' },
|
|
||||||
'viewer': { label: '只读用户', theme: 'default' }
|
|
||||||
}
|
|
||||||
const config = roleMap[row.role] || roleMap['viewer']
|
|
||||||
return `<t-tag theme="${config.theme}">${config.label}</t-tag>`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'status',
|
|
||||||
title: '状态',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (!row || !row.status) return '-'
|
|
||||||
|
|
||||||
return row.status === 'active'
|
|
||||||
? '<t-tag theme="success">启用</t-tag>'
|
|
||||||
: '<t-tag theme="default">禁用</t-tag>'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'lastLoginTime',
|
|
||||||
title: '最后登录'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
colKey: 'action',
|
|
||||||
title: '操作',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
if (!row || !row.id) return '-'
|
|
||||||
|
|
||||||
return `
|
|
||||||
<t-button theme="primary" variant="text" size="small" onclick="editUser(${row.id})">编辑</t-button>
|
|
||||||
<t-button theme="warning" variant="text" size="small" onclick="resetUserPassword(${row.id})">重置密码</t-button>
|
|
||||||
<t-button theme="danger" variant="text" size="small" onclick="deleteUser(${row.id})">删除</t-button>
|
|
||||||
`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 3,
|
total: 3
|
||||||
showJumper: true,
|
|
||||||
showSizer: true
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
|
@ -238,78 +225,104 @@ const passwordData = reactive({
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
username: [{ required: true, message: '请输入用户名' }],
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
realName: [{ required: true, message: '请输入真实姓名' }],
|
realName: [{ required: true, message: '请输入真实姓名', trigger: 'blur' }],
|
||||||
email: [
|
email: [
|
||||||
{ required: true, message: '请输入邮箱地址' },
|
{ required: true, message: '请输入邮箱地址', trigger: 'blur' },
|
||||||
{ email: true, message: '请输入正确的邮箱格式' }
|
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
phone: [
|
phone: [
|
||||||
{ required: true, message: '请输入手机号' },
|
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式' }
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号格式', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
role: [{ required: true, message: '请选择用户角色' }],
|
role: [{ required: true, message: '请选择用户角色', trigger: 'change' }],
|
||||||
password: [{ required: true, message: '请输入初始密码' }]
|
password: [{ required: true, message: '请输入初始密码', trigger: 'blur' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordRules = {
|
const passwordRules = {
|
||||||
newPassword: [
|
newPassword: [
|
||||||
{ required: true, message: '请输入新密码' },
|
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||||
{ min: 6, message: '密码长度不能少于6位' }
|
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' }
|
||||||
],
|
],
|
||||||
confirmPassword: [
|
confirmPassword: [
|
||||||
{ required: true, message: '请再次输入新密码' },
|
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
|
||||||
{
|
{
|
||||||
validator: (val) => val === passwordData.newPassword,
|
validator: (rule, value, callback) => {
|
||||||
message: '两次输入的密码不一致'
|
if (value !== passwordData.newPassword) {
|
||||||
|
callback(new Error('两次输入的密码不一致'))
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局函数,供表格调用
|
// 获取角色类型
|
||||||
window.editUser = (id) => {
|
const getRoleType = (role) => {
|
||||||
currentUser.value = userList.value.find(user => user.id === id)
|
const roleMap = {
|
||||||
if (currentUser.value) {
|
'admin': 'danger',
|
||||||
|
'user': 'primary',
|
||||||
|
'viewer': 'info'
|
||||||
|
}
|
||||||
|
return roleMap[role] || 'info'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取角色标签
|
||||||
|
const getRoleLabel = (role) => {
|
||||||
|
const roleMap = {
|
||||||
|
'admin': '管理员',
|
||||||
|
'user': '普通用户',
|
||||||
|
'viewer': '只读用户'
|
||||||
|
}
|
||||||
|
return roleMap[role] || '未知'
|
||||||
|
}
|
||||||
|
|
||||||
|
const editUser = (user) => {
|
||||||
|
currentUser.value = user
|
||||||
Object.keys(formData).forEach(key => {
|
Object.keys(formData).forEach(key => {
|
||||||
if (key !== 'password' && currentUser.value[key] !== undefined) {
|
if (key !== 'password' && user[key] !== undefined) {
|
||||||
formData[key] = currentUser.value[key]
|
formData[key] = user[key]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
isEdit.value = true
|
isEdit.value = true
|
||||||
formVisible.value = true
|
formVisible.value = true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.resetUserPassword = (id) => {
|
const resetUserPassword = (user) => {
|
||||||
currentUser.value = userList.value.find(user => user.id === id)
|
currentUser.value = user
|
||||||
if (currentUser.value) {
|
|
||||||
passwordData.newPassword = ''
|
passwordData.newPassword = ''
|
||||||
passwordData.confirmPassword = ''
|
passwordData.confirmPassword = ''
|
||||||
resetPasswordVisible.value = true
|
resetPasswordVisible.value = true
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.deleteUser = (id) => {
|
const deleteUser = async (user) => {
|
||||||
const user = userList.value.find(u => u.id === id)
|
|
||||||
if (!user) return
|
|
||||||
|
|
||||||
if (user.username === 'admin') {
|
if (user.username === 'admin') {
|
||||||
MessagePlugin.warning('不能删除管理员账户')
|
ElMessage.warning('不能删除管理员账户')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
DialogPlugin.confirm({
|
try {
|
||||||
header: '确认删除',
|
await ElMessageBox.confirm(
|
||||||
body: `确定要删除用户 "${user.realName}" 吗?此操作不可恢复。`,
|
`确定要删除用户 "${user.realName}" 吗?此操作不可恢复。`,
|
||||||
onConfirm: () => {
|
'确认删除',
|
||||||
const index = userList.value.findIndex(u => u.id === id)
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const index = userList.value.findIndex(u => u.id === user.id)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
userList.value.splice(index, 1)
|
userList.value.splice(index, 1)
|
||||||
pagination.total--
|
pagination.total--
|
||||||
MessagePlugin.success('用户删除成功')
|
ElMessage.success('用户删除成功')
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 用户取消删除
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
|
@ -324,19 +337,22 @@ const handleRefresh = () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
MessagePlugin.success('刷新成功')
|
ElMessage.success('刷新成功')
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
const result = await formRef.value.validate()
|
if (!formRef.value) return
|
||||||
if (result === true) {
|
|
||||||
|
try {
|
||||||
|
await formRef.value.validate()
|
||||||
|
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
// 更新用户
|
// 更新用户
|
||||||
const index = userList.value.findIndex(u => u.id === currentUser.value.id)
|
const index = userList.value.findIndex(u => u.id === currentUser.value.id)
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
Object.assign(userList.value[index], formData)
|
Object.assign(userList.value[index], formData)
|
||||||
MessagePlugin.success('用户更新成功')
|
ElMessage.success('用户更新成功')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 添加用户
|
// 添加用户
|
||||||
|
@ -348,27 +364,34 @@ const handleSubmit = async () => {
|
||||||
}
|
}
|
||||||
userList.value.push(newUser)
|
userList.value.push(newUser)
|
||||||
pagination.total++
|
pagination.total++
|
||||||
MessagePlugin.success('用户添加成功')
|
ElMessage.success('用户添加成功')
|
||||||
}
|
}
|
||||||
formVisible.value = false
|
formVisible.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('表单验证失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
formVisible.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleResetPassword = async () => {
|
const handleResetPassword = async () => {
|
||||||
const result = await passwordFormRef.value.validate()
|
if (!passwordFormRef.value) return
|
||||||
if (result === true) {
|
|
||||||
MessagePlugin.success(`用户 "${currentUser.value.realName}" 密码重置成功`)
|
try {
|
||||||
|
await passwordFormRef.value.validate()
|
||||||
|
ElMessage.success(`用户 "${currentUser.value.realName}" 密码重置成功`)
|
||||||
resetPasswordVisible.value = false
|
resetPasswordVisible.value = false
|
||||||
|
} catch (error) {
|
||||||
|
console.error('密码表单验证失败:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePageChange = (pageInfo) => {
|
const handleSizeChange = (size) => {
|
||||||
pagination.current = pageInfo.current
|
pagination.pageSize = size
|
||||||
pagination.pageSize = pageInfo.pageSize
|
// 重新加载数据
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (page) => {
|
||||||
|
pagination.current = page
|
||||||
|
// 重新加载数据
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -381,4 +404,27 @@ onMounted(() => {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
margin-top: 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
Loading…
Reference in New Issue