更新代码

This commit is contained in:
李志强 2025-11-11 16:34:49 +08:00
parent 8b8b0c54a1
commit 65b281fe35
21 changed files with 3536 additions and 1414 deletions

420
package-lock.json generated
View File

@ -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"
}
}
}
}

View File

@ -1,6 +0,0 @@
{
"dependencies": {
"docx-preview": "^0.3.7",
"vue3-pdf-app": "^1.0.3"
}
}

548
pc/docs/dictionary-usage.md Normal file
View 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
```
---
## 前端 APIVue/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:** 这是正常的,字典数据只在组件的生命周期内存在。建议使用全局 storePinia来缓存字典数据或在每个需要的组件中通过 `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 缓存字典,避免重复请求
通过字典系统,你可以灵活地管理应用中的枚举值和标签数据,而无需修改代码。

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -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,546 +11,284 @@
</div>
<el-divider></el-divider>
<div class="dict-content">
<!-- 左侧字典类型列表 -->
<div class="dict-type-panel">
<div class="panel-header">
<h3>字典类型</h3>
<!-- 内容区域根据当前视图切换 -->
<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="搜索字典类型"
placeholder="搜索字典类型名称或编码"
clearable
style="width: 300px"
@input="handleTypeSearch"
style="width: 200px"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
</div>
<el-table
:data="filteredDictTypes"
stripe
highlight-current-row
@current-change="handleTypeSelect"
v-loading="typeLoading"
style="width: 100%"
height="calc(100vh - 280px)"
v-loading="typeLoading"
>
<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">
<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 label="操作" width="120" fixed="right" align="center">
<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 class="dict-item-panel">
<div class="panel-header">
<h3>字典项</h3>
<div class="panel-actions">
<el-button
type="primary"
size="small"
@click="handleAddItem"
:disabled="!currentDictType"
>
<el-icon><Plus /></el-icon>
添加字典项
<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 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"
:dict-type="currentDictType"
@success="handleTypeSuccess"
/>
</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>
<!-- 字典项编辑对话框 -->
<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: ''
})
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 || ''
})
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) {
//
function handleAddType() {
currentDictType.value = null
dictItems.value = []
}
fetchDictTypes()
} else {
ElMessage.error(res.message || '删除失败')
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败: ' + error.message)
}
}
typeDialogVisible.value = true
}
const handleTypeSubmit = async () => {
if (!typeFormRef.value) return
await typeFormRef.value.validate(async (valid) => {
if (!valid) return
//
function handleEditType(row: any) {
currentDictType.value = { ...row }
typeDialogVisible.value = true
}
//
function handleViewItems(row: any) {
selectedDictTypeId.value = row.id
currentView.value = 'items'
}
//
async function handleDeleteType(row: any) {
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
fetchDictTypes()
if (currentDictType.value && currentDictType.value.id === typeForm.id) {
fetchDictItems(typeForm.id)
}
} else {
ElMessage.error(res.message || '操作失败')
}
} catch (error) {
ElMessage.error('操作失败: ' + error.message)
}
})
}
await ElMessageBox.confirm(
`确定要删除字典类型「${row.dict_name}」吗?删除后不可恢复。`,
'警告',
{ type: 'warning' }
)
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) => {
typeLoading.value = true
try {
await ElMessageBox.confirm('确定要删除该字典项吗?', '提示', {
type: 'warning'
})
const res = await deleteDictItem(row.id)
if (res.success) {
const res = await deleteDictType(row.id)
if (res.success === true) {
ElMessage.success('删除成功')
fetchDictItems(currentDictType.value.id)
fetchDictTypes()
} 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%;
display: flex;
flex-direction: column;
}
.header-bar {
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
@ -562,55 +296,48 @@ onMounted(() => {
h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
.header-actions {
display: flex;
gap: 10px;
}
}
}
.dict-content {
display: flex;
gap: 20px;
flex: 1;
min-height: 0;
.tab-content {
padding: 20px 0;
}
.dict-type-panel,
.dict-item-panel {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 15px;
background: #fff;
.panel-header {
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
margin-bottom: 20px;
h3 {
margin: 0;
font-size: 16px;
}
.panel-actions {
display: flex;
gap: 10px;
}
}
.empty-state {
flex: 1;
.search-box {
display: flex;
align-items: center;
justify-content: center;
}
.filter-box {
display: flex;
align-items: 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>

View File

@ -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="存储容量">

View File

@ -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 || '',

View File

@ -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>

View 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>

View 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>

View 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>

View File

@ -16,6 +16,7 @@
<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"
<!-- 编辑用户对话框组件 -->
<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"
/>
</el-form-item>
<el-form-item label="旧密码" v-if="dialogTitle === '修改密码'">
<el-input
v-model="passwordForm.oldPassword"
type="password"
autocomplete="current-password"
show-password
<!-- 修改密码对话框组件 -->
<ChangePasswordDialog
ref="changePasswordRef"
:modelValue="passwordDialogVisible"
@update:modelValue="passwordDialogVisible = $event"
:user-id="currentUserId"
@submit="handlePasswordChangeSuccess"
@close="passwordDialogVisible = false"
/>
</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>
</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, // ID0
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;
// UserEdit loadUserData
if (userEditRef.value) {
try {
const res = await getUserInfo(user.id);
const data = res.data || res;
// ID
const tenantId = getCurrentTenantId();
// data.role ID
let roleValue = data.role || null;
// 01使"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(); //
}
await userEditRef.value.loadUserData(user.id);
} catch (e) {
console.error("加载用户详情失败:", e);
ElMessage.error("加载用户失败");
return;
}
dialogVisible.value = true;
}
editDialogVisible.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;
//
const handleEditSuccess = () => {
fetchUsers();
};
//
const handleFetchPositions = (departmentId: number | null) => {
if (departmentId && departmentId > 0) {
fetchPositions(departmentId);
} 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);
}
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>

View File

@ -210,6 +210,8 @@ func (c *UserController) GetUserInfo() {
"tenant_id": user.TenantId,
"role": user.Role,
"status": user.Status,
"department_id": user.DepartmentId,
"position_id": user.PositionId,
},
}
c.ServeJSON()

View File

@ -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))
}

View File

@ -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"`

View File

@ -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)
}