更新代码
This commit is contained in:
parent
8b8b0c54a1
commit
65b281fe35
420
package-lock.json
generated
420
package-lock.json
generated
@ -1,420 +0,0 @@
|
||||
{
|
||||
"name": "yunzer_go",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"docx-preview": "^0.3.7",
|
||||
"vue3-pdf-app": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz",
|
||||
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.28.5"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz",
|
||||
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
|
||||
"integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@vue/shared": "3.5.22",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz",
|
||||
"integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
|
||||
"integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.4",
|
||||
"@vue/compiler-core": "3.5.22",
|
||||
"@vue/compiler-dom": "3.5.22",
|
||||
"@vue/compiler-ssr": "3.5.22",
|
||||
"@vue/shared": "3.5.22",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.19",
|
||||
"postcss": "^8.5.6",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz",
|
||||
"integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.22.tgz",
|
||||
"integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
|
||||
"integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
|
||||
"integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.22",
|
||||
"@vue/runtime-core": "3.5.22",
|
||||
"@vue/shared": "3.5.22",
|
||||
"csstype": "^3.1.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
|
||||
"integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.22"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.22.tgz",
|
||||
"integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/docx-preview": {
|
||||
"version": "0.3.7",
|
||||
"resolved": "https://registry.npmmirror.com/docx-preview/-/docx-preview-0.3.7.tgz",
|
||||
"integrity": "sha512-Lav69CTA/IYZPJTsKH7oYeoZjyg96N0wEJMNslGJnZJ+dMUZK85Lt5ASC79yUlD48ecWjuv+rkcmFt6EVPV0Xg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"jszip": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
||||
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.22",
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.22.tgz",
|
||||
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.22",
|
||||
"@vue/compiler-sfc": "3.5.22",
|
||||
"@vue/runtime-dom": "3.5.22",
|
||||
"@vue/server-renderer": "3.5.22",
|
||||
"@vue/shared": "3.5.22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue3-pdf-app": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/vue3-pdf-app/-/vue3-pdf-app-1.0.3.tgz",
|
||||
"integrity": "sha512-qegWTIF4wYKiocZ3KreB70wRXhqSdXWbdERDyyKzT7d5PbjKbS9tD6vaKkCqh3PzTM84NyKPYrQ3iuwJb60YPQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"docx-preview": "^0.3.7",
|
||||
"vue3-pdf-app": "^1.0.3"
|
||||
}
|
||||
}
|
||||
548
pc/docs/dictionary-usage.md
Normal file
548
pc/docs/dictionary-usage.md
Normal file
@ -0,0 +1,548 @@
|
||||
# 字典系统使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
字典(Dictionary)系统是一个用于管理应用中常用的枚举值和标签化数据的功能。通过字典系统,你可以在后台统一管理这些数据,而无需修改代码和重新编译部署。
|
||||
|
||||
### 字典的核心概念
|
||||
|
||||
- **字典类型(Dict Type)**:用来分类管理字典项,例如 `user_status`(用户状态)、`gender`(性别)等
|
||||
- **字典编码(Dict Code)**:字典类型的唯一标识,用于前端查询,例如 `user_status`、`gender`
|
||||
- **字典项(Dict Item)**:具体的字典值和标签,包括:
|
||||
- `dict_value`:存储在数据库中的实际值(例如 `1`、`0`)
|
||||
- `dict_label`:显示给用户的标签文本(例如 "启用"、"禁用")
|
||||
|
||||
### 数据库表结构
|
||||
|
||||
```sql
|
||||
-- 字典类型表
|
||||
CREATE TABLE sys_dict_type (
|
||||
id BIGINT,
|
||||
dict_code VARCHAR(50) -- 字典编码(唯一)
|
||||
dict_name VARCHAR(100) -- 字典名称
|
||||
status TINYINT -- 是否启用
|
||||
...
|
||||
)
|
||||
|
||||
-- 字典项表
|
||||
CREATE TABLE sys_dict_item (
|
||||
id BIGINT,
|
||||
dict_type_id BIGINT -- 关联的字典类型ID
|
||||
dict_label VARCHAR(100) -- 显示标签(如 "启用")
|
||||
dict_value VARCHAR(100) -- 存储值(如 "1")
|
||||
status TINYINT -- 是否启用
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 后端 API 接口
|
||||
|
||||
### 1. 获取字典项(最常用)
|
||||
|
||||
**根据字典编码获取字典项列表:**
|
||||
|
||||
```http
|
||||
GET /api/dict/items/code/:code?include_disabled=0
|
||||
```
|
||||
|
||||
**请求示例:**
|
||||
```bash
|
||||
curl -X GET "http://localhost:8080/api/dict/items/code/user_status?include_disabled=0"
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"dict_type_id": 101,
|
||||
"dict_label": "启用",
|
||||
"dict_value": "1",
|
||||
"status": 1,
|
||||
"sort": 1,
|
||||
...
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"dict_type_id": 101,
|
||||
"dict_label": "禁用",
|
||||
"dict_value": "0",
|
||||
"status": 1,
|
||||
"sort": 2,
|
||||
...
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
- `code` (string, 必需):字典编码,例如 `user_status`
|
||||
- `include_disabled` (int, 可选):是否包含禁用项,0 = 不包含(默认),1 = 包含
|
||||
|
||||
---
|
||||
|
||||
### 2. 字典管理接口
|
||||
|
||||
#### 获取字典类型列表
|
||||
```http
|
||||
GET /api/dict/types?parentId=&status=
|
||||
```
|
||||
|
||||
#### 添加字典类型
|
||||
```http
|
||||
POST /api/dict/types
|
||||
{
|
||||
"dict_code": "user_status",
|
||||
"dict_name": "用户状态",
|
||||
"status": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### 添加字典项
|
||||
```http
|
||||
POST /api/dict/items
|
||||
{
|
||||
"dict_type_id": 101,
|
||||
"dict_label": "启用",
|
||||
"dict_value": "1",
|
||||
"status": 1,
|
||||
"sort": 1
|
||||
}
|
||||
```
|
||||
|
||||
#### 更新/删除字典项
|
||||
```http
|
||||
PUT /api/dict/items/:id
|
||||
DELETE /api/dict/items/:id
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 前端 API(Vue/JavaScript)
|
||||
|
||||
### 前端 API 文件位置
|
||||
|
||||
```
|
||||
pc/src/api/dict.js
|
||||
```
|
||||
|
||||
### 可用函数
|
||||
|
||||
#### getDictItemsByCode(code, includeDisabled = false)
|
||||
|
||||
**最常用的函数**,根据字典编码获取字典项列表。
|
||||
|
||||
**参数:**
|
||||
- `code` (string):字典编码,例如 `'user_status'`
|
||||
- `includeDisabled` (boolean):是否包含禁用项,默认 `false`
|
||||
|
||||
**返回:** Promise,返回字典项数组
|
||||
|
||||
**示例:**
|
||||
```javascript
|
||||
import { getDictItemsByCode } from '@/api/dict'
|
||||
|
||||
// 获取用户状态字典
|
||||
const statusItems = await getDictItemsByCode('user_status')
|
||||
console.log(statusItems)
|
||||
// 输出:
|
||||
// [
|
||||
// { dict_label: '启用', dict_value: '1', ... },
|
||||
// { dict_label: '禁用', dict_value: '0', ... }
|
||||
// ]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 其他可用函数
|
||||
|
||||
```javascript
|
||||
// 获取字典类型列表
|
||||
getDictTypes(params)
|
||||
|
||||
// 根据ID获取字典类型
|
||||
getDictTypeById(id)
|
||||
|
||||
// 添加字典类型
|
||||
addDictType(data)
|
||||
|
||||
// 更新字典类型
|
||||
updateDictType(id, data)
|
||||
|
||||
// 删除字典类型
|
||||
deleteDictType(id)
|
||||
|
||||
// 获取字典项列表(需传入参数过滤)
|
||||
getDictItems(params)
|
||||
|
||||
// 根据ID获取字典项
|
||||
getDictItemById(id)
|
||||
|
||||
// 添加字典项
|
||||
addDictItem(data)
|
||||
|
||||
// 更新字典项
|
||||
updateDictItem(id, data)
|
||||
|
||||
// 删除字典项
|
||||
deleteDictItem(id)
|
||||
|
||||
// 批量更新字典项排序
|
||||
batchUpdateDictItemSort(data)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 前端组件中的使用示例
|
||||
|
||||
### 示例 1:在用户管理页面显示用户状态
|
||||
|
||||
**场景**:在用户列表中,根据用户的 `status` 字段显示对应的状态标签。
|
||||
|
||||
**文件**:`pc/src/views/system/users/index.vue`
|
||||
|
||||
**实现步骤**:
|
||||
|
||||
#### 1. 导入字典 API
|
||||
```javascript
|
||||
import { getDictItemsByCode } from '@/api/dict'
|
||||
```
|
||||
|
||||
#### 2. 定义状态字典数据和加载函数
|
||||
```javascript
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
// 状态字典
|
||||
const statusDict = ref<any[]>([])
|
||||
|
||||
// 加载字典数据
|
||||
const fetchStatusDict = async () => {
|
||||
try {
|
||||
const res = await getDictItemsByCode('user_status')
|
||||
let items: any[] = []
|
||||
|
||||
// 兼容不同的返回结构
|
||||
if (res?.data && Array.isArray(res.data)) {
|
||||
items = res.data
|
||||
} else if (Array.isArray(res)) {
|
||||
items = res
|
||||
}
|
||||
|
||||
statusDict.value = items
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch status dict:', err)
|
||||
statusDict.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 在组件挂载时加载字典
|
||||
onMounted(async () => {
|
||||
await fetchStatusDict()
|
||||
// ... 其他初始化逻辑
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 3. 定义辅助函数
|
||||
```javascript
|
||||
// 根据状态值获取字典标签
|
||||
const getStatusLabel = (status: any) => {
|
||||
const sval = status !== undefined && status !== null ? String(status) : ''
|
||||
|
||||
// 查找匹配的字典项
|
||||
const item = statusDict.value.find(
|
||||
(d: any) => String(d.dict_value) === sval || d.dict_value === status
|
||||
)
|
||||
|
||||
if (item && item.dict_label) {
|
||||
return item.dict_label
|
||||
}
|
||||
|
||||
// 兼容旧逻辑:如果没有匹配的字典项
|
||||
if (status === 1 || sval === '1' || sval === 'active') return '启用'
|
||||
return '禁用'
|
||||
}
|
||||
|
||||
// 根据标签确定 el-tag 的样式类型
|
||||
const getStatusTagType = (status: any) => {
|
||||
const label = getStatusLabel(status)
|
||||
if (!label) return 'info'
|
||||
|
||||
const l = label.toString()
|
||||
if (l.includes('启用') || l.includes('正常') || l.includes('active')) return 'success'
|
||||
if (l.includes('禁用') || l.includes('停用') || l.includes('inactive')) return 'danger'
|
||||
|
||||
return 'info'
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. 在模板中使用
|
||||
```vue
|
||||
<template>
|
||||
<el-table :data="users">
|
||||
<!-- 其他列 -->
|
||||
|
||||
<!-- 状态列 -->
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusTagType(scope.row.status)">
|
||||
{{ getStatusLabel(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 示例 2:在下拉选择中使用字典
|
||||
|
||||
**场景**:在"添加/编辑用户"对话框中,用字典项填充状态下拉选择。
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-dialog title="编辑用户">
|
||||
<el-form :model="form">
|
||||
<!-- 其他字段 -->
|
||||
|
||||
<!-- 状态下拉 -->
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status">
|
||||
<el-option
|
||||
v-for="item in statusDict"
|
||||
:key="item.dict_value"
|
||||
:label="item.dict_label"
|
||||
:value="item.dict_value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const statusDict = ref<any[]>([])
|
||||
|
||||
const fetchStatusDict = async () => {
|
||||
try {
|
||||
const res = await getDictItemsByCode('user_status')
|
||||
const items = res?.data || res || []
|
||||
statusDict.value = Array.isArray(items) ? items : []
|
||||
} catch (err) {
|
||||
statusDict.value = []
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchStatusDict()
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 示例 3:多个字典项的场景
|
||||
|
||||
**场景**:同时加载多个字典(用户状态、性别、部门类型等)。
|
||||
|
||||
```javascript
|
||||
<script setup lang="ts">
|
||||
import { getDictItemsByCode } from '@/api/dict'
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
// 定义多个字典
|
||||
const statusDict = ref<any[]>([])
|
||||
const genderDict = ref<any[]>([])
|
||||
const deptTypeDict = ref<any[]>([])
|
||||
|
||||
// 统一加载函数
|
||||
const fetchAllDicts = async () => {
|
||||
try {
|
||||
const [status, gender, deptType] = await Promise.all([
|
||||
getDictItemsByCode('user_status'),
|
||||
getDictItemsByCode('gender'),
|
||||
getDictItemsByCode('dept_type'),
|
||||
])
|
||||
|
||||
statusDict.value = status?.data || status || []
|
||||
genderDict.value = gender?.data || gender || []
|
||||
deptTypeDict.value = deptType?.data || deptType || []
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch dicts:', err)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchAllDicts()
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 1. 缓存字典数据
|
||||
|
||||
避免在每个组件中都调用字典 API。建议在全局 store 中缓存字典数据:
|
||||
|
||||
**文件**:`pc/src/stores/dict.ts`
|
||||
|
||||
```typescript
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { getDictItemsByCode } from '@/api/dict'
|
||||
|
||||
export const useDictStore = defineStore('dict', () => {
|
||||
const dicts = ref<Record<string, any[]>>({})
|
||||
|
||||
const fetchDict = async (code: string) => {
|
||||
if (dicts.value[code]) {
|
||||
return dicts.value[code]
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getDictItemsByCode(code)
|
||||
const items = res?.data || res || []
|
||||
dicts.value[code] = Array.isArray(items) ? items : []
|
||||
return dicts.value[code]
|
||||
} catch (err) {
|
||||
console.error(`Failed to fetch dict ${code}:`, err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
return { dicts, fetchDict }
|
||||
})
|
||||
```
|
||||
|
||||
**在组件中使用:**
|
||||
|
||||
```javascript
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
const statusDict = await dictStore.fetchDict('user_status')
|
||||
```
|
||||
|
||||
### 2. 创建字典枚举文件
|
||||
|
||||
建议为常用字典创建枚举文件,便于维护:
|
||||
|
||||
**文件**:`pc/src/constants/dicts.ts`
|
||||
|
||||
```typescript
|
||||
// 字典编码常量
|
||||
export const DICT_CODES = {
|
||||
USER_STATUS: 'user_status',
|
||||
GENDER: 'gender',
|
||||
DEPT_TYPE: 'dept_type',
|
||||
POSITION_LEVEL: 'position_level',
|
||||
} as const
|
||||
|
||||
// 用于 TypeScript 类型
|
||||
export type DictCode = typeof DICT_CODES[keyof typeof DICT_CODES]
|
||||
```
|
||||
|
||||
**在组件中使用:**
|
||||
|
||||
```javascript
|
||||
import { DICT_CODES } from '@/constants/dicts'
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
const statusDict = await dictStore.fetchDict(DICT_CODES.USER_STATUS)
|
||||
```
|
||||
|
||||
### 3. 处理不同的数据值类型
|
||||
|
||||
字典值可能是数字、字符串或其他类型。确保比较时进行正确的类型转换:
|
||||
|
||||
```javascript
|
||||
const getStatusLabel = (status: any) => {
|
||||
// 转换为字符串便于比较
|
||||
const statusStr = String(status)
|
||||
|
||||
const item = statusDict.value.find((d: any) => {
|
||||
// 支持多种比较方式
|
||||
return (
|
||||
String(d.dict_value) === statusStr ||
|
||||
d.dict_value === status ||
|
||||
d.dict_value == status // 宽松比较
|
||||
)
|
||||
})
|
||||
|
||||
return item?.dict_label || '未知'
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 错误处理
|
||||
|
||||
始终为字典加载添加适当的错误处理和回退机制:
|
||||
|
||||
```javascript
|
||||
const fetchStatusDict = async () => {
|
||||
try {
|
||||
const res = await getDictItemsByCode('user_status')
|
||||
statusDict.value = res?.data || []
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch status dict:', err)
|
||||
// 使用默认的回退数据
|
||||
statusDict.value = [
|
||||
{ dict_label: '启用', dict_value: '1' },
|
||||
{ dict_label: '禁用', dict_value: '0' },
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 字典数据在页面刷新后丢失了?
|
||||
|
||||
**A:** 这是正常的,字典数据只在组件的生命周期内存在。建议使用全局 store(Pinia)来缓存字典数据,或在每个需要的组件中通过 `onMounted` 加载。
|
||||
|
||||
### Q2: 后端添加了新的字典项,前端不显示新数据?
|
||||
|
||||
**A:** 前端缓存了字典数据。有两种解决方案:
|
||||
1. 刷新浏览器页面,重新加载字典
|
||||
2. 在后端修改字典后,调用缓存清除接口(如果有的话),或手动清除前端 store 中的字典缓存
|
||||
|
||||
### Q3: 字典编码对应的字典类型不存在?
|
||||
|
||||
**A:** 确保:
|
||||
1. 后端已在 `sys_dict_type` 表中添加了对应的字典类型记录
|
||||
2. 字典类型的状态(`status`)为启用(通常为 1)
|
||||
3. 至少添加了一项字典项(`sys_dict_item` 表中有数据)
|
||||
4. 字典编码(`dict_code`)完全匹配(区分大小写)
|
||||
|
||||
### Q4: 如何在后台管理系统中管理字典?
|
||||
|
||||
**A:** 通常在系统设置或配置模块中有"字典管理"功能,可以:
|
||||
- 添加/编辑/删除字典类型
|
||||
- 管理字典项(标签、值、排序、启用/禁用等)
|
||||
|
||||
具体路径取决于你的后台管理系统设计。
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
使用字典系统的核心步骤:
|
||||
|
||||
1. **后端准备**:在 `sys_dict_type` 和 `sys_dict_item` 表中添加字典数据
|
||||
2. **前端导入**:`import { getDictItemsByCode } from '@/api/dict'`
|
||||
3. **加载字典**:在 `onMounted` 或其他合适位置调用 API 加载
|
||||
4. **使用字典**:通过 `dict_value` 和 `dict_label` 来显示和存储数据
|
||||
5. **最优化**:使用 Pinia store 缓存字典,避免重复请求
|
||||
|
||||
通过字典系统,你可以灵活地管理应用中的枚举值和标签数据,而无需修改代码。
|
||||
335
pc/src/views/system/dict/components/DictItemEdit.vue
Normal file
335
pc/src/views/system/dict/components/DictItemEdit.vue
Normal file
@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑字典项' : '添加字典项'"
|
||||
v-model="visible"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
:model="dictItemForm"
|
||||
:rules="formRules"
|
||||
ref="dictItemFormRef"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="字典类型" prop="dict_type_id">
|
||||
<el-input
|
||||
:value="dictTypeDisplayName"
|
||||
disabled
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div class="form-tip">当前字典类型:{{ dictTypeDisplayName }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典标签" prop="dict_label">
|
||||
<el-input
|
||||
v-model="dictItemForm.dict_label"
|
||||
placeholder="请输入字典标签(显示值)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典值" prop="dict_value">
|
||||
<el-input
|
||||
v-model="dictItemForm.dict_value"
|
||||
placeholder="请输入字典值(存储值)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="父级字典项" prop="parent_id">
|
||||
<el-select
|
||||
v-model="dictItemForm.parent_id"
|
||||
placeholder="选择父级字典项(可选)"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in parentDictItems"
|
||||
:key="item.id"
|
||||
:label="item.dict_label"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="dictItemForm.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number
|
||||
v-model="dictItemForm.sort"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
placeholder="数字越小越靠前"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="颜色" prop="color">
|
||||
<el-input
|
||||
v-model="dictItemForm.color"
|
||||
placeholder="请输入颜色值(如:#FF0000 或 red)"
|
||||
>
|
||||
<template #append>
|
||||
<el-color-picker
|
||||
v-model="dictItemForm.color"
|
||||
:predefine="predefineColors"
|
||||
/>
|
||||
</template>
|
||||
</el-input>
|
||||
<div class="form-tip">支持十六进制颜色值(如:#FF0000)或颜色名称(如:red)</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="图标" prop="icon">
|
||||
<el-input
|
||||
v-model="dictItemForm.icon"
|
||||
placeholder="请输入图标(如:✓、✗、⭐)"
|
||||
/>
|
||||
<div class="form-tip">支持emoji或图标字符</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="dictItemForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { addDictItem, updateDictItem, getDictItems } from '@/api/dict';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
dictItem?: any;
|
||||
dictTypeId?: number;
|
||||
dictTypeName?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dictItem: null,
|
||||
dictTypeId: undefined,
|
||||
dictTypeName: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
|
||||
const submitting = ref(false);
|
||||
const dictItemFormRef = ref<FormInstance>();
|
||||
const parentDictItems = ref<any[]>([]);
|
||||
|
||||
// 预定义颜色
|
||||
const predefineColors = [
|
||||
'#ff4500',
|
||||
'#ff8c00',
|
||||
'#ffd700',
|
||||
'#90ee90',
|
||||
'#00ced1',
|
||||
'#1e90ff',
|
||||
'#c71585',
|
||||
'#ff0000',
|
||||
'#00ff00',
|
||||
'#0000ff',
|
||||
];
|
||||
|
||||
// 判断是否为编辑模式
|
||||
const isEditing = computed(() => {
|
||||
return !!(props.dictItem && props.dictItem.id);
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const dictItemForm = reactive({
|
||||
id: null as number | null,
|
||||
dict_type_id: 0,
|
||||
dict_label: '',
|
||||
dict_value: '',
|
||||
parent_id: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
color: '',
|
||||
icon: '',
|
||||
remark: '',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const formRules: FormRules = {
|
||||
dict_label: [
|
||||
{ required: true, message: '请输入字典标签', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
|
||||
],
|
||||
dict_value: [
|
||||
{ required: true, message: '请输入字典值', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
|
||||
],
|
||||
sort: [
|
||||
{ required: true, message: '请输入排序', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '排序必须大于等于 0', trigger: 'blur' },
|
||||
],
|
||||
color: [
|
||||
{ max: 20, message: '颜色值长度不能超过 20 个字符', trigger: 'blur' },
|
||||
],
|
||||
icon: [
|
||||
{ max: 50, message: '图标长度不能超过 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
remark: [
|
||||
{ max: 500, message: '备注长度不能超过 500 个字符', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 获取父级字典项列表(排除自己)
|
||||
async function fetchParentDictItems() {
|
||||
if (!props.dictTypeId) return;
|
||||
try {
|
||||
const res = await getDictItems({ dict_type_id: props.dictTypeId });
|
||||
if (res.success && res.data) {
|
||||
// 如果是编辑模式,排除当前字典项
|
||||
if (isEditing.value && props.dictItem) {
|
||||
parentDictItems.value = res.data.filter(
|
||||
(item: any) => item.id !== props.dictItem.id
|
||||
);
|
||||
} else {
|
||||
parentDictItems.value = res.data;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取父级字典项失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 计算字典类型显示名称
|
||||
const dictTypeDisplayName = computed(() => {
|
||||
if (props.dictTypeName) {
|
||||
return props.dictTypeName;
|
||||
}
|
||||
if (props.dictTypeId) {
|
||||
return `字典类型 ID: ${props.dictTypeId}`;
|
||||
}
|
||||
return '未知字典类型';
|
||||
});
|
||||
|
||||
// 监听 dictItem 变化,填充表单数据
|
||||
watch(
|
||||
() => props.dictItem,
|
||||
(newDictItem) => {
|
||||
if (newDictItem) {
|
||||
dictItemForm.id = newDictItem.id || null;
|
||||
dictItemForm.dict_type_id = newDictItem.dict_type_id || props.dictTypeId || 0;
|
||||
dictItemForm.dict_label = newDictItem.dict_label || '';
|
||||
dictItemForm.dict_value = newDictItem.dict_value || '';
|
||||
dictItemForm.parent_id = newDictItem.parent_id || 0;
|
||||
dictItemForm.status = newDictItem.status !== undefined ? newDictItem.status : 1;
|
||||
dictItemForm.sort = newDictItem.sort || 0;
|
||||
dictItemForm.color = newDictItem.color || '';
|
||||
dictItemForm.icon = newDictItem.icon || '';
|
||||
dictItemForm.remark = newDictItem.remark || '';
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听弹窗打开,获取父级字典项
|
||||
watch(visible, (val) => {
|
||||
if (val) {
|
||||
if (props.dictTypeId) {
|
||||
dictItemForm.dict_type_id = props.dictTypeId;
|
||||
}
|
||||
fetchParentDictItems();
|
||||
}
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
dictItemForm.id = null;
|
||||
dictItemForm.dict_type_id = props.dictTypeId || 0;
|
||||
dictItemForm.dict_label = '';
|
||||
dictItemForm.dict_value = '';
|
||||
dictItemForm.parent_id = 0;
|
||||
dictItemForm.status = 1;
|
||||
dictItemForm.sort = 0;
|
||||
dictItemForm.color = '';
|
||||
dictItemForm.icon = '';
|
||||
dictItemForm.remark = '';
|
||||
dictItemFormRef.value?.clearValidate();
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
function handleClose() {
|
||||
visible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!dictItemFormRef.value) return;
|
||||
|
||||
try {
|
||||
await dictItemFormRef.value.validate();
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dictItemForm.dict_type_id) {
|
||||
ElMessage.error('字典类型ID不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
dict_type_id: dictItemForm.dict_type_id,
|
||||
dict_label: dictItemForm.dict_label,
|
||||
dict_value: dictItemForm.dict_value,
|
||||
parent_id: dictItemForm.parent_id || 0,
|
||||
status: dictItemForm.status,
|
||||
sort: dictItemForm.sort,
|
||||
color: dictItemForm.color || '',
|
||||
icon: dictItemForm.icon || '',
|
||||
remark: dictItemForm.remark || '',
|
||||
};
|
||||
|
||||
if (isEditing.value) {
|
||||
await updateDictItem(dictItemForm.id!, submitData);
|
||||
ElMessage.success('字典项更新成功');
|
||||
} else {
|
||||
await addDictItem(submitData);
|
||||
ElMessage.success('字典项添加成功');
|
||||
}
|
||||
|
||||
emit('success');
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败,请重试');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
316
pc/src/views/system/dict/components/DictItemEditDialog.vue
Normal file
316
pc/src/views/system/dict/components/DictItemEditDialog.vue
Normal file
@ -0,0 +1,316 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑字典项' : '添加字典项'"
|
||||
v-model="visible"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
:model="dictItemForm"
|
||||
:rules="formRules"
|
||||
ref="dictItemFormRef"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="字典类型" prop="dict_type_id">
|
||||
<el-select
|
||||
v-model="dictItemForm.dict_type_id"
|
||||
placeholder="请选择字典类型"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
:disabled="isEditing || !!dictTypeId"
|
||||
>
|
||||
<el-option
|
||||
v-for="type in dictTypes"
|
||||
:key="type.id"
|
||||
:label="`${type.dict_name} (${type.dict_code})`"
|
||||
:value="type.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典标签" prop="dict_label">
|
||||
<el-input
|
||||
v-model="dictItemForm.dict_label"
|
||||
placeholder="请输入字典标签(显示值)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典值" prop="dict_value">
|
||||
<el-input
|
||||
v-model="dictItemForm.dict_value"
|
||||
placeholder="请输入字典值(存储值)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="父级ID" prop="parent_id">
|
||||
<el-input-number
|
||||
v-model="dictItemForm.parent_id"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
placeholder="0表示顶级"
|
||||
/>
|
||||
<div class="form-tip">0表示顶级字典项</div>
|
||||
</el-form-item> -->
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="dictItemForm.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序序号" prop="sort">
|
||||
<el-input-number
|
||||
v-model="dictItemForm.sort"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
placeholder="数字越小越靠前"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="颜色标记" prop="color">
|
||||
<el-input
|
||||
v-model="dictItemForm.color"
|
||||
placeholder="请输入颜色值(如:#FF0000 或 red)"
|
||||
maxlength="20"
|
||||
/>
|
||||
<div class="form-tip">用于前端显示时的颜色标记</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="图标" prop="icon">
|
||||
<el-input
|
||||
v-model="dictItemForm.icon"
|
||||
placeholder="请输入图标名称或类名"
|
||||
maxlength="50"
|
||||
/>
|
||||
<div class="form-tip">用于前端显示时的图标</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="dictItemForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed, onMounted } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { getDictTypes, addDictItem, updateDictItem } from '@/api/dict'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dictItem?: any
|
||||
dictTypeId?: number | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dictItem: null,
|
||||
dictTypeId: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean]
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
})
|
||||
|
||||
const submitting = ref(false)
|
||||
const dictItemFormRef = ref<FormInstance>()
|
||||
const dictTypes = ref<any[]>([])
|
||||
|
||||
// 判断是否为编辑模式
|
||||
const isEditing = computed(() => {
|
||||
return !!(props.dictItem && props.dictItem.id)
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const dictItemForm = reactive({
|
||||
id: null as number | null,
|
||||
dict_type_id: null as number | null,
|
||||
dict_label: '',
|
||||
dict_value: '',
|
||||
parent_id: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
color: '',
|
||||
icon: '',
|
||||
remark: '',
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules: FormRules = {
|
||||
dict_type_id: [
|
||||
{ required: true, message: '请选择字典类型', trigger: 'change' },
|
||||
],
|
||||
dict_label: [
|
||||
{ required: true, message: '请输入字典标签', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
|
||||
],
|
||||
dict_value: [
|
||||
{ required: true, message: '请输入字典值', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' },
|
||||
],
|
||||
parent_id: [
|
||||
{ type: 'number', min: 0, message: '父级ID必须大于等于 0', trigger: 'blur' },
|
||||
],
|
||||
sort: [
|
||||
{ type: 'number', min: 0, message: '排序序号必须大于等于 0', trigger: 'blur' },
|
||||
],
|
||||
color: [
|
||||
{ max: 20, message: '颜色值长度不能超过 20 个字符', trigger: 'blur' },
|
||||
],
|
||||
icon: [
|
||||
{ max: 50, message: '图标长度不能超过 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
remark: [
|
||||
{ max: 500, message: '备注长度不能超过 500 个字符', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
// 获取字典类型列表
|
||||
async function fetchDictTypes() {
|
||||
try {
|
||||
const res = await getDictTypes()
|
||||
if (res.success === true && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
} else if (Array.isArray(res)) {
|
||||
dictTypes.value = res
|
||||
} else if (res && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('获取字典类型列表失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 dictItem 变化,填充表单数据
|
||||
watch(
|
||||
() => props.dictItem,
|
||||
(newDictItem) => {
|
||||
if (newDictItem) {
|
||||
dictItemForm.id = newDictItem.id || null
|
||||
dictItemForm.dict_type_id = newDictItem.dict_type_id || null
|
||||
dictItemForm.dict_label = newDictItem.dict_label || ''
|
||||
dictItemForm.dict_value = newDictItem.dict_value || ''
|
||||
dictItemForm.parent_id = newDictItem.parent_id || 0
|
||||
dictItemForm.status = newDictItem.status !== undefined ? newDictItem.status : 1
|
||||
dictItemForm.sort = newDictItem.sort || 0
|
||||
dictItemForm.color = newDictItem.color || ''
|
||||
dictItemForm.icon = newDictItem.icon || ''
|
||||
dictItemForm.remark = newDictItem.remark || ''
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 监听 dictTypeId 变化
|
||||
watch(
|
||||
() => props.dictTypeId,
|
||||
(newTypeId) => {
|
||||
if (newTypeId && !isEditing.value) {
|
||||
dictItemForm.dict_type_id = newTypeId
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
dictItemForm.id = null
|
||||
dictItemForm.dict_type_id = props.dictTypeId || null
|
||||
dictItemForm.dict_label = ''
|
||||
dictItemForm.dict_value = ''
|
||||
dictItemForm.parent_id = 0
|
||||
dictItemForm.status = 1
|
||||
dictItemForm.sort = 0
|
||||
dictItemForm.color = ''
|
||||
dictItemForm.icon = ''
|
||||
dictItemForm.remark = ''
|
||||
dictItemFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
function handleClose() {
|
||||
visible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!dictItemFormRef.value) return
|
||||
|
||||
try {
|
||||
await dictItemFormRef.value.validate()
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const submitData: any = {
|
||||
dict_type_id: dictItemForm.dict_type_id,
|
||||
dict_label: dictItemForm.dict_label,
|
||||
dict_value: dictItemForm.dict_value,
|
||||
parent_id: dictItemForm.parent_id,
|
||||
status: dictItemForm.status,
|
||||
sort: dictItemForm.sort,
|
||||
color: dictItemForm.color || '',
|
||||
icon: dictItemForm.icon || '',
|
||||
remark: dictItemForm.remark || '',
|
||||
}
|
||||
|
||||
if (isEditing.value) {
|
||||
const res = await updateDictItem(dictItemForm.id!, submitData)
|
||||
if (res.success === true || res.code === 0) {
|
||||
ElMessage.success('字典项更新成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.error(res.message || '更新失败,请重试')
|
||||
}
|
||||
} else {
|
||||
const res = await addDictItem(submitData)
|
||||
if (res.success === true || res.code === 0) {
|
||||
ElMessage.success('字典项添加成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.error(res.message || '添加失败,请重试')
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败,请重试')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDictTypes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
422
pc/src/views/system/dict/components/DictItemList.vue
Normal file
422
pc/src/views/system/dict/components/DictItemList.vue
Normal file
@ -0,0 +1,422 @@
|
||||
<template>
|
||||
<div class="dict-item-list">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-select
|
||||
v-model="selectedDictTypeId"
|
||||
placeholder="选择字典类型"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 250px"
|
||||
@change="handleTypeChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="type in dictTypes"
|
||||
:key="type.id"
|
||||
:label="`${type.dict_name} (${type.dict_code})`"
|
||||
:value="type.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索字典项标签或值"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@input="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select
|
||||
v-model="statusFilter"
|
||||
placeholder="状态筛选"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
@change="handleFilter"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button
|
||||
type="primary"
|
||||
:disabled="!selectedDictTypeId"
|
||||
@click="handleAdd"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加字典项
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<el-alert
|
||||
v-if="!selectedDictTypeId"
|
||||
title="请先选择字典类型"
|
||||
type="info"
|
||||
:closable="false"
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="error" class="error-state">
|
||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="fetchDictItems">重试</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 字典项列表 -->
|
||||
<div v-else-if="selectedDictTypeId">
|
||||
<el-table
|
||||
:data="filteredDictItems"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="dict_label" label="字典标签" min-width="150" align="center" />
|
||||
<el-table-column prop="dict_value" label="字典值" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.dict_value }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column prop="parent_id" label="父级ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.parent_id || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="color" label="颜色" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.color" class="color-display">
|
||||
<span
|
||||
class="color-dot"
|
||||
:style="{ backgroundColor: row.color }"
|
||||
></span>
|
||||
<span>{{ row.color }}</span>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="icon" label="图标" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.icon">{{ row.icon }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注" min-width="200" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<DictItemEditDialog
|
||||
v-model="editDialogVisible"
|
||||
:dict-item="currentDictItem"
|
||||
:dict-type-id="selectedDictTypeId"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
} from 'element-plus'
|
||||
import { Plus, Search, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDictTypes,
|
||||
getDictItems,
|
||||
deleteDictItem,
|
||||
} from '@/api/dict'
|
||||
import DictItemEditDialog from './DictItemEditDialog.vue'
|
||||
|
||||
interface Props {
|
||||
selectedTypeId?: number | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
selectedTypeId: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'refresh': []
|
||||
}>()
|
||||
|
||||
const dictTypes = ref<any[]>([])
|
||||
const dictItems = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const statusFilter = ref<number | ''>('')
|
||||
const selectedDictTypeId = ref<number | null>(null)
|
||||
|
||||
const editDialogVisible = ref(false)
|
||||
const currentDictItem = ref<any>(null)
|
||||
|
||||
// 过滤后的字典项
|
||||
const filteredDictItems = computed(() => {
|
||||
let result = dictItems.value
|
||||
|
||||
// 搜索过滤
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(
|
||||
(item) =>
|
||||
(item.dict_label && item.dict_label.toLowerCase().includes(keyword)) ||
|
||||
(item.dict_value && item.dict_value.toLowerCase().includes(keyword)) ||
|
||||
(item.remark && item.remark.toLowerCase().includes(keyword))
|
||||
)
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if (statusFilter.value !== '') {
|
||||
result = result.filter((item) => item.status === statusFilter.value)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 监听外部传入的selectedTypeId
|
||||
watch(
|
||||
() => props.selectedTypeId,
|
||||
(newId) => {
|
||||
if (newId) {
|
||||
selectedDictTypeId.value = newId
|
||||
fetchDictItems()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return ''
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
} catch {
|
||||
return dateStr as string
|
||||
}
|
||||
}
|
||||
|
||||
// 获取字典类型列表(用于下拉选择)
|
||||
async function fetchDictTypes() {
|
||||
try {
|
||||
const res = await getDictTypes()
|
||||
if (res.success === true && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
} else if (Array.isArray(res)) {
|
||||
dictTypes.value = res
|
||||
} else if (res && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('获取字典类型列表失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取字典项列表
|
||||
async function fetchDictItems() {
|
||||
if (!selectedDictTypeId.value) {
|
||||
dictItems.value = []
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const res = await getDictItems({ dict_type_id: selectedDictTypeId.value })
|
||||
if (res.success === true && res.data) {
|
||||
dictItems.value = Array.isArray(res.data) ? res.data : []
|
||||
} else if (Array.isArray(res)) {
|
||||
dictItems.value = res
|
||||
} else if (res && res.data) {
|
||||
dictItems.value = Array.isArray(res.data) ? res.data : []
|
||||
} else {
|
||||
error.value = res.message || '获取字典项列表失败'
|
||||
dictItems.value = []
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '获取字典项列表失败'
|
||||
dictItems.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 字典类型改变
|
||||
const handleTypeChange = () => {
|
||||
fetchDictItems()
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑在computed中处理
|
||||
}
|
||||
|
||||
// 筛选
|
||||
const handleFilter = () => {
|
||||
// 筛选逻辑在computed中处理
|
||||
}
|
||||
|
||||
// 设置字典类型ID(供外部调用)
|
||||
function setTypeId(typeId: number) {
|
||||
selectedDictTypeId.value = typeId
|
||||
}
|
||||
|
||||
// 添加字典项
|
||||
function handleAdd() {
|
||||
if (!selectedDictTypeId.value) {
|
||||
ElMessage.warning('请先选择字典类型')
|
||||
return
|
||||
}
|
||||
currentDictItem.value = null
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑字典项
|
||||
function handleEdit(row: any) {
|
||||
currentDictItem.value = { ...row }
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除字典项
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除字典项「${row.dict_label}」吗?删除后不可恢复。`,
|
||||
'警告',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await deleteDictItem(row.id)
|
||||
if (res.success === true || res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchDictItems()
|
||||
emit('refresh')
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || '删除失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} catch {
|
||||
// 取消删除
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
function handleEditSuccess() {
|
||||
fetchDictItems()
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
// 刷新
|
||||
function refresh() {
|
||||
fetchDictTypes()
|
||||
if (selectedDictTypeId.value) {
|
||||
fetchDictItems()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
setTypeId,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchDictTypes()
|
||||
if (selectedDictTypeId.value) {
|
||||
fetchDictItems()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.dict-item-list {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
gap: 12px;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.color-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
|
||||
.color-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 220px;
|
||||
padding: 32px 0 16px 0;
|
||||
background: var(--el-bg-color-page);
|
||||
border-radius: 5px;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
222
pc/src/views/system/dict/components/DictTypeEdit.vue
Normal file
222
pc/src/views/system/dict/components/DictTypeEdit.vue
Normal file
@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑字典类型' : '添加字典类型'"
|
||||
v-model="visible"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
:model="dictTypeForm"
|
||||
:rules="formRules"
|
||||
ref="dictTypeFormRef"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="字典编码" prop="dict_code">
|
||||
<el-input
|
||||
v-model="dictTypeForm.dict_code"
|
||||
placeholder="请输入字典编码(如:user_status)"
|
||||
:disabled="isEditing"
|
||||
/>
|
||||
<div class="form-tip">只能包含字母、数字或下划线,且必须唯一</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典名称" prop="dict_name">
|
||||
<el-input
|
||||
v-model="dictTypeForm.dict_name"
|
||||
placeholder="请输入字典名称"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="dictTypeForm.status">
|
||||
<el-radio :value="1">启用</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序序号" prop="sort">
|
||||
<el-input-number
|
||||
v-model="dictTypeForm.sort"
|
||||
:min="0"
|
||||
style="width: 100%"
|
||||
placeholder="数字越小越靠前"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="dictTypeForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed, onMounted } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { addDictType, updateDictType, getDictTypes } from '@/api/dict';
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean;
|
||||
dictType?: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dictType: null,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: boolean];
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val),
|
||||
});
|
||||
|
||||
const submitting = ref(false);
|
||||
const dictTypeFormRef = ref<FormInstance>();
|
||||
// 已移除父级选择
|
||||
|
||||
// 判断是否为编辑模式
|
||||
const isEditing = computed(() => {
|
||||
return !!(props.dictType && props.dictType.id);
|
||||
});
|
||||
|
||||
// 表单数据
|
||||
const dictTypeForm = reactive({
|
||||
id: null as number | null,
|
||||
dict_code: '',
|
||||
dict_name: '',
|
||||
status: 1,
|
||||
sort: 0,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const formRules: FormRules = {
|
||||
dict_code: [
|
||||
{ required: true, message: '请输入字典编码', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_]+$/,
|
||||
message: '只能包含字母、数字或下划线',
|
||||
trigger: 'blur',
|
||||
},
|
||||
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
|
||||
],
|
||||
dict_name: [
|
||||
{ required: true, message: '请输入字典名称', trigger: 'blur' },
|
||||
{ min: 2, max: 100, message: '长度在 2 到 100 个字符', trigger: 'blur' },
|
||||
],
|
||||
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
|
||||
sort: [
|
||||
{ required: true, message: '请输入排序序号', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '排序序号必须大于等于 0', trigger: 'blur' },
|
||||
],
|
||||
remark: [
|
||||
{ max: 500, message: '备注长度不能超过 500 个字符', trigger: 'blur' },
|
||||
],
|
||||
};
|
||||
|
||||
// 已移除父级选择相关数据获取
|
||||
|
||||
// 监听 dictType 变化,填充表单数据
|
||||
watch(
|
||||
() => props.dictType,
|
||||
(newDictType) => {
|
||||
if (newDictType) {
|
||||
dictTypeForm.id = newDictType.id || null;
|
||||
dictTypeForm.dict_code = newDictType.dict_code || '';
|
||||
dictTypeForm.dict_name = newDictType.dict_name || '';
|
||||
dictTypeForm.status = newDictType.status !== undefined ? newDictType.status : 1;
|
||||
dictTypeForm.sort = newDictType.sort || 0;
|
||||
dictTypeForm.remark = newDictType.remark || '';
|
||||
} else {
|
||||
resetForm();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 已移除父级选择监听
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
dictTypeForm.id = null;
|
||||
dictTypeForm.dict_code = '';
|
||||
dictTypeForm.dict_name = '';
|
||||
dictTypeForm.status = 1;
|
||||
dictTypeForm.sort = 0;
|
||||
dictTypeForm.remark = '';
|
||||
dictTypeFormRef.value?.clearValidate();
|
||||
}
|
||||
|
||||
// 关闭对话框
|
||||
function handleClose() {
|
||||
visible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
if (!dictTypeFormRef.value) return;
|
||||
|
||||
try {
|
||||
await dictTypeFormRef.value.validate();
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
const submitData: any = {
|
||||
dict_code: dictTypeForm.dict_code,
|
||||
dict_name: dictTypeForm.dict_name,
|
||||
status: dictTypeForm.status,
|
||||
sort: dictTypeForm.sort,
|
||||
remark: dictTypeForm.remark || '',
|
||||
};
|
||||
|
||||
if (isEditing.value) {
|
||||
await updateDictType(dictTypeForm.id!, submitData);
|
||||
ElMessage.success('字典类型更新成功');
|
||||
} else {
|
||||
await addDictType(submitData);
|
||||
ElMessage.success('字典类型添加成功');
|
||||
}
|
||||
|
||||
emit('success');
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '操作失败,请重试');
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 无需加载父级数据
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
316
pc/src/views/system/dict/components/DictTypeList.vue
Normal file
316
pc/src/views/system/dict/components/DictTypeList.vue
Normal file
@ -0,0 +1,316 @@
|
||||
<template>
|
||||
<div class="dict-type-list">
|
||||
<div class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索字典类型名称或编码"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@input="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-select
|
||||
v-model="statusFilter"
|
||||
placeholder="状态筛选"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
@change="handleFilter"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加字典类型
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-if="error" class="error-state">
|
||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="fetchDictTypes">重试</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 字典类型列表 -->
|
||||
<div v-else>
|
||||
<el-table
|
||||
:data="filteredDictTypes"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="dict_name" label="字典名称" min-width="150" align="center" />
|
||||
<el-table-column prop="dict_code" label="字典编码" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.dict_code }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="tenant_id" label="租户ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.tenant_id || 0 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column prop="parent_id" label="父级ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.parent_id || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="remark" label="备注" min-width="200" align="center" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click.stop="handleViewItems(row)">
|
||||
<el-icon><Menu /></el-icon>
|
||||
查看字典项
|
||||
</el-button>
|
||||
<el-button size="small" type="primary" link @click.stop="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click.stop="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<DictTypeEditDialog
|
||||
v-model="editDialogVisible"
|
||||
:dict-type="currentDictType"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
} from 'element-plus'
|
||||
import { Plus, Search, Edit, Delete, Menu } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDictTypes,
|
||||
deleteDictType,
|
||||
} from '@/api/dict'
|
||||
import DictTypeEditDialog from './DictTypeEditDialog.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
'select-type': [typeId: number]
|
||||
'refresh': []
|
||||
}>()
|
||||
|
||||
const dictTypes = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const statusFilter = ref<number | ''>('')
|
||||
|
||||
const editDialogVisible = ref(false)
|
||||
const currentDictType = ref<any>(null)
|
||||
|
||||
// 过滤后的字典类型
|
||||
const filteredDictTypes = computed(() => {
|
||||
let result = dictTypes.value
|
||||
|
||||
// 搜索过滤
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(
|
||||
(type) =>
|
||||
(type.dict_name && type.dict_name.toLowerCase().includes(keyword)) ||
|
||||
(type.dict_code && type.dict_code.toLowerCase().includes(keyword))
|
||||
)
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if (statusFilter.value !== '') {
|
||||
result = result.filter((type) => type.status === statusFilter.value)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return ''
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
} catch {
|
||||
return dateStr as string
|
||||
}
|
||||
}
|
||||
|
||||
// 获取字典类型列表
|
||||
async function fetchDictTypes() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const res = await getDictTypes()
|
||||
if (res.success === true && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
} else if (Array.isArray(res)) {
|
||||
dictTypes.value = res
|
||||
} else if (res && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
} else {
|
||||
error.value = res.message || '获取字典类型列表失败'
|
||||
dictTypes.value = []
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || '获取字典类型列表失败'
|
||||
dictTypes.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
// 搜索逻辑在computed中处理
|
||||
}
|
||||
|
||||
// 筛选
|
||||
const handleFilter = () => {
|
||||
// 筛选逻辑在computed中处理
|
||||
}
|
||||
|
||||
// 添加字典类型
|
||||
function handleAdd() {
|
||||
currentDictType.value = null
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑字典类型
|
||||
function handleEdit(row: any) {
|
||||
currentDictType.value = { ...row }
|
||||
editDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 查看字典项
|
||||
function handleViewItems(row: any) {
|
||||
emit('select-type', row.id)
|
||||
}
|
||||
|
||||
// 行点击
|
||||
function handleRowClick(row: any) {
|
||||
// 可以在这里处理行点击事件
|
||||
}
|
||||
|
||||
// 删除字典类型
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除字典类型「${row.dict_name}」吗?删除后不可恢复。`,
|
||||
'警告',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await deleteDictType(row.id)
|
||||
if (res.success === true || res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchDictTypes()
|
||||
emit('refresh')
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || '删除失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} catch {
|
||||
// 取消删除
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
function handleEditSuccess() {
|
||||
fetchDictTypes()
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
// 刷新
|
||||
function refresh() {
|
||||
fetchDictTypes()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
refresh,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchDictTypes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.dict-type-list {
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
gap: 12px;
|
||||
|
||||
.toolbar-left {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 220px;
|
||||
padding: 32px 0 16px 0;
|
||||
background: var(--el-bg-color-page);
|
||||
border-radius: 5px;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
<template>
|
||||
<div class="dict-container">
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>字典管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAddType">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加字典类型
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
@ -15,602 +11,333 @@
|
||||
</div>
|
||||
<el-divider></el-divider>
|
||||
|
||||
<div class="dict-content">
|
||||
<!-- 左侧:字典类型列表 -->
|
||||
<div class="dict-type-panel">
|
||||
<div class="panel-header">
|
||||
<h3>字典类型</h3>
|
||||
<el-input
|
||||
v-model="typeSearchKeyword"
|
||||
placeholder="搜索字典类型"
|
||||
clearable
|
||||
@input="handleTypeSearch"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
<el-table
|
||||
:data="filteredDictTypes"
|
||||
stripe
|
||||
highlight-current-row
|
||||
@current-change="handleTypeSelect"
|
||||
v-loading="typeLoading"
|
||||
style="width: 100%"
|
||||
height="calc(100vh - 280px)"
|
||||
>
|
||||
<el-table-column prop="dict_code" label="编码" width="150" />
|
||||
<el-table-column prop="dict_name" label="名称" min-width="120" />
|
||||
<el-table-column prop="status" label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEditType(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDeleteType(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:字典项列表 -->
|
||||
<div class="dict-item-panel">
|
||||
<div class="panel-header">
|
||||
<h3>字典项</h3>
|
||||
<div class="panel-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleAddItem"
|
||||
:disabled="!currentDictType"
|
||||
>
|
||||
<!-- 内容区域:根据当前视图切换 -->
|
||||
<div v-if="currentView === 'types'">
|
||||
<div class="tab-content">
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="handleAddType">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加字典项
|
||||
添加字典类型
|
||||
</el-button>
|
||||
<div class="search-box">
|
||||
<el-input
|
||||
v-model="typeSearchKeyword"
|
||||
placeholder="搜索字典类型名称或编码"
|
||||
clearable
|
||||
style="width: 300px"
|
||||
@input="handleTypeSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="filteredDictTypes"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="typeLoading"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="dict_name" label="字典名称" min-width="150" align="center" />
|
||||
<el-table-column prop="dict_code" label="字典编码" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.dict_code }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- <el-table-column prop="parent_id" label="父级ID" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.parent_id > 0">{{ row.parent_id }}</span>
|
||||
<span v-else style="color: #999">-</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column prop="create_time" label="创建时间" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEditType(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button size="small" type="primary" link @click="handleViewItems(row)">
|
||||
<el-icon><List /></el-icon>
|
||||
查看字典项
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDeleteType(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="currentView === 'items'">
|
||||
<div class="tab-content">
|
||||
<div class="toolbar">
|
||||
<div>
|
||||
<el-button @click="goBackToTypes">
|
||||
返回
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="search-box">
|
||||
<span v-if="selectedDictType">
|
||||
当前字典类型:{{ selectedDictType.dict_name }}({{ selectedDictType.dict_code }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!currentDictType" class="empty-state">
|
||||
<el-empty description="请先选择字典类型" />
|
||||
</div>
|
||||
<el-table
|
||||
v-else
|
||||
:data="dictItems"
|
||||
stripe
|
||||
v-loading="itemLoading"
|
||||
style="width: 100%"
|
||||
height="calc(100vh - 280px)"
|
||||
>
|
||||
<el-table-column prop="dict_label" label="标签" min-width="120" />
|
||||
<el-table-column prop="dict_value" label="值" min-width="120" />
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="status" label="状态" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="color" label="颜色" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag v-if="row.color" :color="row.color" size="small">{{ row.color }}</el-tag>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" type="primary" link @click="handleEditItem(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDeleteItem(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<DictItemList :selected-type-id="selectedDictTypeId || undefined" @refresh="refreshTypesSilently" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 字典类型编辑对话框 -->
|
||||
<el-dialog
|
||||
<DictTypeEditDialog
|
||||
v-model="typeDialogVisible"
|
||||
:title="typeDialogTitle"
|
||||
width="600px"
|
||||
@close="handleTypeDialogClose"
|
||||
>
|
||||
<el-form :model="typeForm" :rules="typeRules" ref="typeFormRef" label-width="100px">
|
||||
<el-form-item label="字典编码" prop="dict_code">
|
||||
<el-input
|
||||
v-model="typeForm.dict_code"
|
||||
placeholder="请输入字典编码(字母、数字、下划线)"
|
||||
:disabled="typeForm.id > 0"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="字典名称" prop="dict_name">
|
||||
<el-input v-model="typeForm.dict_name" placeholder="请输入字典名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="父级字典" prop="parent_id">
|
||||
<el-select v-model="typeForm.parent_id" placeholder="请选择父级字典" clearable>
|
||||
<el-option
|
||||
v-for="type in dictTypes"
|
||||
:key="type.id"
|
||||
:label="type.dict_name"
|
||||
:value="type.id"
|
||||
:disabled="type.id === typeForm.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="typeForm.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="typeForm.sort" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="typeForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="typeDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleTypeSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
:dict-type="currentDictType"
|
||||
@success="handleTypeSuccess"
|
||||
/>
|
||||
|
||||
<!-- 字典项编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="itemDialogVisible"
|
||||
:title="itemDialogTitle"
|
||||
width="600px"
|
||||
@close="handleItemDialogClose"
|
||||
>
|
||||
<el-form :model="itemForm" :rules="itemRules" ref="itemFormRef" label-width="100px">
|
||||
<el-form-item label="字典标签" prop="dict_label">
|
||||
<el-input v-model="itemForm.dict_label" placeholder="请输入字典标签(显示值)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典值" prop="dict_value">
|
||||
<el-input
|
||||
v-model="itemForm.dict_value"
|
||||
placeholder="请输入字典值(存储值)"
|
||||
:disabled="itemForm.id > 0"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="父级项" prop="parent_id">
|
||||
<el-select v-model="itemForm.parent_id" placeholder="请选择父级项" clearable>
|
||||
<el-option
|
||||
v-for="item in dictItems"
|
||||
:key="item.id"
|
||||
:label="item.dict_label"
|
||||
:value="item.id"
|
||||
:disabled="item.id === itemForm.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="itemForm.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="itemForm.sort" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="颜色" prop="color">
|
||||
<el-input v-model="itemForm.color" placeholder="如:#1890ff" />
|
||||
</el-form-item>
|
||||
<el-form-item label="图标" prop="icon">
|
||||
<el-input v-model="itemForm.icon" placeholder="如:el-icon-success" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="itemForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="itemDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleItemSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 字典项独立页面内操作通过 DictItemList 完成 -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Edit, Delete, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Delete,
|
||||
Refresh,
|
||||
Search,
|
||||
List,
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDictTypes,
|
||||
addDictType,
|
||||
updateDictType,
|
||||
deleteDictType,
|
||||
getDictItems,
|
||||
addDictItem,
|
||||
updateDictItem,
|
||||
deleteDictItem
|
||||
} from '@/api/dict'
|
||||
import DictTypeEditDialog from './components/DictTypeEdit.vue'
|
||||
import DictItemList from './components/DictItemList.vue'
|
||||
|
||||
// 数据
|
||||
const dictTypes = ref([])
|
||||
const filteredDictTypes = ref([])
|
||||
const dictItems = ref([])
|
||||
const currentDictType = ref(null)
|
||||
const typeSearchKeyword = ref('')
|
||||
const currentView = ref<'types' | 'items'>('types')
|
||||
const typeLoading = ref(false)
|
||||
const itemLoading = ref(false)
|
||||
|
||||
// 对话框
|
||||
// 字典类型相关
|
||||
const dictTypes = ref<any[]>([])
|
||||
const typeSearchKeyword = ref('')
|
||||
const typeDialogVisible = ref(false)
|
||||
const itemDialogVisible = ref(false)
|
||||
const typeFormRef = ref(null)
|
||||
const itemFormRef = ref(null)
|
||||
const currentDictType = ref<any>(null)
|
||||
|
||||
// 表单数据
|
||||
const typeForm = reactive({
|
||||
id: 0,
|
||||
dict_code: '',
|
||||
dict_name: '',
|
||||
parent_id: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
remark: ''
|
||||
const selectedDictTypeId = ref<number | null>(null)
|
||||
const selectedDictType = computed(() => {
|
||||
if (!selectedDictTypeId.value) {
|
||||
return null
|
||||
}
|
||||
return dictTypes.value.find((type) => type.id === selectedDictTypeId.value) || null
|
||||
})
|
||||
|
||||
const itemForm = reactive({
|
||||
id: 0,
|
||||
dict_type_id: 0,
|
||||
dict_label: '',
|
||||
dict_value: '',
|
||||
parent_id: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
color: '',
|
||||
icon: '',
|
||||
remark: ''
|
||||
// 过滤后的字典类型
|
||||
const filteredDictTypes = computed(() => {
|
||||
if (!typeSearchKeyword.value) {
|
||||
return dictTypes.value
|
||||
}
|
||||
const keyword = typeSearchKeyword.value.toLowerCase()
|
||||
return dictTypes.value.filter(
|
||||
(type) =>
|
||||
(type.dict_name && type.dict_name.toLowerCase().includes(keyword)) ||
|
||||
(type.dict_code && type.dict_code.toLowerCase().includes(keyword))
|
||||
)
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const typeRules = {
|
||||
dict_code: [
|
||||
{ required: true, message: '请输入字典编码', trigger: 'blur' },
|
||||
{ pattern: /^[A-Za-z0-9_]+$/, message: '字典编码只能包含字母、数字、下划线', trigger: 'blur' }
|
||||
],
|
||||
dict_name: [{ required: true, message: '请输入字典名称', trigger: 'blur' }]
|
||||
// 格式化日期
|
||||
function formatDate(dateStr: string | null | undefined): string {
|
||||
if (!dateStr) return ''
|
||||
try {
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
} catch {
|
||||
return dateStr as string
|
||||
}
|
||||
}
|
||||
|
||||
const itemRules = {
|
||||
dict_label: [{ required: true, message: '请输入字典标签', trigger: 'blur' }],
|
||||
dict_value: [{ required: true, message: '请输入字典值', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const typeDialogTitle = computed(() => {
|
||||
return typeForm.id > 0 ? '编辑字典类型' : '添加字典类型'
|
||||
})
|
||||
|
||||
const itemDialogTitle = computed(() => {
|
||||
return itemForm.id > 0 ? '编辑字典项' : '添加字典项'
|
||||
})
|
||||
|
||||
// 方法
|
||||
const fetchDictTypes = async () => {
|
||||
// 获取字典类型列表
|
||||
async function fetchDictTypes() {
|
||||
typeLoading.value = true
|
||||
try {
|
||||
const res = await getDictTypes()
|
||||
if (res.success) {
|
||||
dictTypes.value = res.data || []
|
||||
filteredDictTypes.value = dictTypes.value
|
||||
if (res.success === true && res.data) {
|
||||
dictTypes.value = Array.isArray(res.data) ? res.data : []
|
||||
} else if (Array.isArray(res)) {
|
||||
dictTypes.value = res
|
||||
} else {
|
||||
dictTypes.value = []
|
||||
ElMessage.error(res.message || '获取字典类型失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取字典类型失败: ' + error.message)
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '获取字典类型失败')
|
||||
dictTypes.value = []
|
||||
} finally {
|
||||
typeLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchDictItems = async (dictTypeId) => {
|
||||
if (!dictTypeId) {
|
||||
dictItems.value = []
|
||||
return
|
||||
}
|
||||
itemLoading.value = true
|
||||
try {
|
||||
const res = await getDictItems({ dict_type_id: dictTypeId })
|
||||
if (res.success) {
|
||||
dictItems.value = res.data || []
|
||||
} else {
|
||||
ElMessage.error(res.message || '获取字典项失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取字典项失败: ' + error.message)
|
||||
} finally {
|
||||
itemLoading.value = false
|
||||
}
|
||||
// 标签式视图切换已移除
|
||||
|
||||
// 字典类型搜索
|
||||
function handleTypeSearch() {
|
||||
// 搜索逻辑已在computed中处理
|
||||
}
|
||||
|
||||
const handleTypeSearch = () => {
|
||||
if (!typeSearchKeyword.value) {
|
||||
filteredDictTypes.value = dictTypes.value
|
||||
return
|
||||
}
|
||||
const keyword = typeSearchKeyword.value.toLowerCase()
|
||||
filteredDictTypes.value = dictTypes.value.filter(
|
||||
(type) =>
|
||||
type.dict_code.toLowerCase().includes(keyword) ||
|
||||
type.dict_name.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
// 字典项视图内由子组件处理
|
||||
|
||||
const handleTypeSelect = (row) => {
|
||||
currentDictType.value = row
|
||||
if (row) {
|
||||
fetchDictItems(row.id)
|
||||
} else {
|
||||
dictItems.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddType = () => {
|
||||
Object.assign(typeForm, {
|
||||
id: 0,
|
||||
dict_code: '',
|
||||
dict_name: '',
|
||||
parent_id: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
remark: ''
|
||||
})
|
||||
// 添加字典类型
|
||||
function handleAddType() {
|
||||
currentDictType.value = null
|
||||
typeDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEditType = (row) => {
|
||||
Object.assign(typeForm, {
|
||||
id: row.id,
|
||||
dict_code: row.dict_code,
|
||||
dict_name: row.dict_name,
|
||||
parent_id: row.parent_id || 0,
|
||||
status: row.status,
|
||||
sort: row.sort || 0,
|
||||
remark: row.remark || ''
|
||||
})
|
||||
// 编辑字典类型
|
||||
function handleEditType(row: any) {
|
||||
currentDictType.value = { ...row }
|
||||
typeDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDeleteType = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该字典类型吗?删除后该类型下的所有字典项也将无法使用。', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
const res = await deleteDictType(row.id)
|
||||
if (res.success) {
|
||||
ElMessage.success('删除成功')
|
||||
if (currentDictType.value && currentDictType.value.id === row.id) {
|
||||
currentDictType.value = null
|
||||
dictItems.value = []
|
||||
}
|
||||
fetchDictTypes()
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
// 查看字典项
|
||||
function handleViewItems(row: any) {
|
||||
selectedDictTypeId.value = row.id
|
||||
currentView.value = 'items'
|
||||
}
|
||||
|
||||
const handleTypeSubmit = async () => {
|
||||
if (!typeFormRef.value) return
|
||||
await typeFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
// 删除字典类型
|
||||
async function handleDeleteType(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除字典类型「${row.dict_name}」吗?删除后不可恢复。`,
|
||||
'警告',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
typeLoading.value = true
|
||||
try {
|
||||
let res
|
||||
if (typeForm.id > 0) {
|
||||
res = await updateDictType(typeForm.id, typeForm)
|
||||
} else {
|
||||
res = await addDictType(typeForm)
|
||||
}
|
||||
if (res.success) {
|
||||
ElMessage.success(typeForm.id > 0 ? '更新成功' : '添加成功')
|
||||
typeDialogVisible.value = false
|
||||
const res = await deleteDictType(row.id)
|
||||
if (res.success === true) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchDictTypes()
|
||||
if (currentDictType.value && currentDictType.value.id === typeForm.id) {
|
||||
fetchDictItems(typeForm.id)
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.message || '操作失败')
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败: ' + error.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleTypeDialogClose = () => {
|
||||
typeFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleAddItem = () => {
|
||||
if (!currentDictType.value) {
|
||||
ElMessage.warning('请先选择字典类型')
|
||||
return
|
||||
}
|
||||
Object.assign(itemForm, {
|
||||
id: 0,
|
||||
dict_type_id: currentDictType.value.id,
|
||||
dict_label: '',
|
||||
dict_value: '',
|
||||
parent_id: 0,
|
||||
status: 1,
|
||||
sort: 0,
|
||||
color: '',
|
||||
icon: '',
|
||||
remark: ''
|
||||
})
|
||||
itemDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEditItem = (row) => {
|
||||
Object.assign(itemForm, {
|
||||
id: row.id,
|
||||
dict_type_id: row.dict_type_id,
|
||||
dict_label: row.dict_label,
|
||||
dict_value: row.dict_value,
|
||||
parent_id: row.parent_id || 0,
|
||||
status: row.status,
|
||||
sort: row.sort || 0,
|
||||
color: row.color || '',
|
||||
icon: row.icon || '',
|
||||
remark: row.remark || ''
|
||||
})
|
||||
itemDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleDeleteItem = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该字典项吗?', '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
const res = await deleteDictItem(row.id)
|
||||
if (res.success) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchDictItems(currentDictType.value.id)
|
||||
} else {
|
||||
ElMessage.error(res.message || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('删除失败: ' + error.message)
|
||||
} catch (error: any) {
|
||||
ElMessage.error(error.message || '删除失败')
|
||||
} finally {
|
||||
typeLoading.value = false
|
||||
}
|
||||
} catch {
|
||||
// 取消删除
|
||||
}
|
||||
}
|
||||
|
||||
const handleItemSubmit = async () => {
|
||||
if (!itemFormRef.value) return
|
||||
await itemFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
try {
|
||||
let res
|
||||
if (itemForm.id > 0) {
|
||||
res = await updateDictItem(itemForm.id, itemForm)
|
||||
} else {
|
||||
res = await addDictItem(itemForm)
|
||||
}
|
||||
if (res.success) {
|
||||
ElMessage.success(itemForm.id > 0 ? '更新成功' : '添加成功')
|
||||
itemDialogVisible.value = false
|
||||
fetchDictItems(currentDictType.value.id)
|
||||
} else {
|
||||
ElMessage.error(res.message || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败: ' + error.message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleItemDialogClose = () => {
|
||||
itemFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
// 字典类型操作成功回调
|
||||
function handleTypeSuccess() {
|
||||
fetchDictTypes()
|
||||
if (currentDictType.value) {
|
||||
fetchDictItems(currentDictType.value.id)
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
// 从字典项页返回
|
||||
function goBackToTypes() {
|
||||
currentView.value = 'types'
|
||||
}
|
||||
|
||||
// 子页面刷新后,静默刷新类型列表
|
||||
function refreshTypesSilently() {
|
||||
fetchDictTypes()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
async function refresh() {
|
||||
await fetchDictTypes()
|
||||
ElMessage.success('刷新成功')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDictTypes()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.dict-container {
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dict-content {
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.dict-type-panel,
|
||||
.dict-item-panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
background: #fff;
|
||||
.tab-content {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
.filter-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
.color-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.color-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #e5e5e5;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -32,8 +32,8 @@
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="tenantData.status === 'enabled' ? 'success' : 'info'">
|
||||
{{ tenantData.status === 'enabled' ? '启用' : '禁用' }}
|
||||
<el-tag :type="tenantData.status === 1 ? 'success' : 'info'">
|
||||
{{ tenantData.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="存储容量">
|
||||
|
||||
@ -29,8 +29,8 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="tenantForm.status" placeholder="请选择状态" style="width: 100%">
|
||||
<el-option label="启用" value="enabled" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="存储容量" prop="capacity">
|
||||
@ -103,7 +103,7 @@ const tenantForm = reactive({
|
||||
owner: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
status: 'enabled',
|
||||
status: 1,
|
||||
capacity: 0,
|
||||
remark: '',
|
||||
});
|
||||
@ -131,7 +131,7 @@ function resetForm() {
|
||||
tenantForm.owner = '';
|
||||
tenantForm.phone = '';
|
||||
tenantForm.email = '';
|
||||
tenantForm.status = 'enabled';
|
||||
tenantForm.status = 1;
|
||||
tenantForm.capacity = 0;
|
||||
tenantForm.remark = '';
|
||||
tenantFormRef.value?.resetFields();
|
||||
@ -146,7 +146,7 @@ function initFormData() {
|
||||
tenantForm.owner = props.tenant.owner || '';
|
||||
tenantForm.phone = props.tenant.phone || '';
|
||||
tenantForm.email = props.tenant.email || '';
|
||||
tenantForm.status = props.tenant.status || 'enabled';
|
||||
tenantForm.status = props.tenant.status != null ? Number(props.tenant.status) : 1;
|
||||
// capacity 后端返回的是 MB,直接使用
|
||||
tenantForm.capacity = props.tenant.capacity != null ? Number(props.tenant.capacity) : 0;
|
||||
tenantForm.remark = props.tenant.remark || '';
|
||||
@ -168,7 +168,7 @@ async function handleSubmit() {
|
||||
owner: tenantForm.owner,
|
||||
phone: tenantForm.phone || '',
|
||||
email: tenantForm.email || '',
|
||||
status: tenantForm.status,
|
||||
status: Number(tenantForm.status),
|
||||
// capacity 前后端都使用 MB
|
||||
capacity: tenantForm.capacity || 0,
|
||||
remark: tenantForm.remark || '',
|
||||
|
||||
@ -70,8 +70,8 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'enabled' ? 'success' : 'info'">
|
||||
{{ row.status === 'enabled' ? '启用' : '禁用' }}
|
||||
<el-tag :type="row.status == 1 ? 'success' : 'info'">
|
||||
{{ row.status == 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
210
pc/src/views/system/users/components/ChangePassword.vue
Normal file
210
pc/src/views/system/users/components/ChangePassword.vue
Normal file
@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="修改密码" width="400px" @close="handleClose">
|
||||
<el-form :model="form">
|
||||
<!-- 旧密码 -->
|
||||
<el-form-item label="旧密码">
|
||||
<el-input
|
||||
v-model="passwordForm.oldPassword"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 新密码 -->
|
||||
<el-form-item label="新密码">
|
||||
<el-input
|
||||
v-model="passwordForm.newPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<el-form-item label="确认密码">
|
||||
<el-input
|
||||
v-model="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<el-form-item v-if="passwordError">
|
||||
<el-alert :title="passwordError" type="error" :closable="false" style="color: #f56c6c;" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 对话框脚部 -->
|
||||
<template #footer>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">修改</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { changePassword } from "@/api/user";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
userId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'submit', 'close']);
|
||||
|
||||
const visible = ref(false);
|
||||
const passwordError = ref("");
|
||||
|
||||
const form = ref<any>({
|
||||
id: null,
|
||||
username: "",
|
||||
});
|
||||
|
||||
const passwordForm = ref<any>({
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
// 监听 modelValue
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal;
|
||||
if (newVal && props.userId) {
|
||||
form.value.id = props.userId;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 userId 变化
|
||||
watch(() => props.userId, (newVal) => {
|
||||
if (newVal) {
|
||||
form.value.id = newVal;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
if (!newVal) {
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
});
|
||||
|
||||
// 校验密码
|
||||
const validatePassword = (password: string) => {
|
||||
if (!password) {
|
||||
return "请输入密码";
|
||||
}
|
||||
if (password.length < 6) {
|
||||
return "密码长度不能小于6位";
|
||||
}
|
||||
if (password.length > 16) {
|
||||
return "密码长度不能大于16位";
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 校验确认密码
|
||||
const validateConfirmPassword = (password: string, confirmPassword: string) => {
|
||||
if (!confirmPassword) {
|
||||
return "请再次输入密码";
|
||||
}
|
||||
if (confirmPassword !== password) {
|
||||
return "两次输入的密码不一致";
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
passwordForm.value = {
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
passwordError.value = "";
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 清除之前的错误
|
||||
passwordError.value = "";
|
||||
|
||||
try {
|
||||
if (!form.value.id) {
|
||||
passwordError.value = "用户ID不能为空";
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验旧密码
|
||||
if (!passwordForm.value.oldPassword) {
|
||||
passwordError.value = "请输入旧密码";
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验新密码格式
|
||||
const passwordCheck = validatePassword(passwordForm.value.newPassword);
|
||||
if (passwordCheck !== true) {
|
||||
passwordError.value = passwordCheck;
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验确认密码
|
||||
const confirmCheck = validateConfirmPassword(
|
||||
passwordForm.value.newPassword,
|
||||
passwordForm.value.confirmPassword
|
||||
);
|
||||
if (confirmCheck !== true) {
|
||||
passwordError.value = confirmCheck;
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用接口修改密码
|
||||
const res = await changePassword(form.value.id, passwordForm.value);
|
||||
|
||||
if (res.code === 0 || res.message === '修改成功') {
|
||||
ElMessage.success("密码修改成功");
|
||||
visible.value = false;
|
||||
emit('update:modelValue', false);
|
||||
emit('submit');
|
||||
} else {
|
||||
passwordError.value = res.message || "密码修改失败";
|
||||
}
|
||||
} catch (e: any) {
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "操作失败";
|
||||
passwordError.value = errorMsg;
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
open: (userId: number, username: string) => {
|
||||
form.value = {
|
||||
id: userId,
|
||||
username: username,
|
||||
};
|
||||
passwordForm.value = {
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
passwordError.value = "";
|
||||
visible.value = true;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
402
pc/src/views/system/users/components/UserEdit.vue
Normal file
402
pc/src/views/system/users/components/UserEdit.vue
Normal file
@ -0,0 +1,402 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="dialogTitle" width="500px" @close="handleClose">
|
||||
<el-form :model="form" ref="formRef">
|
||||
<!-- 用户名 -->
|
||||
<el-form-item label="用户名">
|
||||
<el-input v-model="form.username" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 昵称 -->
|
||||
<el-form-item label="昵称">
|
||||
<el-input v-model="form.nickname" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 密码(添加时必填) -->
|
||||
<el-form-item label="密码" v-if="isAdd">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 邮箱 -->
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="form.email" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 部门 -->
|
||||
<el-form-item label="部门">
|
||||
<el-select
|
||||
v-model="form.department_id"
|
||||
placeholder="请选择部门"
|
||||
style="width: 100%"
|
||||
:loading="loadingDepartments"
|
||||
clearable
|
||||
@change="handleDepartmentChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="dept in departmentList"
|
||||
:key="dept.id"
|
||||
:label="dept.name"
|
||||
:value="dept.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 职位 -->
|
||||
<el-form-item label="职位">
|
||||
<el-select
|
||||
v-model="form.position_id"
|
||||
placeholder="请选择职位"
|
||||
style="width: 100%"
|
||||
:loading="loadingPositions"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="pos in positionList"
|
||||
:key="pos.id"
|
||||
:label="pos.name"
|
||||
:value="pos.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 角色 -->
|
||||
<el-form-item label="角色">
|
||||
<el-select
|
||||
v-model="form.role"
|
||||
placeholder="请选择角色"
|
||||
style="width: 100%"
|
||||
:loading="loadingRoles"
|
||||
>
|
||||
<el-option
|
||||
v-for="role in roleList"
|
||||
:key="role.roleId"
|
||||
:label="role.roleName"
|
||||
:value="role.roleId"
|
||||
>
|
||||
<span>{{ role.roleName }}</span>
|
||||
<span style="color: #8492a6; font-size: 13px; margin-left: 8px;">
|
||||
({{ role.roleCode }})
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 状态 -->
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status">
|
||||
<el-option
|
||||
v-for="item in statusDict"
|
||||
:key="item.dict_value"
|
||||
:label="item.dict_label"
|
||||
:value="item.dict_value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 对话框脚部 -->
|
||||
<template #footer>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import {
|
||||
addUser,
|
||||
editUser,
|
||||
getUserInfo,
|
||||
} from "@/api/user";
|
||||
import { getTenantPositions, getPositionsByDepartment } from "@/api/position";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
roleList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
departmentList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
positionList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
loadingRoles: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loadingDepartments: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
loadingPositions: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
statusDict: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
tenantId: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'submit', 'close', 'fetch-positions']);
|
||||
|
||||
const visible = ref(false);
|
||||
const formRef = ref(null);
|
||||
const loadingRoles = ref(false);
|
||||
const loadingDepartments = ref(false);
|
||||
const loadingPositions = ref(false);
|
||||
const isAdd = ref(false);
|
||||
|
||||
const form = ref<any>({
|
||||
id: null,
|
||||
username: "",
|
||||
nickname: "",
|
||||
password: "",
|
||||
email: "",
|
||||
role: null,
|
||||
status: "active",
|
||||
tenant_id: null,
|
||||
department_id: null,
|
||||
position_id: null,
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
return isAdd.value ? "添加用户" : "编辑用户";
|
||||
});
|
||||
|
||||
// 监听 modelValue
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal;
|
||||
});
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
if (!newVal) {
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
});
|
||||
|
||||
const getCurrentTenantId = () => {
|
||||
return props.tenantId || 0;
|
||||
};
|
||||
|
||||
const loadUserData = async (user: any) => {
|
||||
try {
|
||||
// 处理两种调用方式:传递用户对象或用户 ID
|
||||
const userId = typeof user === 'number' ? user : (user?.id || user?.userId);
|
||||
if (!userId) {
|
||||
throw new Error('未提供有效的用户 ID');
|
||||
}
|
||||
|
||||
console.log('Loading user data for ID:', userId);
|
||||
const res = await getUserInfo(userId);
|
||||
console.log('User data response:', res);
|
||||
|
||||
const data = res.data || res;
|
||||
|
||||
const tenantId = getCurrentTenantId();
|
||||
let roleValue = data.role || null;
|
||||
|
||||
let statusStr = "active";
|
||||
if (typeof data.status === 'number') {
|
||||
statusStr = data.status === 1 ? "active" : "inactive";
|
||||
} else if (typeof data.status === 'string') {
|
||||
statusStr = data.status;
|
||||
}
|
||||
|
||||
form.value = {
|
||||
id: data.id,
|
||||
username: data.username,
|
||||
nickname: data.nickname,
|
||||
password: "",
|
||||
email: data.email,
|
||||
role: roleValue,
|
||||
department_id: data.department_id || null,
|
||||
position_id: data.position_id || null,
|
||||
status: statusStr,
|
||||
tenant_id: data.tenant_id || tenantId,
|
||||
};
|
||||
|
||||
// 如果用户有部门,根据部门加载职位
|
||||
if (form.value.department_id) {
|
||||
await loadPositions(form.value.department_id);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Failed to load user data:', e);
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
|
||||
ElMessage.error(errorMsg);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const loadPositions = async (departmentId?: number) => {
|
||||
loadingPositions.value = true;
|
||||
try {
|
||||
const tenantId = getCurrentTenantId();
|
||||
let res;
|
||||
if (departmentId && departmentId > 0) {
|
||||
res = await getPositionsByDepartment(departmentId);
|
||||
} else {
|
||||
res = await getTenantPositions(tenantId);
|
||||
}
|
||||
// 职位列表由父组件通过 prop 传入,这里仅用于加载示例
|
||||
} catch (error: any) {
|
||||
console.error('获取职位列表失败:', error);
|
||||
} finally {
|
||||
loadingPositions.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDepartmentChange = (departmentId: number | null) => {
|
||||
form.value.position_id = null;
|
||||
if (departmentId && departmentId > 0) {
|
||||
// 让父组件加载对应部门的职位
|
||||
emit('fetch-positions', departmentId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
form.value = {
|
||||
id: null,
|
||||
username: "",
|
||||
nickname: "",
|
||||
password: "",
|
||||
email: "",
|
||||
role: null,
|
||||
status: "active",
|
||||
tenant_id: null,
|
||||
department_id: null,
|
||||
position_id: null,
|
||||
};
|
||||
isAdd.value = false;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (isAdd.value) {
|
||||
// 新增用户
|
||||
const submitData: any = {
|
||||
username: form.value.username,
|
||||
nickname: form.value.nickname,
|
||||
password: form.value.password,
|
||||
email: form.value.email,
|
||||
status: form.value.status,
|
||||
};
|
||||
|
||||
if (form.value.role) {
|
||||
submitData.role = form.value.role;
|
||||
}
|
||||
|
||||
if (form.value.department_id) {
|
||||
submitData.department_id = form.value.department_id;
|
||||
}
|
||||
if (form.value.position_id) {
|
||||
submitData.position_id = form.value.position_id;
|
||||
}
|
||||
|
||||
if (form.value.tenant_id) {
|
||||
submitData.tenant_id = form.value.tenant_id;
|
||||
}
|
||||
|
||||
await addUser(submitData);
|
||||
ElMessage.success("添加成功");
|
||||
} else {
|
||||
// 编辑用户
|
||||
if (!form.value.id || form.value.id === 0) {
|
||||
ElMessage.error("用户ID不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
const submitData: any = {
|
||||
id: form.value.id,
|
||||
username: form.value.username,
|
||||
nickname: form.value.nickname,
|
||||
email: form.value.email,
|
||||
status: form.value.status,
|
||||
};
|
||||
|
||||
if (form.value.role) {
|
||||
submitData.role = form.value.role;
|
||||
}
|
||||
|
||||
if (form.value.department_id) {
|
||||
submitData.department_id = form.value.department_id;
|
||||
}
|
||||
if (form.value.position_id) {
|
||||
submitData.position_id = form.value.position_id;
|
||||
}
|
||||
|
||||
if (form.value.tenant_id) {
|
||||
submitData.tenant_id = form.value.tenant_id;
|
||||
}
|
||||
|
||||
await editUser(form.value.id, submitData);
|
||||
ElMessage.success("更新成功");
|
||||
}
|
||||
|
||||
visible.value = false;
|
||||
emit('submit');
|
||||
} catch (e: any) {
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "操作失败";
|
||||
ElMessage.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
loadUserData,
|
||||
openAdd: (tenantId?: number) => {
|
||||
isAdd.value = true;
|
||||
form.value = {
|
||||
id: 0,
|
||||
username: "",
|
||||
nickname: "",
|
||||
password: "",
|
||||
email: "",
|
||||
role: null,
|
||||
status: "active",
|
||||
tenant_id: tenantId || getCurrentTenantId(),
|
||||
department_id: null,
|
||||
position_id: null,
|
||||
};
|
||||
visible.value = true;
|
||||
},
|
||||
openEdit: (user: any) => {
|
||||
isAdd.value = false;
|
||||
visible.value = true;
|
||||
// 异步加载用户详细信息
|
||||
loadUserData(user);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
358
pc/src/views/system/users/components/UserList.vue
Normal file
358
pc/src/views/system/users/components/UserList.vue
Normal file
@ -0,0 +1,358 @@
|
||||
<template>
|
||||
<div class="user-list-container">
|
||||
<!-- 列表操作栏 -->
|
||||
<div class="list-header">
|
||||
<el-button type="primary" @click="handleAddUser">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加用户
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 用户列表表格 -->
|
||||
<el-table :data="users" style="width: 100%" v-loading="loading">
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="用户名"
|
||||
width="150"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="nickname"
|
||||
label="昵称"
|
||||
width="150"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="email"
|
||||
label="邮箱"
|
||||
align="center"
|
||||
min-width="200"
|
||||
/>
|
||||
<el-table-column prop="department" label="部门" width="150" align="center">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.departmentName || '未分配' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="position" label="职位" width="150" align="center">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.positionName || '未分配' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="role" label="角色" width="150" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getRoleTagType(scope.row.roleName)">
|
||||
{{ scope.row.roleName || '未分配角色' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusTagType(scope.row.status)">
|
||||
{{ getStatusLabel(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="lastLoginTime"
|
||||
label="最后登录"
|
||||
width="180"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="lastLoginIp"
|
||||
label="最后登录IP"
|
||||
width="150"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.lastLoginIp || '未知' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleEdit(scope.row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button size="small" type="warning" @click="handleChangePassword(scope.row)">
|
||||
修改密码
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="scope.row.username !== 'admin'"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
background
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
layout="total, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getTenantUsers,
|
||||
deleteUser,
|
||||
getUserInfo,
|
||||
} from "@/api/user";
|
||||
import { getRoleByTenantId, getAllRoles } from "@/api/role";
|
||||
import { getTenantDepartments } from "@/api/department";
|
||||
import { getTenantPositions } from "@/api/position";
|
||||
import { getDictItemsByCode } from '@/api/dict'
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
role: string;
|
||||
status: string;
|
||||
lastLogin: string;
|
||||
tenant_id: number;
|
||||
}
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// 声明 props 和 emit
|
||||
const props = defineProps({
|
||||
roleList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
departmentList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
positionList: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
statusDict: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['add-user', 'edit-user', 'change-password', 'refresh', 'delete-user']);
|
||||
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const users = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 获取当前登录用户的租户ID
|
||||
const getCurrentTenantId = () => {
|
||||
if (authStore.user && authStore.user.tenant_id) {
|
||||
return authStore.user.tenant_id;
|
||||
}
|
||||
const userInfo = localStorage.getItem('userInfo');
|
||||
if (userInfo) {
|
||||
try {
|
||||
const user = JSON.parse(userInfo);
|
||||
return user.tenant_id || user.tenantId || 0;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse user info:', e);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
// 获取角色标签类型
|
||||
const getRoleTagType = (roleName: string) => {
|
||||
if (!roleName) return 'info';
|
||||
if (roleName.includes('管理员')) return 'danger';
|
||||
if (roleName.includes('用户')) return 'primary';
|
||||
return 'success';
|
||||
};
|
||||
|
||||
// 根据状态值获取字典显示文本
|
||||
const getStatusLabel = (status: any) => {
|
||||
const sval = status !== undefined && status !== null ? String(status) : '';
|
||||
const item = props.statusDict.find((d: any) => String(d.dict_value) === sval || d.dict_value === status);
|
||||
if (item && item.dict_label) return item.dict_label;
|
||||
// 兼容旧逻辑:数字 1 表示启用
|
||||
if (status === 1 || sval === '1' || sval === 'active') return '启用';
|
||||
return '禁用';
|
||||
}
|
||||
|
||||
// 根据状态字典或文本返回 el-tag 的 type
|
||||
const getStatusTagType = (status: any) => {
|
||||
const label = getStatusLabel(status);
|
||||
if (!label) return 'info';
|
||||
const l = label.toString();
|
||||
if (l.includes('启用') || l.includes('正常') || l.includes('active')) return 'success';
|
||||
if (l.includes('禁用') || l.includes('停用') || l.includes('inactive')) return 'danger';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
const fetchUsers = async () => {
|
||||
loading.value = true;
|
||||
const tenantId = getCurrentTenantId();
|
||||
try {
|
||||
const res = await getTenantUsers(tenantId);
|
||||
// 兼容接口返回的数据结构
|
||||
let userList: any[] = [];
|
||||
if (Array.isArray(res)) {
|
||||
userList = res;
|
||||
} else if (res?.data && Array.isArray(res.data)) {
|
||||
userList = res.data;
|
||||
} else if (res?.data?.data && Array.isArray(res.data.data)) {
|
||||
userList = res.data.data;
|
||||
} else if (res?.data) {
|
||||
userList = res.data;
|
||||
}
|
||||
|
||||
// 映射接口字段到表格所需结构
|
||||
users.value = userList.map((item: any) => {
|
||||
// 查找角色名称
|
||||
let roleName = '';
|
||||
let roleValue = item.role || null;
|
||||
|
||||
if (roleValue) {
|
||||
const roleInfo = (props.roleList as any[]).find(r => r.roleId === roleValue);
|
||||
roleName = roleInfo ? roleInfo.roleName : '';
|
||||
}
|
||||
|
||||
// 查找部门名称
|
||||
let departmentName = '';
|
||||
const departmentId = item.department_id || null;
|
||||
if (departmentId) {
|
||||
const deptInfo = (props.departmentList as any[]).find(d => d.id === departmentId);
|
||||
departmentName = deptInfo ? deptInfo.name : '';
|
||||
}
|
||||
|
||||
// 查找职位名称
|
||||
let positionName = '';
|
||||
const positionId = item.position_id || null;
|
||||
if (positionId) {
|
||||
const posInfo = (props.positionList as any[]).find(p => p.id === positionId);
|
||||
positionName = posInfo ? posInfo.name : '';
|
||||
}
|
||||
|
||||
// 处理时间字段
|
||||
const lastLoginTime = item.last_login_time || item.lastLoginTime || null;
|
||||
const lastLoginIp = item.last_login_ip || item.lastLoginIp || null;
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
username: item.username,
|
||||
nickname: item.nickname,
|
||||
email: item.email,
|
||||
role: roleValue,
|
||||
roleName: roleName,
|
||||
department_id: departmentId,
|
||||
departmentName: departmentName,
|
||||
position_id: positionId,
|
||||
positionName: positionName,
|
||||
status: item.status,
|
||||
lastLoginTime: lastLoginTime
|
||||
? new Date(lastLoginTime).toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
: "从未登录",
|
||||
lastLoginIp: lastLoginIp || null,
|
||||
};
|
||||
});
|
||||
total.value = users.value.length;
|
||||
} catch (e) {
|
||||
users.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageChange = (p: number) => {
|
||||
page.value = p;
|
||||
};
|
||||
|
||||
const handleAddUser = () => {
|
||||
emit('add-user');
|
||||
};
|
||||
|
||||
const handleEdit = (user: User) => {
|
||||
emit('edit-user', user);
|
||||
};
|
||||
|
||||
const handleChangePassword = (user: User) => {
|
||||
emit('change-password', user);
|
||||
};
|
||||
|
||||
const handleDelete = (user: User) => {
|
||||
ElMessageBox.confirm("确认删除该用户?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteUser(user.id);
|
||||
ElMessage.success("删除成功");
|
||||
emit('delete-user', user);
|
||||
await fetchUsers();
|
||||
} catch (e) {
|
||||
ElMessage.error("删除失败");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
await fetchUsers();
|
||||
emit('refresh');
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchUsers();
|
||||
});
|
||||
|
||||
// 暴露 fetchUsers 方法给父组件
|
||||
defineExpose({
|
||||
fetchUsers,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.user-list-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pagination-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -7,15 +7,16 @@
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加用户
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 用户列表表格 -->
|
||||
<el-table :data="users" style="width: 100%" v-loading="loading">
|
||||
<el-table-column
|
||||
prop="username"
|
||||
@ -52,10 +53,10 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<el-table-column prop="status" label="状态" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? "启用" : "禁用" }}
|
||||
<el-tag :type="getStatusTagType(scope.row.status)">
|
||||
{{ getStatusLabel(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@ -93,6 +94,8 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
background
|
||||
@ -104,137 +107,52 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Dialog for add/edit -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
|
||||
<el-form :model="currentForm">
|
||||
<el-form-item label="用户名" v-show="dialogTitle !== '修改密码'">
|
||||
<el-input v-model="form.username" />
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称" v-if="dialogTitle !== '修改密码'">
|
||||
<el-input v-model="form.nickname" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" v-if="dialogTitle === '添加用户'">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="旧密码" v-if="dialogTitle === '修改密码'">
|
||||
<el-input
|
||||
v-model="passwordForm.oldPassword"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码" v-if="dialogTitle === '修改密码'">
|
||||
<el-input
|
||||
v-model="passwordForm.newPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" v-if="dialogTitle === '修改密码'">
|
||||
<el-input
|
||||
v-model="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="dialogTitle === '修改密码' && passwordError">
|
||||
<el-alert :title="passwordError" type="error" :closable="false" style="color: #f56c6c;" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" v-if="dialogTitle !== '修改密码'">
|
||||
<el-input v-model="form.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" v-if="dialogTitle !== '修改密码'">
|
||||
<el-select
|
||||
v-model="form.department_id"
|
||||
placeholder="请选择部门"
|
||||
style="width: 100%"
|
||||
:loading="loadingDepartments"
|
||||
clearable
|
||||
@change="handleDepartmentChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="dept in departmentList"
|
||||
:key="dept.id"
|
||||
:label="dept.name"
|
||||
:value="dept.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="职位" v-if="dialogTitle !== '修改密码'">
|
||||
<el-select
|
||||
v-model="form.position_id"
|
||||
placeholder="请选择职位"
|
||||
style="width: 100%"
|
||||
:loading="loadingPositions"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="pos in positionList"
|
||||
:key="pos.id"
|
||||
:label="pos.name"
|
||||
:value="pos.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色" v-if="dialogTitle !== '修改密码'">
|
||||
<el-select
|
||||
v-model="form.role"
|
||||
placeholder="请选择角色"
|
||||
style="width: 100%"
|
||||
:loading="loadingRoles"
|
||||
>
|
||||
<el-option
|
||||
v-for="role in roleList"
|
||||
:key="role.roleId"
|
||||
:label="role.roleName"
|
||||
:value="role.roleId"
|
||||
>
|
||||
<span>{{ role.roleName }}</span>
|
||||
<span style="color: #8492a6; font-size: 13px; margin-left: 8px;">
|
||||
({{ role.roleCode }})
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" v-if="dialogTitle !== '修改密码'">
|
||||
<el-select v-model="form.status">
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitForm">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 编辑用户对话框组件 -->
|
||||
<UserEditDialog
|
||||
ref="userEditRef"
|
||||
:modelValue="editDialogVisible"
|
||||
@update:modelValue="editDialogVisible = $event"
|
||||
:is-edit="isEdit"
|
||||
:role-list="roleList"
|
||||
:department-list="departmentList"
|
||||
:position-list="positionList"
|
||||
:loading-roles="loadingRoles"
|
||||
:loading-departments="loadingDepartments"
|
||||
:loading-positions="loadingPositions"
|
||||
:status-dict="statusDict"
|
||||
:tenant-id="getCurrentTenantId()"
|
||||
@submit="handleEditSuccess"
|
||||
@close="editDialogVisible = false"
|
||||
/>
|
||||
|
||||
<!-- 修改密码对话框组件 -->
|
||||
<ChangePasswordDialog
|
||||
ref="changePasswordRef"
|
||||
:modelValue="passwordDialogVisible"
|
||||
@update:modelValue="passwordDialogVisible = $event"
|
||||
:user-id="currentUserId"
|
||||
@submit="handlePasswordChangeSuccess"
|
||||
@close="passwordDialogVisible = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getAllUsers,
|
||||
getTenantUsers,
|
||||
addUser,
|
||||
editUser,
|
||||
deleteUser,
|
||||
getUserInfo,
|
||||
changePassword,
|
||||
} from "@/api/user";
|
||||
import { getRoleByTenantId, getAllRoles } from "@/api/role";
|
||||
import { getTenantDepartments } from "@/api/department";
|
||||
import { getTenantPositions, getPositionsByDepartment } from "@/api/position";
|
||||
import { getDictItemsByCode } from '@/api/dict'
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import UserEditDialog from './components/UserEdit.vue'
|
||||
import ChangePasswordDialog from './components/ChangePassword.vue'
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
@ -262,6 +180,32 @@ const positionList = ref<any[]>([]);
|
||||
const loadingPositions = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
// 状态字典
|
||||
const statusDict = ref<any[]>([]);
|
||||
|
||||
// 编辑/密码对话框状态
|
||||
const editDialogVisible = ref(false);
|
||||
const passwordDialogVisible = ref(false);
|
||||
const editDialogTitle = ref("添加用户");
|
||||
const isEdit = ref(false);
|
||||
const currentUserId = ref<number | undefined>(undefined);
|
||||
|
||||
// 子组件 ref
|
||||
const userEditRef = ref()
|
||||
const changePasswordRef = ref()
|
||||
|
||||
const fetchStatusDict = async () => {
|
||||
try {
|
||||
const res = await getDictItemsByCode('user_status');
|
||||
let items: any[] = [];
|
||||
if (res?.data && Array.isArray(res.data)) items = res.data;
|
||||
else if (Array.isArray(res)) items = res;
|
||||
statusDict.value = items;
|
||||
} catch (err) {
|
||||
statusDict.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前登录用户的租户ID
|
||||
const getCurrentTenantId = () => {
|
||||
if (authStore.user && authStore.user.tenant_id) {
|
||||
@ -300,8 +244,6 @@ const isPlatformUser = () => {
|
||||
const fetchRoles = async () => {
|
||||
loadingRoles.value = true;
|
||||
try {
|
||||
// 如果是平台用户,使用 getAllRoles 获取所有角色(包括平台角色和租户角色)
|
||||
// 如果是租户员工,使用 getRoleByTenantId 获取当前租户的角色
|
||||
let res;
|
||||
if (isPlatformUser()) {
|
||||
res = await getAllRoles();
|
||||
@ -349,10 +291,8 @@ const fetchPositions = async (departmentId?: number) => {
|
||||
const tenantId = getCurrentTenantId();
|
||||
let res;
|
||||
if (departmentId && departmentId > 0) {
|
||||
// 根据部门获取职位
|
||||
res = await getPositionsByDepartment(departmentId);
|
||||
} else {
|
||||
// 获取所有职位
|
||||
res = await getTenantPositions(tenantId);
|
||||
}
|
||||
if (res.code === 0 && res.data) {
|
||||
@ -368,16 +308,6 @@ const fetchPositions = async (departmentId?: number) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 部门改变时,重新加载职位列表
|
||||
const handleDepartmentChange = (departmentId: number | null) => {
|
||||
form.value.position_id = null; // 清空职位选择
|
||||
if (departmentId && departmentId > 0) {
|
||||
fetchPositions(departmentId);
|
||||
} else {
|
||||
fetchPositions(); // 加载所有职位
|
||||
}
|
||||
};
|
||||
|
||||
// 获取角色标签类型
|
||||
const getRoleTagType = (roleName: string) => {
|
||||
if (!roleName) return 'info';
|
||||
@ -386,38 +316,31 @@ const getRoleTagType = (roleName: string) => {
|
||||
return 'success';
|
||||
};
|
||||
|
||||
//校验密码
|
||||
const validatePassword = (password: string) => {
|
||||
if (!password) {
|
||||
return "请输入密码";
|
||||
}
|
||||
if (password.length < 6) {
|
||||
return "密码长度不能小于6位";
|
||||
}
|
||||
if (password.length > 16) {
|
||||
return "密码长度不能大于16位";
|
||||
}
|
||||
return true;
|
||||
};
|
||||
// 根据状态值获取字典显示文本
|
||||
const getStatusLabel = (status: any) => {
|
||||
const sval = status !== undefined && status !== null ? String(status) : '';
|
||||
const item = statusDict.value.find((d: any) => String(d.dict_value) === sval || d.dict_value === status);
|
||||
if (item && item.dict_label) return item.dict_label;
|
||||
// 兼容旧逻辑
|
||||
if (status === 1 || sval === '1' || sval === 'active') return '启用';
|
||||
return '禁用';
|
||||
}
|
||||
|
||||
// 校验确认密码,需传递新密码和确认密码
|
||||
const validateConfirmPassword = (password: string, confirmPassword: string) => {
|
||||
if (!confirmPassword) {
|
||||
return "请再次输入密码";
|
||||
}
|
||||
if (confirmPassword !== password) {
|
||||
return "两次输入的密码不一致";
|
||||
}
|
||||
return true;
|
||||
};
|
||||
// 根据状态字典返回 el-tag 的 type
|
||||
const getStatusTagType = (status: any) => {
|
||||
const label = getStatusLabel(status);
|
||||
if (!label) return 'info';
|
||||
const l = label.toString();
|
||||
if (l.includes('启用') || l.includes('正常') || l.includes('active')) return 'success';
|
||||
if (l.includes('禁用') || l.includes('停用') || l.includes('inactive')) return 'danger';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
const fetchUsers = async () => {
|
||||
loading.value = true;
|
||||
// 传tenantid给接口
|
||||
let tenantId = getCurrentTenantId ? getCurrentTenantId() : null;
|
||||
try {
|
||||
const res = await getTenantUsers(tenantId);
|
||||
// 兼容接口返回的数据结构
|
||||
let userList: any[] = [];
|
||||
if (Array.isArray(res)) {
|
||||
userList = res;
|
||||
@ -428,19 +351,16 @@ const fetchUsers = async () => {
|
||||
} else if (res?.data) {
|
||||
userList = res.data;
|
||||
}
|
||||
// 映射接口字段到表格所需结构
|
||||
|
||||
users.value = userList.map((item: any) => {
|
||||
// 查找角色名称
|
||||
let roleName = '';
|
||||
let roleValue = item.role || null; // role 字段存储的是角色ID(数字)
|
||||
let roleValue = item.role || null;
|
||||
|
||||
if (roleValue) {
|
||||
// 通过角色ID查找角色信息
|
||||
const roleInfo = roleList.value.find(r => r.roleId === roleValue);
|
||||
roleName = roleInfo ? roleInfo.roleName : '';
|
||||
}
|
||||
|
||||
// 查找部门名称
|
||||
let departmentName = '';
|
||||
const departmentId = item.department_id || null;
|
||||
if (departmentId) {
|
||||
@ -448,7 +368,6 @@ const fetchUsers = async () => {
|
||||
departmentName = deptInfo ? deptInfo.name : '';
|
||||
}
|
||||
|
||||
// 查找职位名称
|
||||
let positionName = '';
|
||||
const positionId = item.position_id || null;
|
||||
if (positionId) {
|
||||
@ -456,9 +375,7 @@ const fetchUsers = async () => {
|
||||
positionName = posInfo ? posInfo.name : '';
|
||||
}
|
||||
|
||||
// 处理最后登录时间,后端返回的是 last_login_time
|
||||
const lastLoginTime = item.last_login_time || item.lastLoginTime || null;
|
||||
// 处理最后登录IP,后端返回的是 last_login_ip
|
||||
const lastLoginIp = item.last_login_ip || item.lastLoginIp || null;
|
||||
|
||||
return {
|
||||
@ -497,48 +414,19 @@ const fetchUsers = async () => {
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// 先加载角色、部门、职位列表,再加载用户列表
|
||||
await Promise.all([
|
||||
fetchRoles(),
|
||||
fetchDepartments(),
|
||||
fetchPositions(),
|
||||
fetchStatusDict(),
|
||||
]);
|
||||
fetchUsers();
|
||||
});
|
||||
|
||||
const handlePageChange = (p: number) => {
|
||||
page.value = p;
|
||||
// 分页仅前端做演示
|
||||
};
|
||||
|
||||
// 为添加 / 编辑对话框而添加
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref("");
|
||||
const isEdit = ref(false);
|
||||
const form = ref<any>({
|
||||
id: null,
|
||||
username: "",
|
||||
nickname: "",
|
||||
password: "",
|
||||
email: "",
|
||||
role: null, // role 字段存储角色ID
|
||||
status: "active",
|
||||
tenant_id: null,
|
||||
});
|
||||
const passwordForm = ref<any>({
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
const passwordError = ref("");
|
||||
|
||||
// 根据对话框类型返回对应的表单数据
|
||||
const currentForm = computed(() => {
|
||||
return dialogTitle.value === '修改密码' ? passwordForm.value : form.value;
|
||||
});
|
||||
|
||||
//刷新界面
|
||||
const refresh = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
@ -546,6 +434,7 @@ const refresh = async () => {
|
||||
fetchRoles(),
|
||||
fetchDepartments(),
|
||||
fetchPositions(),
|
||||
fetchStatusDict(),
|
||||
]);
|
||||
await fetchUsers();
|
||||
ElMessage.success('刷新成功');
|
||||
@ -556,237 +445,51 @@ const refresh = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 添加用户
|
||||
const handleAddUser = () => {
|
||||
dialogTitle.value = "添加用户";
|
||||
editDialogTitle.value = "添加用户";
|
||||
isEdit.value = false;
|
||||
|
||||
// 从本地缓存读取tenant_id
|
||||
let tenantId = null;
|
||||
const cachedUser = localStorage.getItem("userInfo");
|
||||
if (cachedUser) {
|
||||
try {
|
||||
const userInfo = JSON.parse(cachedUser);
|
||||
tenantId = userInfo.tenant_id || null;
|
||||
} catch (e) {
|
||||
tenantId = null;
|
||||
}
|
||||
// 清空编辑对话框的数据,让子组件处理
|
||||
if (userEditRef.value) {
|
||||
userEditRef.value.openAdd(getCurrentTenantId());
|
||||
}
|
||||
|
||||
form.value = {
|
||||
id: 0, // 添加用户时ID为0
|
||||
username: "",
|
||||
nickname: "",
|
||||
password: "",
|
||||
email: "",
|
||||
role: null, // role 字段存储角色ID
|
||||
department_id: null,
|
||||
position_id: null,
|
||||
status: "active",
|
||||
tenant_id: tenantId,
|
||||
};
|
||||
// 重新加载职位列表(所有职位)
|
||||
fetchPositions();
|
||||
dialogVisible.value = true;
|
||||
editDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑用户
|
||||
const handleEdit = async (user: User) => {
|
||||
dialogTitle.value = "编辑用户";
|
||||
editDialogTitle.value = "编辑用户";
|
||||
isEdit.value = true;
|
||||
try {
|
||||
const res = await getUserInfo(user.id);
|
||||
const data = res.data || res;
|
||||
|
||||
// 获取当前租户ID
|
||||
const tenantId = getCurrentTenantId();
|
||||
|
||||
// 处理角色:data.role 存储的是角色ID(数字)
|
||||
let roleValue = data.role || null;
|
||||
|
||||
// 处理状态:后端返回的是数字(0或1),前端使用字符串("active"或"inactive")
|
||||
let statusStr = "active";
|
||||
if (typeof data.status === 'number') {
|
||||
statusStr = data.status === 1 ? "active" : "inactive";
|
||||
} else if (typeof data.status === 'string') {
|
||||
statusStr = data.status;
|
||||
}
|
||||
|
||||
form.value = {
|
||||
id: data.id,
|
||||
username: data.username,
|
||||
nickname: data.nickname,
|
||||
password: "",
|
||||
email: data.email,
|
||||
role: roleValue, // role 字段存储角色ID
|
||||
department_id: data.department_id || null,
|
||||
position_id: data.position_id || null,
|
||||
status: statusStr,
|
||||
tenant_id: data.tenant_id || tenantId,
|
||||
};
|
||||
|
||||
// 如果用户有部门,根据部门加载职位列表
|
||||
if (form.value.department_id) {
|
||||
fetchPositions(form.value.department_id);
|
||||
} else {
|
||||
fetchPositions(); // 加载所有职位
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error("加载用户失败");
|
||||
return;
|
||||
}
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
// 清除之前的错误
|
||||
passwordError.value = "";
|
||||
|
||||
try {
|
||||
if (dialogTitle.value === "修改密码") {
|
||||
// 校验旧密码
|
||||
if (!passwordForm.value.oldPassword) {
|
||||
passwordError.value = "请输入旧密码";
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验密码格式
|
||||
const passwordCheck = validatePassword(passwordForm.value.newPassword);
|
||||
if (passwordCheck !== true) {
|
||||
passwordError.value = passwordCheck;
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验确认密码
|
||||
const confirmCheck = validateConfirmPassword(
|
||||
passwordForm.value.newPassword,
|
||||
passwordForm.value.confirmPassword
|
||||
);
|
||||
if (confirmCheck !== true) {
|
||||
passwordError.value = confirmCheck;
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await changePassword(form.value.id, passwordForm.value);
|
||||
|
||||
// 检查返回结果
|
||||
if (res.code === 0) {
|
||||
// 先显示成功消息
|
||||
ElMessage.success({
|
||||
message: "密码修改成功",
|
||||
type: "success",
|
||||
customClass: "my-el-message-success",
|
||||
showClose: true,
|
||||
center: true,
|
||||
offset: 60,
|
||||
});
|
||||
// 延迟关闭弹窗,确保消息已经渲染
|
||||
setTimeout(() => {
|
||||
dialogVisible.value = false;
|
||||
// 重置密码表单
|
||||
passwordForm.value = {
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
}, 100);
|
||||
} else {
|
||||
passwordError.value = res.message || "密码修改失败";
|
||||
}
|
||||
} else if (isEdit.value) {
|
||||
// 检查ID是否存在
|
||||
if (!form.value.id || form.value.id === 0) {
|
||||
ElMessage.error("用户ID不能为空,请重新选择用户");
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建提交数据
|
||||
const submitData: any = {
|
||||
id: form.value.id,
|
||||
username: form.value.username,
|
||||
nickname: form.value.nickname,
|
||||
email: form.value.email,
|
||||
status: form.value.status,
|
||||
};
|
||||
|
||||
// role 字段存储角色ID
|
||||
if (form.value.role) {
|
||||
submitData.role = form.value.role; // 后端接收 role 参数
|
||||
}
|
||||
|
||||
// 部门和职位ID
|
||||
if (form.value.department_id) {
|
||||
submitData.department_id = form.value.department_id;
|
||||
}
|
||||
if (form.value.position_id) {
|
||||
submitData.position_id = form.value.position_id;
|
||||
}
|
||||
|
||||
if (form.value.tenant_id) {
|
||||
submitData.tenant_id = form.value.tenant_id;
|
||||
}
|
||||
|
||||
console.log('编辑用户提交数据:', submitData); // 调试日志
|
||||
await editUser(form.value.id, submitData);
|
||||
ElMessage.success({
|
||||
message: "更新成功",
|
||||
type: "success",
|
||||
customClass: "my-el-message-success",
|
||||
showClose: true,
|
||||
center: true,
|
||||
offset: 60,
|
||||
});
|
||||
dialogVisible.value = false;
|
||||
fetchUsers();
|
||||
} else {
|
||||
// 构建提交数据
|
||||
const submitData: any = {
|
||||
username: form.value.username,
|
||||
nickname: form.value.nickname,
|
||||
password: form.value.password,
|
||||
email: form.value.email,
|
||||
status: form.value.status,
|
||||
};
|
||||
|
||||
// role 字段存储角色ID
|
||||
if (form.value.role) {
|
||||
submitData.role = form.value.role; // 后端接收 role 参数
|
||||
}
|
||||
|
||||
// 部门和职位ID
|
||||
if (form.value.department_id) {
|
||||
submitData.department_id = form.value.department_id;
|
||||
}
|
||||
if (form.value.position_id) {
|
||||
submitData.position_id = form.value.position_id;
|
||||
}
|
||||
|
||||
if (form.value.tenant_id) {
|
||||
submitData.tenant_id = form.value.tenant_id;
|
||||
}
|
||||
|
||||
console.log('添加用户提交数据:', submitData); // 调试日志
|
||||
await addUser(submitData);
|
||||
ElMessage.success({
|
||||
message: "添加成功",
|
||||
type: "success",
|
||||
customClass: "my-el-message-success",
|
||||
showClose: true,
|
||||
center: true,
|
||||
offset: 60,
|
||||
});
|
||||
dialogVisible.value = false;
|
||||
fetchUsers();
|
||||
}
|
||||
} catch (e: any) {
|
||||
// 显示具体的错误信息
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "操作失败";
|
||||
if (dialogTitle.value === "修改密码") {
|
||||
passwordError.value = errorMsg;
|
||||
} else {
|
||||
ElMessage.error(errorMsg);
|
||||
// 让子组件 UserEdit 通过 loadUserData 加载用户信息
|
||||
if (userEditRef.value) {
|
||||
try {
|
||||
await userEditRef.value.loadUserData(user.id);
|
||||
} catch (e) {
|
||||
console.error("加载用户详情失败:", e);
|
||||
ElMessage.error("加载用户失败");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
editDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑成功回调
|
||||
const handleEditSuccess = () => {
|
||||
fetchUsers();
|
||||
};
|
||||
|
||||
// 职位加载回调
|
||||
const handleFetchPositions = (departmentId: number | null) => {
|
||||
if (departmentId && departmentId > 0) {
|
||||
fetchPositions(departmentId);
|
||||
} else {
|
||||
fetchPositions();
|
||||
}
|
||||
};
|
||||
|
||||
// 删除用户
|
||||
const handleDelete = async (user: User) => {
|
||||
ElMessageBox.confirm("确认删除该用户?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
@ -803,21 +506,15 @@ const handleDelete = async (user: User) => {
|
||||
});
|
||||
};
|
||||
|
||||
//修改密码
|
||||
const handleChangePassword = async (user: User) => {
|
||||
dialogTitle.value = "修改密码";
|
||||
isEdit.value = true;
|
||||
passwordError.value = "";
|
||||
form.value = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
};
|
||||
passwordForm.value = {
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
// 修改密码
|
||||
const handleChangePassword = (user: User) => {
|
||||
currentUserId.value = user.id;
|
||||
passwordDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 修改密码成功回调
|
||||
const handlePasswordChangeSuccess = () => {
|
||||
fetchUsers();
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@ -202,14 +202,16 @@ func (c *UserController) GetUserInfo() {
|
||||
"code": 0,
|
||||
"message": "查询成功",
|
||||
"data": map[string]interface{}{
|
||||
"id": user.Id,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"nickname": user.Nickname,
|
||||
"tenant_id": user.TenantId,
|
||||
"role": user.Role,
|
||||
"status": user.Status,
|
||||
"id": user.Id,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"nickname": user.Nickname,
|
||||
"tenant_id": user.TenantId,
|
||||
"role": user.Role,
|
||||
"status": user.Status,
|
||||
"department_id": user.DepartmentId,
|
||||
"position_id": user.PositionId,
|
||||
},
|
||||
}
|
||||
c.ServeJSON()
|
||||
|
||||
@ -2,8 +2,6 @@ package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
// DictType 字典类型模型
|
||||
@ -51,8 +49,3 @@ type DictItem struct {
|
||||
func (d *DictItem) TableName() string {
|
||||
return "sys_dict_item"
|
||||
}
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(DictType))
|
||||
orm.RegisterModel(new(DictItem))
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ type Tenant struct {
|
||||
Owner string `orm:"size(50)" json:"owner"`
|
||||
Phone string `orm:"size(20);null" json:"phone"`
|
||||
Email string `orm:"size(100);null" json:"email"`
|
||||
Status string `orm:"size(20);default(enabled)" json:"status"`
|
||||
Status int `orm:"default(1)" json:"status"`
|
||||
AuditStatus string `orm:"size(20);default(pending)" json:"audit_status"`
|
||||
AuditComment string `orm:"type(text);null" json:"audit_comment"`
|
||||
AuditBy string `orm:"size(50);null" json:"audit_by"`
|
||||
|
||||
@ -79,7 +79,7 @@ func ValidateUser(username, password string, tenantName string) (*models.User, *
|
||||
// 1. 根据租户名称查询租户(只查询未删除的)
|
||||
var tenant struct {
|
||||
Id int
|
||||
Status string
|
||||
Status int
|
||||
DeleteTime interface{} // 使用 interface{} 来处理 NULL 值
|
||||
}
|
||||
err := o.Raw("SELECT id, status, delete_time FROM yz_tenants WHERE name = ? AND delete_time IS NULL", tenantName).QueryRow(&tenant)
|
||||
@ -91,13 +91,13 @@ func ValidateUser(username, password string, tenantName string) (*models.User, *
|
||||
return nil, nil, fmt.Errorf("查询租户失败: %v", err)
|
||||
}
|
||||
|
||||
// 检查租户状态
|
||||
if tenant.Status == "disabled" {
|
||||
// 检查租户状态(0=禁用,1=启用)
|
||||
if tenant.Status == 0 {
|
||||
return nil, nil, errors.New("租户已被禁用")
|
||||
}
|
||||
|
||||
if tenant.Status != "enabled" {
|
||||
return nil, nil, fmt.Errorf("租户状态异常: %s", tenant.Status)
|
||||
if tenant.Status != 1 {
|
||||
return nil, nil, fmt.Errorf("租户状态异常: %d", tenant.Status)
|
||||
}
|
||||
|
||||
tenantId := tenant.Id
|
||||
@ -127,7 +127,7 @@ func AddUser(username, password, email, nickname, avatar string, tenantId, role,
|
||||
// 1. 验证租户是否存在且有效
|
||||
o := orm.NewOrm()
|
||||
var tenantExists bool
|
||||
err := o.Raw("SELECT EXISTS(SELECT 1 FROM yz_tenants WHERE id = ? AND delete_time IS NULL AND status = 'enabled')", tenantId).QueryRow(&tenantExists)
|
||||
err := o.Raw("SELECT EXISTS(SELECT 1 FROM yz_tenants WHERE id = ? AND delete_time IS NULL AND status = 1)", tenantId).QueryRow(&tenantExists)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("验证租户失败: %v", err)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user