完成租户登录
This commit is contained in:
parent
b9938b4c0d
commit
ddf90424ba
539
pc/package-lock.json
generated
539
pc/package-lock.json
generated
@ -9,11 +9,13 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"axios": "^1.13.1",
|
||||
"chart": "^0.1.2",
|
||||
"chart.js": "^4.5.1",
|
||||
"element-plus": "^2.11.5",
|
||||
"less": "^4.4.2",
|
||||
"marked": "^16.4.1",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3"
|
||||
@ -59,6 +61,15 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.4",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz",
|
||||
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz",
|
||||
@ -938,6 +949,12 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@transloadit/prettier-bytes": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz",
|
||||
"integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
|
||||
@ -945,6 +962,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/event-emitter": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmmirror.com/@types/event-emitter/-/event-emitter-0.3.5.tgz",
|
||||
"integrity": "sha512-zx2/Gg0Eg7gwEiOIIh5w9TrhKKTeQh7CPCOPNc0el4pLSwzebA8SmnHwZs2dWlLONvyulykSwGSQxQHLhjGLvQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
"version": "4.17.20",
|
||||
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.20.tgz",
|
||||
@ -976,6 +999,61 @@
|
||||
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@uppy/companion-client": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@uppy/companion-client/-/companion-client-2.2.2.tgz",
|
||||
"integrity": "sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@uppy/utils": "^4.1.2",
|
||||
"namespace-emitter": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@uppy/core": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/@uppy/core/-/core-2.3.4.tgz",
|
||||
"integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@transloadit/prettier-bytes": "0.0.7",
|
||||
"@uppy/store-default": "^2.1.1",
|
||||
"@uppy/utils": "^4.1.3",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"mime-match": "^1.0.2",
|
||||
"namespace-emitter": "^2.0.1",
|
||||
"nanoid": "^3.1.25",
|
||||
"preact": "^10.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@uppy/store-default": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@uppy/store-default/-/store-default-2.1.1.tgz",
|
||||
"integrity": "sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@uppy/utils": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@uppy/utils/-/utils-4.1.3.tgz",
|
||||
"integrity": "sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash.throttle": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@uppy/xhr-upload": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz",
|
||||
"integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@uppy/companion-client": "^2.2.2",
|
||||
"@uppy/utils": "^4.1.2",
|
||||
"nanoid": "^3.1.25"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@uppy/core": "^2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
|
||||
@ -1223,6 +1301,155 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@wangeditor/basic-modules": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmmirror.com/@wangeditor/basic-modules/-/basic-modules-1.1.7.tgz",
|
||||
"integrity": "sha512-cY9CPkLJaqF05STqfpZKWG4LpxTMeGSIIF1fHvfm/mz+JXatCagjdkbxdikOuKYlxDdeqvOeBmsUBItufDLXZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-url": "^1.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@wangeditor/core": "1.x",
|
||||
"dom7": "^3.0.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"nanoid": "^3.2.0",
|
||||
"slate": "^0.72.0",
|
||||
"snabbdom": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@wangeditor/code-highlight": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@wangeditor/code-highlight/-/code-highlight-1.0.3.tgz",
|
||||
"integrity": "sha512-iazHwO14XpCuIWJNTQTikqUhGKyqj+dUNWJ9288Oym9M2xMVHvnsOmDU2sgUDWVy+pOLojReMPgXCsvvNlOOhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prismjs": "^1.23.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@wangeditor/core": "1.x",
|
||||
"dom7": "^3.0.0",
|
||||
"slate": "^0.72.0",
|
||||
"snabbdom": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@wangeditor/core": {
|
||||
"version": "1.1.19",
|
||||
"resolved": "https://registry.npmmirror.com/@wangeditor/core/-/core-1.1.19.tgz",
|
||||
"integrity": "sha512-KevkB47+7GhVszyYF2pKGKtCSj/YzmClsD03C3zTt+9SR2XWT5T0e3yQqg8baZpcMvkjs1D8Dv4fk8ok/UaS2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/event-emitter": "^0.3.3",
|
||||
"event-emitter": "^0.3.5",
|
||||
"html-void-elements": "^2.0.0",
|
||||
"i18next": "^20.4.0",
|
||||
"scroll-into-view-if-needed": "^2.2.28",
|
||||
"slate-history": "^0.66.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@uppy/core": "^2.1.1",
|
||||
"@uppy/xhr-upload": "^2.0.3",
|
||||
"dom7": "^3.0.0",
|
||||
"is-hotkey": "^0.2.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.foreach": "^4.5.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"lodash.toarray": "^4.4.0",
|
||||
"nanoid": "^3.2.0",
|
||||
"slate": "^0.72.0",
|
||||
"snabbdom": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@wangeditor/editor": {
|
||||
"version": "5.1.23",
|
||||
"resolved": "https://registry.npmmirror.com/@wangeditor/editor/-/editor-5.1.23.tgz",
|
||||
"integrity": "sha512-0RxfeVTuK1tktUaPROnCoFfaHVJpRAIE2zdS0mpP+vq1axVQpLjM8+fCvKzqYIkH0Pg+C+44hJpe3VVroSkEuQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@uppy/core": "^2.1.1",
|
||||
"@uppy/xhr-upload": "^2.0.3",
|
||||
"@wangeditor/basic-modules": "^1.1.7",
|
||||
"@wangeditor/code-highlight": "^1.0.3",
|
||||
"@wangeditor/core": "^1.1.19",
|
||||
"@wangeditor/list-module": "^1.0.5",
|
||||
"@wangeditor/table-module": "^1.1.4",
|
||||
"@wangeditor/upload-image-module": "^1.0.2",
|
||||
"@wangeditor/video-module": "^1.1.4",
|
||||
"dom7": "^3.0.0",
|
||||
"is-hotkey": "^0.2.0",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.foreach": "^4.5.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"lodash.toarray": "^4.4.0",
|
||||
"nanoid": "^3.2.0",
|
||||
"slate": "^0.72.0",
|
||||
"snabbdom": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@wangeditor/list-module": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/@wangeditor/list-module/-/list-module-1.0.5.tgz",
|
||||
"integrity": "sha512-uDuYTP6DVhcYf7mF1pTlmNn5jOb4QtcVhYwSSAkyg09zqxI1qBqsfUnveeDeDqIuptSJhkh81cyxi+MF8sEPOQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@wangeditor/core": "1.x",
|
||||
"dom7": "^3.0.0",
|
||||
"slate": "^0.72.0",
|
||||
"snabbdom": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@wangeditor/table-module": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/@wangeditor/table-module/-/table-module-1.1.4.tgz",
|
||||
"integrity": "sha512-5saanU9xuEocxaemGdNi9t8MCDSucnykEC6jtuiT72kt+/Hhh4nERYx1J20OPsTCCdVr7hIyQenFD1iSRkIQ6w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@wangeditor/core": "1.x",
|
||||
"dom7": "^3.0.0",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.throttle": "^4.1.1",
|
||||
"nanoid": "^3.2.0",
|
||||
"slate": "^0.72.0",
|
||||
"snabbdom": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@wangeditor/upload-image-module": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@wangeditor/upload-image-module/-/upload-image-module-1.0.2.tgz",
|
||||
"integrity": "sha512-z81lk/v71OwPDYeQDxj6cVr81aDP90aFuywb8nPD6eQeECtOymrqRODjpO6VGvCVxVck8nUxBHtbxKtjgcwyiA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@uppy/core": "^2.0.3",
|
||||
"@uppy/xhr-upload": "^2.0.3",
|
||||
"@wangeditor/basic-modules": "1.x",
|
||||
"@wangeditor/core": "1.x",
|
||||
"dom7": "^3.0.0",
|
||||
"lodash.foreach": "^4.5.0",
|
||||
"slate": "^0.72.0",
|
||||
"snabbdom": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@wangeditor/video-module": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/@wangeditor/video-module/-/video-module-1.1.4.tgz",
|
||||
"integrity": "sha512-ZdodDPqKQrgx3IwWu4ZiQmXI8EXZ3hm2/fM6E3t5dB8tCaIGWQZhmqd6P5knfkRAd3z2+YRSRbxOGfoRSp/rLg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@uppy/core": "^2.1.4",
|
||||
"@uppy/xhr-upload": "^2.0.7",
|
||||
"@wangeditor/core": "1.x",
|
||||
"dom7": "^3.0.0",
|
||||
"nanoid": "^3.2.0",
|
||||
"slate": "^0.72.0",
|
||||
"snabbdom": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz",
|
||||
@ -1434,6 +1661,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/compute-scroll-into-view": {
|
||||
"version": "1.0.20",
|
||||
"resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
|
||||
"integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/confbox": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.2.tgz",
|
||||
@ -1459,6 +1692,19 @@
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/d/-/d-1.0.2.tgz",
|
||||
"integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"es5-ext": "^0.10.64",
|
||||
"type": "^2.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/data-view-buffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
|
||||
@ -1577,6 +1823,15 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom7": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/dom7/-/dom7-3.0.0.tgz",
|
||||
"integrity": "sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ssr-window": "^3.0.0-alpha.1"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
@ -1771,6 +2026,46 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/es5-ext": {
|
||||
"version": "0.10.64",
|
||||
"resolved": "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.64.tgz",
|
||||
"integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"es6-iterator": "^2.0.3",
|
||||
"es6-symbol": "^3.1.3",
|
||||
"esniff": "^2.0.1",
|
||||
"next-tick": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-iterator": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/es6-iterator/-/es6-iterator-2.0.3.tgz",
|
||||
"integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d": "1",
|
||||
"es5-ext": "^0.10.35",
|
||||
"es6-symbol": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-symbol": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/es6-symbol/-/es6-symbol-3.1.4.tgz",
|
||||
"integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d": "^1.0.2",
|
||||
"ext": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.11",
|
||||
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.11.tgz",
|
||||
@ -1826,6 +2121,21 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/esniff": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/esniff/-/esniff-2.0.1.tgz",
|
||||
"integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d": "^1.0.1",
|
||||
"es5-ext": "^0.10.62",
|
||||
"event-emitter": "^0.3.5",
|
||||
"type": "^2.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||
@ -1836,6 +2146,16 @@
|
||||
"@types/estree": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/event-emitter": {
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmmirror.com/event-emitter/-/event-emitter-0.3.5.tgz",
|
||||
"integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d": "1",
|
||||
"es5-ext": "~0.10.14"
|
||||
}
|
||||
},
|
||||
"node_modules/exsolve": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz",
|
||||
@ -1843,6 +2163,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ext": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/ext/-/ext-1.7.0.tgz",
|
||||
"integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"type": "^2.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
|
||||
@ -2168,6 +2497,25 @@
|
||||
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-void-elements": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-2.0.1.tgz",
|
||||
"integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "20.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/i18next/-/i18next-20.6.1.tgz",
|
||||
"integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@ -2194,6 +2542,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "9.0.21",
|
||||
"resolved": "https://registry.npmmirror.com/immer/-/immer-9.0.21.tgz",
|
||||
"integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/internal-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz",
|
||||
@ -2354,6 +2712,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-hotkey": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-hotkey/-/is-hotkey-0.2.0.tgz",
|
||||
"integrity": "sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-map": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz",
|
||||
@ -2394,6 +2758,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-regex": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz",
|
||||
@ -2487,6 +2860,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-url": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/is-url/-/is-url-1.2.4.tgz",
|
||||
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-weakmap": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz",
|
||||
@ -2616,6 +2995,49 @@
|
||||
"lodash-es": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.debounce": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.foreach": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz",
|
||||
"integrity": "sha512-aEXTF4d+m05rVOAUG3z4vZZ4xVexLKZGF0lIxuHZ1Hplpk/3B6Z1+/ICICYRLm7c41Z2xiejbkCkJoTlypoXhQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isequal": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.throttle": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
|
||||
"integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.toarray": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz",
|
||||
"integrity": "sha512-QyffEA3i5dma5q2490+SgCvDN0pXLmRGSyAANuVi0HQ01Pkfr9fuoKQW8wm1wGBnJITs/mS7wQvS6VshUEBFCw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@ -2639,6 +3061,18 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "16.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/marked/-/marked-16.4.1.tgz",
|
||||
"integrity": "sha512-ntROs7RaN3EvWfy3EZi14H4YxmT6A5YvywfhO+0pm+cH/dnSQRmdAmoFIc3B9aiwTehyk7pESH4ofyBY+V5hZg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"marked": "bin/marked.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@ -2676,6 +3110,15 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/mime-match/-/mime-match-1.0.2.tgz",
|
||||
"integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wildcard": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
||||
@ -2742,6 +3185,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/namespace-emitter": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/namespace-emitter/-/namespace-emitter-2.0.1.tgz",
|
||||
"integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
|
||||
@ -2777,6 +3226,12 @@
|
||||
"node": ">= 4.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/next-tick": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/next-tick/-/next-tick-1.1.0.tgz",
|
||||
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/normalize-wheel-es": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
|
||||
@ -2971,6 +3426,25 @@
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.27.2",
|
||||
"resolved": "https://registry.npmmirror.com/preact/-/preact-10.27.2.tgz",
|
||||
"integrity": "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/prismjs": {
|
||||
"version": "1.30.0",
|
||||
"resolved": "https://registry.npmmirror.com/prismjs/-/prismjs-1.30.0.tgz",
|
||||
"integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@ -3171,6 +3645,15 @@
|
||||
"license": "ISC",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/scroll-into-view-if-needed": {
|
||||
"version": "2.2.31",
|
||||
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
|
||||
"integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"compute-scroll-into-view": "^1.0.20"
|
||||
}
|
||||
},
|
||||
"node_modules/scule": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz",
|
||||
@ -3306,6 +3789,38 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/slate": {
|
||||
"version": "0.72.8",
|
||||
"resolved": "https://registry.npmmirror.com/slate/-/slate-0.72.8.tgz",
|
||||
"integrity": "sha512-/nJwTswQgnRurpK+bGJFH1oM7naD5qDmHd89JyiKNT2oOKD8marW0QSBtuFnwEbL5aGCS8AmrhXQgNOsn4osAw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immer": "^9.0.6",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"tiny-warning": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/slate-history": {
|
||||
"version": "0.66.0",
|
||||
"resolved": "https://registry.npmmirror.com/slate-history/-/slate-history-0.66.0.tgz",
|
||||
"integrity": "sha512-6MWpxGQZiMvSINlCbMW43E2YBSVMCMCIwQfBzGssjWw4kb0qfvj0pIdblWNRQZD0hR6WHP+dHHgGSeVdMWzfng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-plain-object": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"slate": ">=0.65.3"
|
||||
}
|
||||
},
|
||||
"node_modules/snabbdom": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/snabbdom/-/snabbdom-3.6.3.tgz",
|
||||
"integrity": "sha512-W2lHLLw2qR2Vv0DcMmcxXqcfdBaIcoN+y/86SmHv8fn4DazEQSH6KN3TjZcWvwujW56OHiiirsbHWZb4vx/0fg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz",
|
||||
@ -3334,6 +3849,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ssr-window": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/ssr-window/-/ssr-window-3.0.0.tgz",
|
||||
"integrity": "sha512-q+8UfWDg9Itrg0yWK7oe5p/XRCJpJF9OBtXfOPgSJl+u3Xd5KI328RUEvUqSMVM9CiQUEf1QdBzJMkYGErj9QA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/stop-iteration-iterator": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
|
||||
@ -3455,6 +3976,12 @@
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.15",
|
||||
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||
@ -3495,6 +4022,12 @@
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmmirror.com/type/-/type-2.7.3.tgz",
|
||||
"integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/typed-array-buffer": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
|
||||
@ -3953,6 +4486,12 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/wildcard": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/wildcard/-/wildcard-1.1.2.tgz",
|
||||
"integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,11 +10,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"axios": "^1.13.1",
|
||||
"chart": "^0.1.2",
|
||||
"chart.js": "^4.5.1",
|
||||
"element-plus": "^2.11.5",
|
||||
"less": "^4.4.2",
|
||||
"marked": "^16.4.1",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-router": "^4.6.3"
|
||||
|
||||
99
pc/src/api/file.js
Normal file
99
pc/src/api/file.js
Normal file
@ -0,0 +1,99 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/**
|
||||
* 获取所有文件
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getAllFiles() {
|
||||
return request({
|
||||
url: "/api/files",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的文件
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getMyFiles() {
|
||||
return request({
|
||||
url: "/api/files/my",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取文件
|
||||
* @param {number|string} id 文件ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getFileById(id) {
|
||||
return request({
|
||||
url: `/api/files/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param {FormData} formData 文件数据
|
||||
* @param {Object} options 额外选项
|
||||
* @param {string} [options.category]
|
||||
* @param {string} [options.tenantId]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function uploadFile(formData, options = {}) {
|
||||
if (options.category) {
|
||||
formData.append('category', options.category);
|
||||
}
|
||||
if (options.tenantId) {
|
||||
formData.append('tenant_id', options.tenantId);
|
||||
}
|
||||
return request({
|
||||
url: "/api/files",
|
||||
method: "post",
|
||||
data: formData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新文件信息
|
||||
* @param {number|string} id 文件ID
|
||||
* @param {Object} fileData 更新的数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function updateFile(id, fileData) {
|
||||
return request({
|
||||
url: `/api/files/${id}`,
|
||||
method: "put",
|
||||
data: fileData,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param {number|string} id 文件ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteFile(id) {
|
||||
return request({
|
||||
url: `/api/files/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索文件
|
||||
* @param {string} keyword 关键字
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function searchFiles(keyword) {
|
||||
return request({
|
||||
url: `/api/files/search`,
|
||||
method: "get",
|
||||
params: { keyword },
|
||||
});
|
||||
}
|
||||
115
pc/src/api/knowledge.js
Normal file
115
pc/src/api/knowledge.js
Normal file
@ -0,0 +1,115 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/**
|
||||
* 获取知识列表
|
||||
* @param {Object} params 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getKnowledgeList(params) {
|
||||
return request({
|
||||
url: "/api/knowledge/list",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识详情
|
||||
* @param {number|string} id 知识ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getKnowledgeDetail(id) {
|
||||
return request({
|
||||
url: `/api/knowledge/detail`,
|
||||
method: "get",
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建知识
|
||||
* @param {Object} data 知识数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function createKnowledge(data) {
|
||||
return request({
|
||||
url: "/api/knowledge/create",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新知识
|
||||
* @param {number|string} id 知识ID
|
||||
* @param {Object} data 更新的数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function updateKnowledge(id, data) {
|
||||
return request({
|
||||
url: `/api/knowledge/update`,
|
||||
method: "post",
|
||||
data: { ...data, id },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除知识
|
||||
* @param {number|string} id 知识ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteKnowledge(id) {
|
||||
return request({
|
||||
url: `/api/knowledge/delete`,
|
||||
method: "post",
|
||||
data: { id },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类列表
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getCategoryList() {
|
||||
return request({
|
||||
url: "/api/knowledge/categories",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取标签列表
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getTagList() {
|
||||
return request({
|
||||
url: "/api/knowledge/tags",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加分类
|
||||
* @param {Object} data 分类数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function addCategory(data) {
|
||||
return request({
|
||||
url: "/api/knowledge/category/add",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加标签
|
||||
* @param {Object} data 标签数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function addTag(data) {
|
||||
return request({
|
||||
url: "/api/knowledge/tag/add",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
@ -1,70 +1,113 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
// 获取知识列表
|
||||
/**
|
||||
* 获取知识列表
|
||||
* @param {Object} params 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getKnowledgeList(params) {
|
||||
return request({
|
||||
url: `/api/knowledge/list`,
|
||||
url: "/api/knowledge/list",
|
||||
method: "get",
|
||||
params: params,
|
||||
params,
|
||||
});
|
||||
}
|
||||
// 获取知识详情
|
||||
|
||||
/**
|
||||
* 获取知识详情
|
||||
* @param {number|string} id 知识ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getKnowledgeDetail(id) {
|
||||
return request({
|
||||
url: `/api/knowledge/detail/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
// 创建知识
|
||||
|
||||
/**
|
||||
* 创建知识
|
||||
* @param {Object} data 知识数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function createKnowledge(data) {
|
||||
return request({
|
||||
url: `/api/knowledge/create`,
|
||||
url: "/api/knowledge/create",
|
||||
method: "post",
|
||||
data: data,
|
||||
data,
|
||||
});
|
||||
}
|
||||
// 更新知识
|
||||
|
||||
/**
|
||||
* 更新知识
|
||||
* @param {number|string} id 知识ID
|
||||
* @param {Object} data 更新的数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function updateKnowledge(id, data) {
|
||||
return request({
|
||||
url: `/api/knowledge/update/${id}`,
|
||||
method: "put",
|
||||
data: data,
|
||||
data,
|
||||
});
|
||||
}
|
||||
// 删除知识
|
||||
|
||||
/**
|
||||
* 删除知识
|
||||
* @param {number|string} id 知识ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteKnowledge(id) {
|
||||
return request({
|
||||
url: `/api/knowledge/delete/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
// 获取分类列表
|
||||
|
||||
/**
|
||||
* 获取分类列表
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getCategoryList() {
|
||||
return request({
|
||||
url: `/api/knowledge/category/list`,
|
||||
url: "/api/knowledge/category/list",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
// 获取标签列表
|
||||
|
||||
/**
|
||||
* 获取标签列表
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getTagList() {
|
||||
return request({
|
||||
url: `/api/knowledge/tag/list`,
|
||||
url: "/api/knowledge/tag/list",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
// 添加分类
|
||||
|
||||
/**
|
||||
* 添加分类
|
||||
* @param {Object} data 分类数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function addCategory(data) {
|
||||
return request({
|
||||
url: `/api/knowledge/category/add`,
|
||||
url: "/api/knowledge/category/add",
|
||||
method: "post",
|
||||
data: data,
|
||||
data,
|
||||
});
|
||||
}
|
||||
// 添加标签
|
||||
|
||||
/**
|
||||
* 添加标签
|
||||
* @param {Object} data 标签数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function addTag(data) {
|
||||
return request({
|
||||
url: `/api/knowledge/tag/add`,
|
||||
url: "/api/knowledge/tag/add",
|
||||
method: "post",
|
||||
data: data,
|
||||
data,
|
||||
});
|
||||
}
|
||||
@ -1,13 +1,14 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
// 登录
|
||||
export function login(username, password) {
|
||||
// 登录(使用租户名称)
|
||||
export function login(username, password, tenantName) {
|
||||
return request({
|
||||
url: `/api/login`,
|
||||
method: "post",
|
||||
data: {
|
||||
username: username,
|
||||
password: password,
|
||||
tenant_name: tenantName, // 后端期望的字段名是 tenant_name
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@ -8,3 +8,37 @@ export function getAllMenus() {
|
||||
});
|
||||
}
|
||||
|
||||
// 更新菜单状态
|
||||
export function updateMenuStatus(menuId, status) {
|
||||
return request({
|
||||
url: `/api/menu/status/${menuId}`,
|
||||
method: "patch",
|
||||
data: { status },
|
||||
});
|
||||
}
|
||||
|
||||
// 创建菜单
|
||||
export function createMenu(menuData) {
|
||||
return request({
|
||||
url: `/api/menu`,
|
||||
method: "post",
|
||||
data: menuData,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新菜单
|
||||
export function updateMenu(menuId, menuData) {
|
||||
return request({
|
||||
url: `/api/menu/${menuId}`,
|
||||
method: "put",
|
||||
data: menuData,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除菜单
|
||||
export function deleteMenu(menuId) {
|
||||
return request({
|
||||
url: `/api/menu/${menuId}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
55
pc/src/api/tenant.js
Normal file
55
pc/src/api/tenant.js
Normal file
@ -0,0 +1,55 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
//获取所有租户
|
||||
export function getAllTenants() {
|
||||
return request({
|
||||
url: "/api/tenant/list",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
//新增租户
|
||||
export function createTenant(tenantData) {
|
||||
return request({
|
||||
url: "/api/tenant",
|
||||
method: "post",
|
||||
data: tenantData,
|
||||
});
|
||||
}
|
||||
|
||||
// 编辑租户
|
||||
export function updateTenant(tenantId, tenantData) {
|
||||
return request({
|
||||
url: `/api/tenant/${tenantId}`,
|
||||
method: "put",
|
||||
data: tenantData,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除租户
|
||||
export function deleteTenant(tenantId) {
|
||||
return request({
|
||||
url: `/api/tenant/${tenantId}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
// 审核租户
|
||||
export function auditTenant(tenantId, auditData) {
|
||||
return request({
|
||||
url: `/api/tenant/${tenantId}/audit`,
|
||||
method: "post",
|
||||
data: auditData,
|
||||
});
|
||||
}
|
||||
|
||||
// 查看租户详情
|
||||
export function getTenantDetail(tenantId) {
|
||||
return request({
|
||||
url: `/api/tenant/${tenantId}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -54,6 +54,16 @@
|
||||
--card-bg-color: #ffffff;
|
||||
--card-border-color: #ebeef5;
|
||||
|
||||
// container-box 亮色主题
|
||||
--container-box-bg: #fff;
|
||||
--container-box-border: #ebeef5;
|
||||
--container-box-shadow: 0 2px 8px 0 rgba(0,0,0,0.04);
|
||||
|
||||
// 滚动条颜色
|
||||
--scrollbar-track-color: #f1f1f1;
|
||||
--scrollbar-thumb-color: #c1c1c1;
|
||||
--scrollbar-thumb-hover-color: #a8a8a8;
|
||||
|
||||
// Element Plus 组件主题适配
|
||||
--el-bg-color: var(--bg-color);
|
||||
--el-bg-color-page: var(--bg-color-page);
|
||||
@ -125,6 +135,16 @@
|
||||
// 卡片背景
|
||||
--card-bg-color: #252526;
|
||||
--card-border-color: #3c3c3d;
|
||||
|
||||
// container-box 暗色主题
|
||||
--container-box-bg: #232325;
|
||||
--container-box-border: #363637;
|
||||
--container-box-shadow: 0 2px 8px 0 rgba(0,0,0,0.09);
|
||||
|
||||
// 滚动条颜色
|
||||
--scrollbar-track-color: #2d2d2d;
|
||||
--scrollbar-thumb-color: #555555;
|
||||
--scrollbar-thumb-hover-color: #666666;
|
||||
|
||||
// Element Plus 组件主题适配
|
||||
--el-bg-color: var(--bg-color);
|
||||
@ -150,6 +170,12 @@ body {
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
// 卡片样式
|
||||
.el-card {
|
||||
background-color: var(--card-bg-color);
|
||||
@ -157,6 +183,19 @@ body {
|
||||
color: var(--text-color-primary);
|
||||
}
|
||||
|
||||
// container-box样式
|
||||
.container-box {
|
||||
background-color: var(--container-box-bg);
|
||||
border: 1px solid var(--container-box-border);
|
||||
box-shadow: var(--container-box-shadow);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
transition:
|
||||
background-color 0.3s,
|
||||
border-color 0.3s,
|
||||
box-shadow 0.3s;
|
||||
}
|
||||
|
||||
// 输入框样式
|
||||
.el-input__inner {
|
||||
background-color: var(--fill-color-blank);
|
||||
@ -168,24 +207,427 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮样式过渡
|
||||
// 按钮样式(全局主题适配)
|
||||
.el-button {
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
|
||||
// 默认按钮
|
||||
&.el-button--default {
|
||||
background-color: var(--fill-color-blank) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--fill-color) !important;
|
||||
border-color: var(--border-color-lighter) !important;
|
||||
color: var(--text-color-disabled) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 文本按钮
|
||||
&.el-button--text {
|
||||
color: var(--text-color-primary) !important;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: var(--text-color-disabled) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表格样式
|
||||
// 表格样式(全局主题适配)
|
||||
.el-table {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color-primary);
|
||||
background-color: var(--bg-color) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
border-color: var(--border-color-lighter) !important;
|
||||
|
||||
th {
|
||||
background-color: var(--fill-color-light);
|
||||
color: var(--text-color-primary);
|
||||
// 表头
|
||||
th.el-table__cell {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
border-bottom-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
td {
|
||||
border-color: var(--border-color-lighter);
|
||||
// 表格行
|
||||
tr {
|
||||
background-color: var(--bg-color) !important;
|
||||
|
||||
// 鼠标悬停
|
||||
&:hover > td {
|
||||
background-color: var(--fill-color-lighter) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 表格单元格
|
||||
td.el-table__cell {
|
||||
border-bottom-color: var(--border-color-lighter) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
// 斑马纹表格
|
||||
&.el-table--striped {
|
||||
.el-table__body tr.el-table__row--striped td {
|
||||
background-color: var(--fill-color-extra-light) !important;
|
||||
}
|
||||
|
||||
.el-table__body tr.el-table__row--striped:hover td {
|
||||
background-color: var(--fill-color-lighter) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 边框表格
|
||||
&.el-table--border {
|
||||
border-color: var(--border-color-lighter) !important;
|
||||
|
||||
&::after,
|
||||
&::before {
|
||||
background-color: var(--border-color-lighter) !important;
|
||||
}
|
||||
|
||||
th.el-table__cell,
|
||||
td.el-table__cell {
|
||||
border-right-color: var(--border-color-lighter) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.el-table__empty-block {
|
||||
background-color: var(--bg-color) !important;
|
||||
color: var(--text-color-secondary) !important;
|
||||
}
|
||||
|
||||
// 空状态文字
|
||||
.el-table__empty-text {
|
||||
color: var(--text-color-secondary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 标签样式(全局主题适配)
|
||||
.el-tag {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
border-color: var(--border-color-lighter) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
// 对话框样式(全局主题适配)
|
||||
.el-dialog {
|
||||
background-color: var(--bg-color) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
|
||||
.el-dialog__header {
|
||||
border-bottom-color: var(--border-color-lighter) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
.el-dialog__title {
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
background-color: var(--bg-color) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
.el-dialog__footer {
|
||||
border-top-color: var(--border-color-lighter) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// MessageBox 确认框样式(全局主题适配)
|
||||
.el-overlay.is-message-box {
|
||||
background-color: rgba(0, 0, 0, 0.5) !important;
|
||||
|
||||
&[data-theme="dark"] {
|
||||
background-color: rgba(0, 0, 0, 0.7) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-message-box {
|
||||
background-color: var(--bg-color) !important;
|
||||
|
||||
.el-message-box__header {
|
||||
.el-message-box__title {
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-message-box__content {
|
||||
color: var(--text-color-primary) !important;
|
||||
|
||||
.el-message-box__message {
|
||||
color: var(--text-color-primary) !important;
|
||||
|
||||
p {
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 描述列表样式(全局主题适配)
|
||||
.el-descriptions {
|
||||
.el-descriptions__header {
|
||||
.el-descriptions__title {
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-descriptions__table {
|
||||
.el-descriptions__label {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
border-color: var(--border-color-lighter) !important;
|
||||
}
|
||||
|
||||
.el-descriptions__content {
|
||||
background-color: var(--bg-color) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
border-color: var(--border-color-lighter) !important;
|
||||
}
|
||||
|
||||
td.el-descriptions__label,
|
||||
td.el-descriptions__content {
|
||||
border-color: var(--border-color-lighter) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 表单样式(全局主题适配)
|
||||
.el-form {
|
||||
.el-form-item__label {
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
.el-form-item__error {
|
||||
color: var(--danger-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 选择器样式(全局主题适配)
|
||||
.el-select {
|
||||
// Element Plus 2.x 使用 el-select__wrapper
|
||||
.el-select__wrapper {
|
||||
background-color: var(--fill-color-blank) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px var(--border-color) inset !important;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
box-shadow: 0 0 0 1px var(--primary-color) inset !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果选择器内部使用了 el-input__wrapper(兼容性处理)
|
||||
.el-input__wrapper {
|
||||
background-color: var(--fill-color-blank) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px var(--border-color) inset !important;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
box-shadow: 0 0 0 1px var(--primary-color) inset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
background-color: transparent !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
.el-input__suffix {
|
||||
.el-input__suffix-inner {
|
||||
.el-select__caret {
|
||||
color: var(--text-color-secondary) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 选择器下拉菜单样式(全局主题适配)
|
||||
.el-select-dropdown,
|
||||
.el-select-dropdown__wrap {
|
||||
background-color: var(--bg-color-overlay) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
|
||||
.el-select-dropdown__item {
|
||||
color: var(--text-color-primary) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
color: var(--primary-color) !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
color: var(--text-color-disabled) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-select-dropdown__empty {
|
||||
color: var(--text-color-secondary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Element Plus Popper 组件样式(全局主题适配,用于所有通过 Popper 渲染的下拉组件)
|
||||
.el-popper,
|
||||
.el-popper__arrow::before {
|
||||
background-color: var(--bg-color-overlay) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
.el-popper.is-dark {
|
||||
background-color: var(--bg-color-overlay) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
// 输入框组件样式(全局主题适配,兼容 Element Plus 2.x)
|
||||
.el-input {
|
||||
.el-input__wrapper {
|
||||
background-color: var(--fill-color-blank) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 1px var(--border-color) inset !important;
|
||||
}
|
||||
|
||||
&.is-focus {
|
||||
box-shadow: 0 0 0 1px var(--primary-color) inset !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-input__inner {
|
||||
background-color: transparent !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-color-placeholder) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 文本域样式(全局主题适配)
|
||||
.el-textarea {
|
||||
.el-textarea__inner {
|
||||
background-color: var(--fill-color-blank) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--text-color-placeholder) !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单选按钮组样式(全局主题适配)
|
||||
.el-radio-group {
|
||||
.el-radio {
|
||||
color: var(--text-color-primary) !important;
|
||||
|
||||
.el-radio__label {
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
.el-radio__label {
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 数字输入框样式(全局主题适配)
|
||||
.el-input-number {
|
||||
.el-input__wrapper {
|
||||
background-color: var(--fill-color-blank) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
|
||||
.el-input__inner {
|
||||
background-color: transparent !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-input-number__decrease,
|
||||
.el-input-number__increase {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
|
||||
&:hover:not(.is-disabled) {
|
||||
background-color: var(--fill-color) !important;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
background-color: var(--fill-color) !important;
|
||||
color: var(--text-color-disabled) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 开关组件样式(全局主题适配)
|
||||
.el-switch {
|
||||
--el-switch-on-color: var(--primary-color);
|
||||
--el-switch-off-color: var(--fill-color-dark);
|
||||
|
||||
.el-switch__core {
|
||||
background-color: var(--fill-color-dark) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
|
||||
&.is-checked {
|
||||
.el-switch__core {
|
||||
background-color: var(--primary-color) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
.el-switch__core {
|
||||
background-color: var(--fill-color) !important;
|
||||
border-color: var(--border-color-lighter) !important;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 警告提示样式(全局主题适配)
|
||||
.el-alert {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
border-color: var(--border-color-lighter) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
// 菜单样式
|
||||
@ -377,3 +819,295 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动条样式(全局)
|
||||
// WebKit 浏览器(Chrome, Safari, Edge)
|
||||
* {
|
||||
// Firefox 滚动条样式
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-track-color);
|
||||
|
||||
// WebKit 滚动条样式
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-track-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb-color);
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--scrollbar-thumb-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-corner {
|
||||
background: var(--scrollbar-track-color);
|
||||
}
|
||||
}
|
||||
|
||||
// 更细的滚动条样式(可选)
|
||||
.thin-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.pagination-bar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: 14px 0 0 0;
|
||||
}
|
||||
/* 标准文章样式 支持黑白主题 */
|
||||
.markdown-body {
|
||||
color: var(--text-color, #222);
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
word-break: break-word;
|
||||
background: var(--content-bg);
|
||||
transition: color 0.3s, background 0.3s;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: var(--title-color, var(--text-color, #222));
|
||||
margin-top: 1.6em;
|
||||
margin-bottom: 0.8em;
|
||||
font-family: inherit;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
h1 {
|
||||
font-size: 2.2em;
|
||||
border-bottom: 1px solid var(--border-color-light, #eaecef);
|
||||
padding-bottom: 0.4em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.8em;
|
||||
border-bottom: 1px solid var(--border-color-light, #eaecef);
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.4em;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
h5 {
|
||||
font-size: 1.06em;
|
||||
}
|
||||
h6 {
|
||||
font-size: 1em;
|
||||
color: var(--text-color-secondary, #969696);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.9em 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 2em;
|
||||
margin: 0.9em 0;
|
||||
}
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
li {
|
||||
margin: 0.7em 0 0.7em 0.7em;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color, #2d8cf0);
|
||||
text-decoration: underline;
|
||||
transition: color 0.2s;
|
||||
&:hover {
|
||||
color: var(--primary-hover, #1a73e8);
|
||||
}
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 1em auto;
|
||||
border-radius: 4px;
|
||||
background: var(--img-bg, #f7f7fa);
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid var(--blockquote-border, #d0dde9);
|
||||
background: var(--blockquote-bg, #f7f9fa);
|
||||
padding: 0.6em 1.2em;
|
||||
margin: 1.1em 0;
|
||||
color: var(--blockquote-color, #6580a0);
|
||||
font-style: italic;
|
||||
transition: background 0.3s, border-color 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: var(
|
||||
--code-font,
|
||||
"Fira Mono",
|
||||
"Menlo",
|
||||
"Consolas",
|
||||
"monospace"
|
||||
);
|
||||
background: var(--code-bg, #f4f4f4);
|
||||
border-radius: 3px;
|
||||
padding: 0.15em 0.4em;
|
||||
color: var(--code-color, #c7254e);
|
||||
font-size: 0.97em;
|
||||
margin: 0 0.1em;
|
||||
transition: background 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--pre-bg, #f6f8fa);
|
||||
border-radius: 4px;
|
||||
padding: 1em 1.2em;
|
||||
font-family: var(
|
||||
--code-font,
|
||||
"Fira Mono",
|
||||
"Menlo",
|
||||
"Consolas",
|
||||
"monospace"
|
||||
);
|
||||
font-size: 0.98em;
|
||||
overflow-x: auto;
|
||||
color: var(--pre-color, #212529);
|
||||
margin: 1.2em 0;
|
||||
transition: background 0.3s, color 0.3s;
|
||||
code {
|
||||
background: none;
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1.2em 0;
|
||||
color: inherit;
|
||||
font-size: 1em;
|
||||
background: var(--table-bg, #fff);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
border: 1px solid var(--table-border, #dee2e6);
|
||||
padding: 0.5em 1em;
|
||||
color: inherit;
|
||||
}
|
||||
th {
|
||||
background: var(--table-head-bg, #f4f8fb);
|
||||
font-weight: 600;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background: var(--table-row-even-bg, #f9fbfc);
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border-color-light, #eaecef);
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
br {
|
||||
display: block;
|
||||
margin: 0.2em 0;
|
||||
content: "";
|
||||
}
|
||||
|
||||
// 兼容 Element 表单
|
||||
.el-form-item__label,
|
||||
.el-form-item__content,
|
||||
.el-form-item__error,
|
||||
.el-form-item__error-tip {
|
||||
color: var(--text-color, #222);
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色主题支持 */
|
||||
.dark .markdown-body {
|
||||
color: #fff;
|
||||
background: #181b21;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: #fff;
|
||||
}
|
||||
h6 {
|
||||
color: #ccc;
|
||||
}
|
||||
a {
|
||||
color: #fff;
|
||||
&:hover {
|
||||
color: #e6e6e6;
|
||||
}
|
||||
}
|
||||
img {
|
||||
background: #23272b;
|
||||
}
|
||||
blockquote {
|
||||
border-left-color: #5d7896;
|
||||
background: #232a33;
|
||||
color: #cce3fa;
|
||||
}
|
||||
code {
|
||||
background: #31343b;
|
||||
color: #fff;
|
||||
}
|
||||
pre {
|
||||
background: #23272d;
|
||||
color: #fff;
|
||||
}
|
||||
table {
|
||||
background: #23272b;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
border-color: #333b47;
|
||||
color: #fff;
|
||||
}
|
||||
th {
|
||||
background: #273041;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background: #20232a;
|
||||
}
|
||||
hr {
|
||||
border-top-color: #333b47;
|
||||
}
|
||||
// Element兼容
|
||||
.el-form-item__label,
|
||||
.el-form-item__content,
|
||||
.el-form-item__error,
|
||||
.el-form-item__error-tip {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@ -91,6 +91,9 @@ const handleCommand = (command) => {
|
||||
// 清除缓存中的user数据
|
||||
localStorage.removeItem('user');
|
||||
sessionStorage.removeItem('user');
|
||||
//清除租户数据
|
||||
localStorage.removeItem('tenant');
|
||||
sessionStorage.removeItem('tenant');
|
||||
router.push('/login');
|
||||
}
|
||||
};
|
||||
|
||||
@ -8,9 +8,13 @@ import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { useAuthStore } from './stores/auth'
|
||||
import { initTheme } from './utils/theme'
|
||||
// 导入全局组件
|
||||
import WangEditor from '@/views/components/WangEditor.vue';
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
// 全局注册 WangEditor 组件
|
||||
app.component('WangEditor', WangEditor);
|
||||
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
@ -29,7 +33,7 @@ authStore.checkAuth()
|
||||
// 如果用户已登录,在应用启动时加载动态路由
|
||||
if (authStore.isLoggedIn) {
|
||||
loadAndAddDynamicRoutes().then(() => {
|
||||
console.log('应用启动时已加载动态路由');
|
||||
// console.log('应用启动时已加载动态路由');
|
||||
}).catch(err => {
|
||||
console.error('应用启动时加载动态路由失败:', err);
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@ import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import { convertMenusToRoutes } from "./dynamicRoutes";
|
||||
|
||||
// 静态路由:登录页独立,404 页面独立
|
||||
// 仪表盘路由作为静态路由,永远放在第一个位置
|
||||
const staticRoutes = [
|
||||
{
|
||||
path: "/login",
|
||||
@ -13,8 +14,22 @@ const staticRoutes = [
|
||||
path: "/",
|
||||
name: "Main",
|
||||
component: () => import("@/views/Main.vue"),
|
||||
redirect: "/dashboard", // 默认重定向,动态路由加载后会更新
|
||||
children: [], // 所有页面路由都会作为 children 添加到这里
|
||||
redirect: "/dashboard", // 默认重定向到仪表盘
|
||||
children: [
|
||||
// 仪表盘路由:静态路由,永远放在第一个
|
||||
{
|
||||
path: "dashboard",
|
||||
name: "Dashboard",
|
||||
component: () => import("@/views/dashboard/index.vue"),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: "仪表盘",
|
||||
icon: "fa-solid fa-gauge",
|
||||
id: 1,
|
||||
isStatic: true // 标记为静态路由
|
||||
}
|
||||
}
|
||||
],
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
@ -76,15 +91,18 @@ function addDynamicRoutes(menus) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dynamicRoutes = convertMenusToRoutes(menus);
|
||||
// 过滤掉 ID 为 1 的仪表盘菜单,因为它是静态路由
|
||||
const filteredMenus = menus.filter(menu => menu.id !== 1);
|
||||
|
||||
const dynamicRoutes = convertMenusToRoutes(filteredMenus);
|
||||
|
||||
// 打印路由树结构(只打印路径信息,不序列化组件)
|
||||
console.log('生成的路由树:', dynamicRoutes.map(r => ({
|
||||
path: r.path,
|
||||
name: r.name,
|
||||
hasComponent: !!r.component,
|
||||
childrenCount: r.children?.length || 0
|
||||
})));
|
||||
// console.log('生成的路由树:', dynamicRoutes.map(r => ({
|
||||
// path: r.path,
|
||||
// name: r.name,
|
||||
// hasComponent: !!r.component,
|
||||
// childrenCount: r.children?.length || 0
|
||||
// })));
|
||||
|
||||
// 获取主路由
|
||||
const mainRoute = router.getRoutes().find(r => r.name === 'Main');
|
||||
@ -93,45 +111,71 @@ function addDynamicRoutes(menus) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取现有的静态路由(仪表盘路由)
|
||||
const staticChildren = mainRoute.children || [];
|
||||
const dashboardRoute = staticChildren.find(r => r.name === 'Dashboard');
|
||||
|
||||
// 移除旧的主路由
|
||||
router.removeRoute('Main');
|
||||
|
||||
// 查找第一个有效路由作为默认重定向
|
||||
const firstRoute = findFirstValidRoute(dynamicRoutes);
|
||||
let redirectPath = "/dashboard"; // 默认值
|
||||
if (firstRoute && firstRoute.meta?.menuPath) {
|
||||
redirectPath = firstRoute.meta.menuPath;
|
||||
}
|
||||
|
||||
// 重新添加主路由,包含所有动态子路由
|
||||
// 重新添加主路由,仪表盘路由放在第一个,后面跟动态路由
|
||||
router.addRoute({
|
||||
...mainRoute,
|
||||
redirect: redirectPath,
|
||||
children: dynamicRoutes // 所有动态路由作为 Main 的 children
|
||||
redirect: "/dashboard", // 始终重定向到仪表盘
|
||||
children: [
|
||||
// 仪表盘静态路由永远放在第一个
|
||||
...(dashboardRoute ? [dashboardRoute] : []),
|
||||
// 动态路由跟在后面
|
||||
...dynamicRoutes
|
||||
]
|
||||
});
|
||||
|
||||
// 添加知识库的子路由(详情页和编辑页)
|
||||
// 直接在 Main 路由下添加完整路径的子路由
|
||||
router.addRoute('Main', {
|
||||
path: 'apps/knowledge/detail/:id',
|
||||
name: 'apps-knowledge-detail',
|
||||
component: () => import('@/views/apps/knowledge/components/detail.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '知识详情'
|
||||
}
|
||||
});
|
||||
router.addRoute('Main', {
|
||||
path: 'apps/knowledge/edit/:id',
|
||||
name: 'apps-knowledge-edit',
|
||||
component: () => import('@/views/apps/knowledge/components/edit.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
title: '编辑知识'
|
||||
}
|
||||
});
|
||||
|
||||
dynamicRoutesAdded = true;
|
||||
|
||||
// 打印路由信息用于调试
|
||||
const finalMainRoute = router.getRoutes().find(r => r.name === 'Main');
|
||||
console.log('动态路由已添加:', {
|
||||
redirect: redirectPath,
|
||||
childrenCount: dynamicRoutes.length,
|
||||
childrenPaths: finalMainRoute?.children?.map(c => ({
|
||||
path: c.path,
|
||||
name: c.name,
|
||||
metaPath: c.meta?.menuPath
|
||||
}))
|
||||
});
|
||||
// console.log('动态路由已添加:', {
|
||||
// redirect: "/dashboard",
|
||||
// staticRoutesCount: dashboardRoute ? 1 : 0,
|
||||
// dynamicRoutesCount: dynamicRoutes.length,
|
||||
// totalChildrenCount: (dashboardRoute ? 1 : 0) + dynamicRoutes.length,
|
||||
// childrenPaths: finalMainRoute?.children?.map(c => ({
|
||||
// path: c.path,
|
||||
// name: c.name,
|
||||
// isStatic: c.meta?.isStatic || false,
|
||||
// metaPath: c.meta?.menuPath
|
||||
// }))
|
||||
// });
|
||||
|
||||
// 测试路由解析(使用完整路径)
|
||||
const testResolve = router.resolve('/dashboard');
|
||||
console.log('测试路由解析 /dashboard:', {
|
||||
matched: testResolve.matched.map(m => ({ name: m.name, path: m.path })),
|
||||
fullPath: testResolve.fullPath,
|
||||
name: testResolve.name,
|
||||
hasMatched: testResolve.matched.length > 0
|
||||
});
|
||||
// console.log('测试路由解析 /dashboard:', {
|
||||
// matched: testResolve.matched.map(m => ({ name: m.name, path: m.path })),
|
||||
// fullPath: testResolve.fullPath,
|
||||
// name: testResolve.name,
|
||||
// hasMatched: testResolve.matched.length > 0
|
||||
// });
|
||||
}
|
||||
|
||||
// 查找第一个有效的路由(有组件的路由)
|
||||
|
||||
@ -13,6 +13,12 @@ service.interceptors.request.use(
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
// 对于有 body 的请求(POST、PUT、PATCH),确保设置 Content-Type
|
||||
if (config.data && ['post', 'put', 'patch'].includes(config.method?.toLowerCase())) {
|
||||
if (!config.headers['Content-Type'] && !config.headers['content-type']) {
|
||||
config.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
return config;
|
||||
},
|
||||
error => {
|
||||
|
||||
@ -61,6 +61,7 @@
|
||||
<div slot="header">
|
||||
<span>正文内容</span>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div class="markdown-body" v-html="compiledMarkdown"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
@ -123,7 +124,8 @@ async function fetchDetail() {
|
||||
try {
|
||||
const idValue = id.value as string | number
|
||||
const res = await getKnowledgeDetail(idValue)
|
||||
const data = res.data
|
||||
// API 返回的数据结构: { code: 0, data: {...}, message: "success" }
|
||||
const data = (res.code === 0 && res.data) ? res.data : (res.data || res)
|
||||
formData.value = {
|
||||
title: data.title || '',
|
||||
category: data.categoryName || '',
|
||||
@ -133,6 +135,9 @@ async function fetchDetail() {
|
||||
updateTime: data.updateTime || '',
|
||||
content: data.content || '',
|
||||
}
|
||||
if (data.title) {
|
||||
knowledgeTitle.value = data.title
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error("获取详情失败")
|
||||
}
|
||||
@ -143,10 +148,7 @@ function goBack() {
|
||||
router.push("/apps/knowledge")
|
||||
}
|
||||
function handleEdit() {
|
||||
router.push({
|
||||
name: "apps-knowledge-edit",
|
||||
params: { id: id.value as string }
|
||||
})
|
||||
router.push(`/apps/knowledge/edit/${id.value}`)
|
||||
}
|
||||
function handleDelete() {
|
||||
ElMessageBox.confirm(
|
||||
@ -160,77 +162,196 @@ function handleDelete() {
|
||||
).then(async () => {
|
||||
try {
|
||||
const idValue = id.value as string | number
|
||||
await deleteKnowledge(idValue)
|
||||
ElMessage.success("删除成功")
|
||||
goBack()
|
||||
} catch (e) {
|
||||
ElMessage.error("删除失败")
|
||||
const res = await deleteKnowledge(idValue)
|
||||
// API 返回的数据结构: { code: 0, message: "删除成功", data: null }
|
||||
if (res.code === 0) {
|
||||
ElMessage.success("删除成功")
|
||||
goBack()
|
||||
} else {
|
||||
ElMessage.error(res.message || "删除失败")
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || "删除失败")
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 全屏显示,覆盖父容器 */
|
||||
<style scoped lang="less">
|
||||
/* 知识详情页面样式 - 使用主题变量 */
|
||||
.knowledge-detail {
|
||||
position: fixed;
|
||||
top: 81px; /* 减去 header 高度 */
|
||||
left: 160px; /* 侧边栏宽度,如果收起的活是 64px */
|
||||
right: 0;
|
||||
bottom: 81px; /* 减去 footer 高度 */
|
||||
z-index: 1000;
|
||||
overflow-y: auto;
|
||||
background: var(--background-color);
|
||||
background-color: var(--bg-color-page);
|
||||
min-height: 100%;
|
||||
padding: 24px;
|
||||
transition: left var(--transition-base);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.detail-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
background: var(--card-bg);
|
||||
background-color: var(--card-bg-color);
|
||||
padding: 16px 24px;
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--box-shadow);
|
||||
border: 1px solid var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.detail-header h2 {
|
||||
margin: 0 0 0 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
color: var(--text-color-primary);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.detail-body {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
/* padding: 0 24px 0 24px; */
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
width: 320px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.header{
|
||||
// margin-bottom: 14px;
|
||||
:deep(.el-card) {
|
||||
background-color: var(--card-bg-color);
|
||||
border-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.header {
|
||||
color: var(--text-color-primary);
|
||||
font-weight: 600;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
color: var(--text-color-secondary);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
color: var(--text-color-primary);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.el-tag) {
|
||||
background-color: var(--fill-color-light);
|
||||
border-color: var(--border-color-lighter);
|
||||
color: var(--text-color-primary);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.content-panel {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
:deep(.el-card) {
|
||||
background-color: var(--card-bg-color);
|
||||
border-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
color: var(--text-color-primary);
|
||||
border-bottom-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: var(--text-color);
|
||||
color: var(--text-color-primary);
|
||||
transition: color 0.3s ease;
|
||||
|
||||
// Markdown 内容样式适配主题
|
||||
:deep(h1), :deep(h2), :deep(h3), :deep(h4), :deep(h5), :deep(h6) {
|
||||
color: var(--text-color-primary);
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
:deep(p), :deep(li), :deep(span), :deep(div) {
|
||||
color: var(--text-color-primary);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
:deep(a) {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(code) {
|
||||
background-color: var(--fill-color-light);
|
||||
color: var(--text-color-primary);
|
||||
border-color: var(--border-color-lighter);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
background-color: var(--fill-color-light);
|
||||
border-color: var(--border-color-lighter);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
:deep(ul), :deep(ol) {
|
||||
color: var(--text-color-primary);
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
:deep(blockquote) {
|
||||
border-left: 4px solid var(--primary-color);
|
||||
padding-left: 16px;
|
||||
margin: 16px 0;
|
||||
color: var(--text-color-secondary);
|
||||
background-color: var(--fill-color-extra-light);
|
||||
}
|
||||
|
||||
:deep(table) {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
|
||||
th, td {
|
||||
border: 1px solid var(--border-color-lighter);
|
||||
padding: 8px 12px;
|
||||
color: var(--text-color-primary);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--fill-color-light);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 响应式布局 */
|
||||
|
||||
@ -82,15 +82,18 @@
|
||||
<!-- 左侧编辑器 -->
|
||||
<div class="editor-panel">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<span>编辑正文</span>
|
||||
</template>
|
||||
<WangEditor v-model="formData.content" />
|
||||
</el-card>
|
||||
</div>
|
||||
<!-- 右侧预览 -->
|
||||
<div class="preview-panel">
|
||||
<el-card shadow="never">
|
||||
<div slot="header">
|
||||
<template #header>
|
||||
<span>预览效果</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="markdown-body" v-html="compiledMarkdown"></div>
|
||||
</el-card>
|
||||
</div>
|
||||
@ -193,7 +196,8 @@ const fetchDetail = async () => {
|
||||
const currentId = id.value as string;
|
||||
if (currentId && currentId !== "new") {
|
||||
const res = await getKnowledgeDetail(currentId);
|
||||
const data = res.data;
|
||||
// API 返回的数据结构: { code: 0, data: {...}, message: "success" }
|
||||
const data = (res.code === 0 && res.data) ? res.data : (res.data || res);
|
||||
|
||||
// 映射后端数据到前端表单
|
||||
// categoryName 是从数据库联查得到的分类名称
|
||||
@ -216,7 +220,8 @@ const fetchDetail = async () => {
|
||||
// Tags 可能是 JSON 字符串,需要解析
|
||||
if (data.tags) {
|
||||
try {
|
||||
formData.tags = JSON.parse(data.tags);
|
||||
const parsed = typeof data.tags === 'string' ? JSON.parse(data.tags) : data.tags;
|
||||
formData.tags = Array.isArray(parsed) ? parsed : [];
|
||||
} catch {
|
||||
// 如果解析失败,尝试作为字符串数组处理
|
||||
formData.tags = Array.isArray(data.tags) ? data.tags : [];
|
||||
@ -233,15 +238,20 @@ const fetchDetail = async () => {
|
||||
// 获取分类和标签
|
||||
const loadCategoryAndTag = async () => {
|
||||
try {
|
||||
const [catRes, tagResRes] = await Promise.all([
|
||||
getCategoryList
|
||||
? getCategoryList()
|
||||
: Promise.resolve({ data: [] }),
|
||||
getTagList ? getTagList() : Promise.resolve({ data: [] }),
|
||||
const [catRes, tagRes] = await Promise.all([
|
||||
getCategoryList(),
|
||||
getTagList(),
|
||||
]);
|
||||
categoryList.value = catRes.data || [];
|
||||
tagList.value = tagResRes.data || [];
|
||||
} catch (e) {
|
||||
|
||||
// API 返回的数据结构: { code: 0, data: [...], message: "success" }
|
||||
const categories = (catRes.code === 0 && catRes.data) ? catRes.data : (catRes.data || []);
|
||||
const tags = (tagRes.code === 0 && tagRes.data) ? tagRes.data : (tagRes.data || []);
|
||||
|
||||
categoryList.value = Array.isArray(categories) ? categories : [];
|
||||
// 标签数据可能是 { tagId, tagName } 格式,需要提取 tagName
|
||||
tagList.value = Array.isArray(tags) ? tags.map(tag => tag.tagName || tag) : [];
|
||||
} catch (e: any) {
|
||||
console.error("加载分类和标签失败:", e);
|
||||
categoryList.value = [];
|
||||
tagList.value = [];
|
||||
}
|
||||
@ -270,34 +280,65 @@ const handleSubmit = () => {
|
||||
|
||||
const currentId = id.value as string;
|
||||
|
||||
// 准备提交的数据(符合后端格式)
|
||||
const submitData = {
|
||||
id: isEdit.value ? parseInt(currentId as string) : 0,
|
||||
title: formData.title,
|
||||
categoryId: formData.categoryId,
|
||||
author: formData.author,
|
||||
content: formData.content,
|
||||
tags: JSON.stringify(formData.tags), // 转换为 JSON 字符串
|
||||
// 检查 categoryId 是否已设置
|
||||
if (!formData.categoryId) {
|
||||
ElMessage.warning("请选择分类");
|
||||
return;
|
||||
}
|
||||
|
||||
// 准备提交的数据(符合后端格式,字段名对应后端 JSON tag)
|
||||
// 确保数据类型正确:categoryId 必须是数字
|
||||
const submitData: any = {
|
||||
title: formData.title || "",
|
||||
categoryId: Number(formData.categoryId) || 0,
|
||||
author: formData.author || "",
|
||||
content: formData.content || "",
|
||||
tags: Array.isArray(formData.tags) && formData.tags.length > 0
|
||||
? JSON.stringify(formData.tags)
|
||||
: "[]",
|
||||
status: 1, // 默认已发布
|
||||
};
|
||||
|
||||
// 调试:打印提交的数据
|
||||
console.log("提交的数据:", JSON.stringify(submitData, null, 2));
|
||||
|
||||
if (isEdit.value && currentId !== "new") {
|
||||
// 编辑
|
||||
// 编辑:需要添加 id(后端 JSON tag 是 "id")
|
||||
const knowledgeId = parseInt(currentId as string);
|
||||
if (isNaN(knowledgeId) || knowledgeId <= 0) {
|
||||
ElMessage.error("知识ID无效");
|
||||
return;
|
||||
}
|
||||
// updateKnowledge 函数会将 id 合并到 data 中,所以这里不需要手动添加
|
||||
try {
|
||||
await updateKnowledge(currentId, submitData);
|
||||
ElMessage.success("保存成功");
|
||||
goBack();
|
||||
const res = await updateKnowledge(knowledgeId, submitData);
|
||||
// API 返回的数据结构: { code: 0, message: "更新成功", data: null }
|
||||
if (res.code === 0) {
|
||||
ElMessage.success("保存成功");
|
||||
goBack();
|
||||
} else {
|
||||
ElMessage.error(res.message || "保存失败");
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || "保存失败");
|
||||
console.error("保存失败:", e);
|
||||
const errorMessage = e.response?.data?.message || e.message || "保存失败";
|
||||
ElMessage.error(errorMessage);
|
||||
}
|
||||
} else {
|
||||
// 新建
|
||||
// 新建:不包含 id 字段
|
||||
try {
|
||||
await createKnowledge(submitData);
|
||||
ElMessage.success("创建成功");
|
||||
goBack();
|
||||
const res = await createKnowledge(submitData);
|
||||
// API 返回的数据结构: { code: 0, message: "创建成功", data: { id: ... } }
|
||||
if (res.code === 0) {
|
||||
ElMessage.success("创建成功");
|
||||
goBack();
|
||||
} else {
|
||||
ElMessage.error(res.message || "创建失败");
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || "创建失败");
|
||||
console.error("创建失败:", e);
|
||||
const errorMessage = e.response?.data?.message || e.message || "创建失败";
|
||||
ElMessage.error(errorMessage);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -332,41 +373,54 @@ defineExpose({
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style scoped lang="less">
|
||||
.knowledge-edit {
|
||||
position: fixed;
|
||||
top: 81px;
|
||||
left: 160px;
|
||||
right: 0;
|
||||
bottom: 81px;
|
||||
z-index: 1000;
|
||||
background: var(--background-color);
|
||||
background-color: var(--bg-color-page);
|
||||
min-height: 100%;
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
transition: left var(--transition-base);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.edit-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
background: var(--card-bg);
|
||||
background-color: var(--card-bg-color);
|
||||
padding: 16px 24px;
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--box-shadow);
|
||||
border: 1px solid var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.edit-header h2 {
|
||||
margin: 0 0 0 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
color: var(--text-color-primary);
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
/* 基本信息区 */
|
||||
.edit-meta {
|
||||
margin-bottom: 20px;
|
||||
|
||||
:deep(.el-card) {
|
||||
background-color: var(--card-bg-color);
|
||||
border-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
color: var(--text-color-primary);
|
||||
border-bottom-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
:deep(.el-divider) {
|
||||
border-color: var(--border-color-lighter);
|
||||
transition: border-color 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.meta-actions {
|
||||
@ -378,60 +432,106 @@ defineExpose({
|
||||
.edit-body {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
height: calc(100vh - 320px);
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.editor-panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
:deep(.el-card) {
|
||||
background-color: var(--card-bg-color);
|
||||
border-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
color: var(--text-color-primary);
|
||||
border-bottom-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 编辑器容器样式 */
|
||||
.editor-panel {
|
||||
position: relative;
|
||||
:deep(.el-card) {
|
||||
background-color: var(--card-bg-color);
|
||||
border-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
color: var(--text-color-primary);
|
||||
border-bottom-color: var(--border-color-lighter);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
color: var(--text-color);
|
||||
color: var(--text-color-primary);
|
||||
min-height: 400px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
transition: color 0.3s ease;
|
||||
|
||||
.markdown-body :deep(h1),
|
||||
.markdown-body :deep(h2),
|
||||
.markdown-body :deep(h3),
|
||||
.markdown-body :deep(h4),
|
||||
.markdown-body :deep(h5),
|
||||
.markdown-body :deep(h6) {
|
||||
color: var(--text-color);
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
:deep(h1),
|
||||
:deep(h2),
|
||||
:deep(h3),
|
||||
:deep(h4),
|
||||
:deep(h5),
|
||||
:deep(h6) {
|
||||
color: var(--text-color-primary);
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.markdown-body :deep(p) {
|
||||
color: var(--text-secondary);
|
||||
margin: 8px 0;
|
||||
}
|
||||
:deep(p), :deep(li), :deep(span), :deep(div) {
|
||||
color: var(--text-color-primary);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.markdown-body :deep(code) {
|
||||
background-color: var(--background-hover);
|
||||
color: var(--text-color);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
:deep(a) {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.markdown-body :deep(pre) {
|
||||
background-color: var(--background-hover);
|
||||
padding: 12px;
|
||||
border-radius: var(--border-radius);
|
||||
overflow-x: auto;
|
||||
:deep(code) {
|
||||
background-color: var(--fill-color-light);
|
||||
color: var(--text-color-primary);
|
||||
border-color: var(--border-color-lighter);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
background-color: var(--fill-color-light);
|
||||
border-color: var(--border-color-lighter);
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
|
||||
code {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(img) {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
margin: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式布局 */
|
||||
@ -454,9 +554,23 @@ defineExpose({
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
:deep() {
|
||||
.el-form-item--default {
|
||||
margin-bottom: 0px;
|
||||
:deep(.el-form-item--default) {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper), :deep(.el-textarea__inner) {
|
||||
background-color: var(--fill-color-blank) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
&::placeholder {
|
||||
color: var(--text-color-placeholder) !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -8,15 +8,6 @@
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-select
|
||||
v-model="searchType"
|
||||
placeholder="知识库"
|
||||
size="default"
|
||||
class="search-select"
|
||||
style="width: auto"
|
||||
>
|
||||
<el-option label="知识库" value="knowledge" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
placeholder="请输入关键字、产品编码进行查询"
|
||||
@ -181,7 +172,7 @@
|
||||
import { ref, reactive, computed, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { getKnowledgeList, deleteKnowledge } from "@/api/knowlege";
|
||||
import { getKnowledgeList, deleteKnowledge } from "@/api/knowledge";
|
||||
|
||||
// 类型定义
|
||||
interface Knowledge {
|
||||
@ -286,8 +277,15 @@ async function fetchRepoList() {
|
||||
keyword: keyword.value, // 支持关键词搜索
|
||||
});
|
||||
|
||||
repoList.value = result.list || [];
|
||||
total.value = result.total || 0;
|
||||
// API 返回的数据结构: { code: 0, data: { list: [...], total: 1 }, message: "success" }
|
||||
if (result.code === 0 && result.data) {
|
||||
repoList.value = result.data.list || [];
|
||||
total.value = result.data.total || 0;
|
||||
} else {
|
||||
// 兼容旧的数据结构(直接返回 list 和 total)
|
||||
repoList.value = result.list || result.data?.list || [];
|
||||
total.value = result.total || result.data?.total || 0;
|
||||
}
|
||||
|
||||
// 更新统计数据(避免重复调用 API)
|
||||
updateStats();
|
||||
@ -302,27 +300,18 @@ async function fetchRepoList() {
|
||||
}
|
||||
|
||||
function handleCreate() {
|
||||
// 跳转到新建知识库页面
|
||||
router.push({
|
||||
name: "apps-knowledge-edit",
|
||||
params: { id: "new" },
|
||||
});
|
||||
// 跳转到新建知识库页面,使用路径跳转
|
||||
router.push(`/apps/knowledge/edit/new`);
|
||||
}
|
||||
|
||||
function handleView(repo: Knowledge) {
|
||||
// 跳转到知识详情页面,使用 params 参数传递 id
|
||||
router.push({
|
||||
name: "apps-knowledge-detail",
|
||||
params: { id: repo.id.toString() },
|
||||
});
|
||||
// 跳转到知识详情页面,使用路径跳转
|
||||
router.push(`/apps/knowledge/detail/${repo.id}`);
|
||||
}
|
||||
|
||||
function handleEdit(repo: Knowledge) {
|
||||
// 跳转到编辑知识页面,使用 params 参数传递 id
|
||||
router.push({
|
||||
name: "apps-knowledge-edit",
|
||||
params: { id: repo.id.toString() },
|
||||
});
|
||||
// 跳转到编辑知识页面,使用路径跳转
|
||||
router.push(`/apps/knowledge/edit/${repo.id}`);
|
||||
}
|
||||
|
||||
function handleDelete(repo: Knowledge) {
|
||||
@ -393,98 +382,74 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// 顶部横幅样式
|
||||
// 顶部横幅样式 - 简约大气版本
|
||||
.hero.new-style {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 60px 20px;
|
||||
margin-bottom: 30px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--card-bg-color);
|
||||
padding: 48px 24px;
|
||||
margin-bottom: 32px;
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--box-shadow-dark);
|
||||
transition: box-shadow 0.3s ease;
|
||||
|
||||
// 背景装饰
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -10%;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 50%;
|
||||
filter: blur(80px);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -30%;
|
||||
left: -10%;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 50%;
|
||||
filter: blur(60px);
|
||||
}
|
||||
border: 1px solid var(--border-color-lighter);
|
||||
box-shadow: var(--box-shadow);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.hero-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 42px;
|
||||
color: white;
|
||||
margin: 0 0 12px;
|
||||
font-weight: 700;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||
letter-spacing: -0.5px;
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color-primary);
|
||||
margin: 0 0 8px;
|
||||
letter-spacing: -0.3px;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 18px;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
margin: 0 0 30px;
|
||||
font-weight: 300;
|
||||
font-size: 15px;
|
||||
color: var(--text-color-secondary);
|
||||
margin: 0 0 32px;
|
||||
font-weight: 400;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
max-width: 900px;
|
||||
margin: 0 auto 30px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto 24px;
|
||||
background: var(--fill-color-blank);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--box-shadow);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: none;
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid var(--border-color-lighter);
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--box-shadow-dark);
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.search-select {
|
||||
border-right: 1px solid var(--border-color-lighter);
|
||||
border-radius: 0;
|
||||
transition: border-color 0.3s ease;
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 12px 20px;
|
||||
font-weight: 500;
|
||||
background-color: var(--fill-color-blank);
|
||||
padding: 12px 16px;
|
||||
font-weight: 400;
|
||||
background-color: transparent;
|
||||
color: var(--text-color-primary);
|
||||
}
|
||||
}
|
||||
@ -492,30 +457,36 @@ onMounted(() => {
|
||||
.search-input {
|
||||
flex: 1;
|
||||
|
||||
:deep(.el-input__wrapper){
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
font-size: 16px;
|
||||
box-shadow: none;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
padding: 12px 16px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background-color: var(--primary-color);
|
||||
border: none;
|
||||
padding: 0 32px;
|
||||
font-size: 16px;
|
||||
border-left: 1px solid;
|
||||
padding: 0 28px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
color: #fff;
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 0;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(135deg, #764ba2 0%, #667eea 100%);
|
||||
transform: scale(1.05);
|
||||
background-color: var(--primary-color);
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
|
||||
@ -524,29 +495,30 @@ onMounted(() => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.hot-label {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.hot-tag {
|
||||
background-color: rgba(255, 255, 255, 0.25);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
background-color: var(--fill-color-light);
|
||||
color: var(--text-color-primary);
|
||||
border: 1px solid var(--border-color-lighter);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
transition: all 0.2s ease;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.4);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
background-color: var(--fill-color);
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -933,43 +905,34 @@ onMounted(() => {
|
||||
// 响应式调整
|
||||
@media (max-width: 768px) {
|
||||
.hero.new-style {
|
||||
padding: 40px 15px;
|
||||
border-radius: 0;
|
||||
padding: 32px 16px;
|
||||
border-radius: 8px;
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
|
||||
.search-select,
|
||||
.search-input {
|
||||
:deep(.el-input__inner) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.search-select {
|
||||
:deep(.el-input__inner) {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border-color-lighter);
|
||||
}
|
||||
|
||||
.search-button {
|
||||
border-radius: 0 0 8px 8px;
|
||||
width: 100%;
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.hot-search {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
621
pc/src/views/components/WangEditor.vue
Normal file
621
pc/src/views/components/WangEditor.vue
Normal file
@ -0,0 +1,621 @@
|
||||
<template>
|
||||
<div class="wang-editor-wrapper" :class="{ focused: isFocused }">
|
||||
<div ref="toolbarRef" class="toolbar-container"></div>
|
||||
<div ref="editorRef" class="editor-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import '@wangeditor/editor/dist/css/style.css';
|
||||
import { uploadFile } from '@/api/file';
|
||||
|
||||
interface Props {
|
||||
modelValue: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: '',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string];
|
||||
}>();
|
||||
|
||||
const toolbarRef = ref<HTMLDivElement>();
|
||||
const editorRef = ref<HTMLDivElement>();
|
||||
const isFocused = ref(false);
|
||||
let editorInstance: any = null;
|
||||
let isDestroyed = false;
|
||||
|
||||
// 获取上传文件的 URL
|
||||
const getUploadUrl = (): string => {
|
||||
return import.meta.env.VITE_API_BASE_URL;
|
||||
};
|
||||
|
||||
// 获取 Authorization Header
|
||||
const getAuthHeaders = () => {
|
||||
const token = localStorage.getItem('token');
|
||||
return token ? { Authorization: `Bearer ${token}` } : {};
|
||||
};
|
||||
|
||||
// 上传图片处理函数
|
||||
const handleUploadImage = async (file: File, insertFn: (url: string, alt?: string, href?: string) => void) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response: any = await uploadFile(formData, {
|
||||
category: '编辑器',
|
||||
});
|
||||
|
||||
// axios 拦截器已经返回了 response.data,所以直接使用 response
|
||||
if (response?.success) {
|
||||
const fileUrl = response.data.file_url; // 例如: /uploads/2024/01/15/xxx.jpg
|
||||
const baseUrl = getUploadUrl() || window.location.origin;
|
||||
|
||||
// 处理URL:如果已经是完整URL则直接使用,否则拼接baseUrl
|
||||
let fullUrl = fileUrl;
|
||||
if (!fileUrl.startsWith('http')) {
|
||||
// 确保URL以/开头,baseUrl不以/结尾
|
||||
const base = baseUrl.replace(/\/$/, '');
|
||||
const url = fileUrl.startsWith('/') ? fileUrl : '/' + fileUrl;
|
||||
fullUrl = `${base}${url}`;
|
||||
}
|
||||
|
||||
console.log('图片上传成功,URL:', fullUrl);
|
||||
insertFn(fullUrl, file.name, fullUrl);
|
||||
ElMessage.success('图片上传成功');
|
||||
} else {
|
||||
ElMessage.error('上传失败:' + (response?.message || '未知错误'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Upload error:', error);
|
||||
ElMessage.error('上传失败:' + (error.message || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
// 上传视频处理函数
|
||||
const handleUploadVideo = async (file: File, insertFn: (url: string, poster?: string) => void) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response: any = await uploadFile(formData, {
|
||||
category: '编辑器',
|
||||
});
|
||||
|
||||
if (response?.success) {
|
||||
const fileUrl = response.data.file_url;
|
||||
const baseUrl = getUploadUrl() || window.location.origin;
|
||||
|
||||
let fullUrl = fileUrl;
|
||||
if (!fileUrl.startsWith('http')) {
|
||||
const base = baseUrl.replace(/\/$/, '');
|
||||
const url = fileUrl.startsWith('/') ? fileUrl : '/' + fileUrl;
|
||||
fullUrl = `${base}${url}`;
|
||||
}
|
||||
|
||||
console.log('视频上传成功,URL:', fullUrl);
|
||||
insertFn(fullUrl, '');
|
||||
ElMessage.success('视频上传成功');
|
||||
} else {
|
||||
ElMessage.error('上传失败:' + (response?.message || '未知错误'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Upload error:', error);
|
||||
ElMessage.error('上传失败:' + (error.message || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
// 上传附件处理函数
|
||||
const handleUploadAttachment = async (file: File, insertFn: (url: string, text?: string) => void) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response: any = await uploadFile(formData, {
|
||||
category: '编辑器',
|
||||
});
|
||||
|
||||
if (response?.success) {
|
||||
const fileUrl = response.data.file_url;
|
||||
const baseUrl = getUploadUrl() || window.location.origin;
|
||||
|
||||
let fullUrl = fileUrl;
|
||||
if (!fileUrl.startsWith('http')) {
|
||||
const base = baseUrl.replace(/\/$/, '');
|
||||
const url = fileUrl.startsWith('/') ? fileUrl : '/' + fileUrl;
|
||||
fullUrl = `${base}${url}`;
|
||||
}
|
||||
|
||||
console.log('附件上传成功,URL:', fullUrl);
|
||||
insertFn(fullUrl, file.name);
|
||||
ElMessage.success('附件上传成功');
|
||||
} else {
|
||||
ElMessage.error('上传失败:' + (response?.message || '未知错误'));
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Upload error:', error);
|
||||
ElMessage.error('上传失败:' + (error.message || '未知错误'));
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化编辑器
|
||||
const initEditor = async () => {
|
||||
if (!editorRef.value || !toolbarRef.value || isDestroyed) return;
|
||||
|
||||
try {
|
||||
// 动态导入 wangEditor
|
||||
const { createEditor, createToolbar } = await import('@wangeditor/editor');
|
||||
|
||||
const editorConfig = {
|
||||
placeholder: '请输入内容...',
|
||||
onChange: (editor: any) => {
|
||||
if (!isDestroyed) {
|
||||
const html = editor.getHtml();
|
||||
emit('update:modelValue', html);
|
||||
}
|
||||
},
|
||||
// 自定义上传配置
|
||||
MENU_CONF: {
|
||||
// 图片上传配置
|
||||
uploadImage: {
|
||||
server: '/api/files',
|
||||
fieldName: 'file',
|
||||
headers: getAuthHeaders(),
|
||||
customUpload: async (file: File, insertFn: (url: string, alt?: string, href?: string) => void) => {
|
||||
await handleUploadImage(file, insertFn);
|
||||
},
|
||||
allowedFileTypes: ['image/*'],
|
||||
maxFileSize: 5 * 1024 * 1024, // 5MB
|
||||
},
|
||||
// 视频上传配置
|
||||
uploadVideo: {
|
||||
server: '/api/files',
|
||||
fieldName: 'file',
|
||||
headers: getAuthHeaders(),
|
||||
customUpload: async (file: File, insertFn: (url: string, poster?: string) => void) => {
|
||||
await handleUploadVideo(file, insertFn);
|
||||
},
|
||||
allowedFileTypes: ['video/*'],
|
||||
maxFileSize: 100 * 1024 * 1024, // 100MB
|
||||
},
|
||||
// 附件上传配置
|
||||
uploadAttachment: {
|
||||
server: '/api/files',
|
||||
fieldName: 'file',
|
||||
headers: getAuthHeaders(),
|
||||
customUpload: async (file: File, insertFn: (url: string, text?: string) => void) => {
|
||||
await handleUploadAttachment(file, insertFn);
|
||||
},
|
||||
allowedFileTypes: ['*'],
|
||||
maxFileSize: 50 * 1024 * 1024, // 50MB
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 创建编辑器
|
||||
editorInstance = createEditor({
|
||||
selector: editorRef.value,
|
||||
html: props.modelValue || '',
|
||||
config: editorConfig,
|
||||
mode: 'default',
|
||||
});
|
||||
|
||||
// 创建工具栏
|
||||
createToolbar({
|
||||
editor: editorInstance,
|
||||
selector: toolbarRef.value,
|
||||
config: {},
|
||||
});
|
||||
|
||||
// 监听编辑器焦点事件(兼容不同版本的 API)
|
||||
nextTick(() => {
|
||||
if (editorInstance) {
|
||||
// 方法1: 使用 on 方法(如果存在)
|
||||
if (typeof editorInstance.on === 'function') {
|
||||
editorInstance.on('focus', () => {
|
||||
isFocused.value = true;
|
||||
});
|
||||
|
||||
editorInstance.on('blur', () => {
|
||||
isFocused.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 方法2: 直接在 DOM 元素上监听
|
||||
const editorDom = editorRef.value;
|
||||
if (editorDom) {
|
||||
const textDom = editorDom.querySelector('.w-e-text');
|
||||
if (textDom) {
|
||||
textDom.addEventListener('focus', () => {
|
||||
isFocused.value = true;
|
||||
});
|
||||
textDom.addEventListener('blur', () => {
|
||||
isFocused.value = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize editor:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听外部值变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (editorInstance && newVal !== editorInstance.getHtml()) {
|
||||
editorInstance.setHtml(newVal || '');
|
||||
}
|
||||
});
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
clear: () => {
|
||||
if (editorInstance) {
|
||||
editorInstance.clear();
|
||||
}
|
||||
},
|
||||
getContent: () => {
|
||||
if (editorInstance) {
|
||||
return editorInstance.getHtml();
|
||||
}
|
||||
return props.modelValue;
|
||||
},
|
||||
setContent: (content: string) => {
|
||||
if (editorInstance) {
|
||||
editorInstance.setHtml(content || '');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
initEditor();
|
||||
}, 100);
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
isDestroyed = true;
|
||||
if (editorInstance) {
|
||||
editorInstance.destroy();
|
||||
editorInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.wang-editor-wrapper {
|
||||
width: 100%;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background-color: var(--fill-color-blank);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.toolbar-container {
|
||||
border-bottom: 1px solid var(--border-color-lighter);
|
||||
background-color: var(--fill-color-light);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
min-height: 400px;
|
||||
background-color: var(--fill-color-blank);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
// 全局样式:WangEditor 编辑器主题适配
|
||||
.wang-editor-wrapper {
|
||||
// 工具栏样式
|
||||
:deep(.w-e-toolbar) {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
border-bottom-color: var(--border-color-lighter) !important;
|
||||
border-bottom: 1px solid var(--border-color-lighter) !important;
|
||||
|
||||
// 工具栏按钮项
|
||||
.w-e-bar-item {
|
||||
button {
|
||||
color: var(--text-color-primary) !important;
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
transition: all 0.2s ease !important;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background-color: var(--fill-color) !important;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
&:active:not(.disabled) {
|
||||
background-color: var(--fill-color-dark) !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--fill-color-dark) !important;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: var(--text-color-disabled) !important;
|
||||
cursor: not-allowed !important;
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 按钮组分隔线
|
||||
&::after {
|
||||
background-color: var(--border-color-lighter) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 工具栏分割线
|
||||
.w-e-bar-divider {
|
||||
background-color: var(--border-color-lighter) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑器文本容器
|
||||
:deep(.w-e-text-container) {
|
||||
background-color: var(--fill-color-blank) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
border: none !important;
|
||||
|
||||
// 编辑器内容区域
|
||||
.w-e-text {
|
||||
color: var(--text-color-primary) !important;
|
||||
background-color: transparent !important;
|
||||
min-height: 400px !important;
|
||||
|
||||
// 编辑器焦点状态
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
// 编辑器内的段落
|
||||
p {
|
||||
color: var(--text-color-primary) !important;
|
||||
margin: 0.5em 0 !important;
|
||||
}
|
||||
|
||||
// 编辑器内的标题
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--text-color-primary) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
// 编辑器内的链接
|
||||
a {
|
||||
color: var(--primary-color) !important;
|
||||
text-decoration: underline !important;
|
||||
|
||||
&:hover {
|
||||
color: var(--primary-color) !important;
|
||||
opacity: 0.8 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑器内的代码
|
||||
code {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
border: 1px solid var(--border-color-lighter) !important;
|
||||
padding: 2px 6px !important;
|
||||
border-radius: 3px !important;
|
||||
}
|
||||
|
||||
// 编辑器内的代码块
|
||||
pre {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
border: 1px solid var(--border-color-lighter) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
border-radius: 4px !important;
|
||||
|
||||
code {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑器内的引用
|
||||
blockquote {
|
||||
border-left: 4px solid var(--border-color) !important;
|
||||
background-color: var(--fill-color-light) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
padding: 0.6em 1.2em !important;
|
||||
margin: 1em 0 !important;
|
||||
}
|
||||
|
||||
// 编辑器内的表格
|
||||
table {
|
||||
border-collapse: collapse !important;
|
||||
border: 1px solid var(--border-color-lighter) !important;
|
||||
|
||||
th, td {
|
||||
border: 1px solid var(--border-color-lighter) !important;
|
||||
background-color: var(--fill-color-blank) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑器内的列表
|
||||
ul, ol {
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
li {
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 占位符样式
|
||||
.placeholder {
|
||||
color: var(--text-color-placeholder) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单下拉框
|
||||
:deep(.w-e-drop-panel) {
|
||||
background-color: var(--bg-color-overlay) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: var(--box-shadow) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
|
||||
.w-e-list-item {
|
||||
color: var(--text-color-primary) !important;
|
||||
transition: all 0.2s ease !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: var(--text-color-disabled) !important;
|
||||
cursor: not-allowed !important;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分隔线
|
||||
.w-e-drop-panel-divider {
|
||||
background-color: var(--border-color-lighter) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 工具栏下拉菜单
|
||||
:deep(.w-e-toolbar-menu) {
|
||||
background-color: var(--bg-color-overlay) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: var(--box-shadow) !important;
|
||||
|
||||
.w-e-menu-item {
|
||||
color: var(--text-color-primary) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模态框
|
||||
:deep(.w-e-modal) {
|
||||
background-color: var(--bg-color-overlay) !important;
|
||||
border: 1px solid var(--border-color) !important;
|
||||
box-shadow: var(--box-shadow) !important;
|
||||
|
||||
.w-e-modal-header {
|
||||
border-bottom: 1px solid var(--border-color-lighter) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
}
|
||||
|
||||
.w-e-modal-body {
|
||||
background-color: var(--bg-color-overlay) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
|
||||
input, textarea, select {
|
||||
background-color: var(--fill-color-blank) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--primary-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.w-e-modal-footer {
|
||||
border-top: 1px solid var(--border-color-lighter) !important;
|
||||
|
||||
button {
|
||||
background-color: var(--fill-color-blank) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--fill-color-light) !important;
|
||||
border-color: var(--primary-color) !important;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 工具栏图标颜色
|
||||
:deep(.w-e-bar-item svg),
|
||||
:deep(.w-e-bar-item .w-e-icon) {
|
||||
fill: var(--text-color-primary) !important;
|
||||
color: var(--text-color-primary) !important;
|
||||
transition: fill 0.2s ease, color 0.2s ease !important;
|
||||
}
|
||||
|
||||
:deep(.w-e-bar-item:hover:not(.disabled) svg),
|
||||
:deep(.w-e-bar-item:hover:not(.disabled) .w-e-icon),
|
||||
:deep(.w-e-bar-item.active svg),
|
||||
:deep(.w-e-bar-item.active .w-e-icon) {
|
||||
fill: var(--primary-color) !important;
|
||||
color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
// 工具栏按钮组
|
||||
:deep(.w-e-bar-divider) {
|
||||
background-color: var(--border-color-lighter) !important;
|
||||
}
|
||||
|
||||
// 编辑区域边框
|
||||
&.focused {
|
||||
border-color: var(--primary-color) !important;
|
||||
box-shadow: 0 0 0 1px var(--primary-color) inset !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 深色主题额外适配
|
||||
[data-theme="dark"] {
|
||||
.wang-editor-wrapper {
|
||||
:deep(.w-e-text-container .w-e-text) {
|
||||
// 编辑器内的图片边框
|
||||
img {
|
||||
border: 1px solid var(--border-color-lighter) !important;
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
|
||||
// 编辑器内的水平线
|
||||
hr {
|
||||
border-top-color: var(--border-color-lighter) !important;
|
||||
}
|
||||
|
||||
// 编辑器内的表格行交替颜色
|
||||
table tr:nth-child(even) {
|
||||
background-color: var(--fill-color-extra-light) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { login } from "@/api/login";
|
||||
@ -7,24 +7,64 @@ import { login } from "@/api/login";
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const tenant = ref('')
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const passwordVisible = ref(false)
|
||||
const rememberMe = ref(false)
|
||||
const loading = ref(false)
|
||||
const errorMsg = ref('')
|
||||
|
||||
// 自动填充记住的用户名、租户
|
||||
onMounted(() => {
|
||||
const savedUser = localStorage.getItem('loginUsername')
|
||||
const savedTenant = localStorage.getItem('tenant')
|
||||
const savedRemember = localStorage.getItem('loginRememberMe')
|
||||
if (savedRemember === 'true') {
|
||||
tenant.value = savedTenant || ''
|
||||
username.value = savedUser || ''
|
||||
rememberMe.value = true
|
||||
}
|
||||
})
|
||||
|
||||
// 写入租户数据到缓存工具函数
|
||||
function cacheTenant(tenantValue) {
|
||||
localStorage.setItem('tenant', tenantValue)
|
||||
sessionStorage.setItem('tenant', tenantValue)
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
errorMsg.value = ''
|
||||
if (!username.value || !password.value) {
|
||||
errorMsg.value = '请输入用户名和密码'
|
||||
if (!tenant.value || !username.value || !password.value) {
|
||||
errorMsg.value = '请输入租户名称、用户名和密码'
|
||||
return
|
||||
}
|
||||
// 验证租户名称不能为空
|
||||
const tenantName = tenant.value.trim()
|
||||
if (!tenantName) {
|
||||
errorMsg.value = '租户名称不能为空'
|
||||
return
|
||||
}
|
||||
// 记住我本地存储
|
||||
if (rememberMe.value) {
|
||||
localStorage.setItem('loginUsername', username.value)
|
||||
localStorage.setItem('loginRememberMe', 'true')
|
||||
localStorage.setItem('tenant', tenantName)
|
||||
} else {
|
||||
localStorage.removeItem('loginUsername')
|
||||
localStorage.setItem('loginRememberMe', 'false')
|
||||
// 仍要写tenant到localStorage(以便系统获取租户上下文)
|
||||
localStorage.setItem('tenant', tenantName)
|
||||
}
|
||||
// 写入租户到缓存(无论记住与否都要写,便于系统获取租户上下文)
|
||||
cacheTenant(tenantName)
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await login(username.value, password.value)
|
||||
// 检查响应结构:code === 0 表示成功,数据在 data 中
|
||||
const res = await login(username.value, password.value, tenantName)
|
||||
if (res && res.code === 0 && res.data) {
|
||||
// 保存登录信息(token 和用户信息)
|
||||
authStore.setLoginInfo(res.data)
|
||||
// 登录如果有返回租户信息,可以用res.data.tenant,但这里优先使用当前登录租户输入
|
||||
cacheTenant(tenantName)
|
||||
router.push({ path: '/dashboard' })
|
||||
} else {
|
||||
errorMsg.value = res.message || '登录失败'
|
||||
@ -35,90 +75,417 @@ const handleLogin = async () => {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转注册、忘记密码页面
|
||||
const goRegister = () => {
|
||||
router.push({ path: '/register' })
|
||||
}
|
||||
const goForget = () => {
|
||||
router.push({ path: '/forget' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-form">
|
||||
<h2>登录</h2>
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
placeholder="用户名"
|
||||
autocomplete="username"
|
||||
/>
|
||||
<div class="login-bg">
|
||||
<div class="login-card">
|
||||
<div class="login-side">
|
||||
<div class="brand">
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
|
||||
<rect width="48" height="48" rx="18" fill="#eef8fc"/>
|
||||
<circle cx="24" cy="24" r="14" fill="#52a8ff" opacity="0.15"/>
|
||||
<circle cx="24" cy="24" r="9" fill="#3b7ddd" opacity="0.12"/>
|
||||
<text x="24" y="30" text-anchor="middle" fill="#2d5fa7" font-size="16" font-family="Arial" font-weight="bold">Mete</text>
|
||||
</svg>
|
||||
<span class="brand-title">后台管理系统</span>
|
||||
</div>
|
||||
<div class="illus">
|
||||
<svg viewBox="0 0 300 160" style="max-width:100%;" fill="none">
|
||||
<ellipse cx="150" cy="140" rx="120" ry="16" fill="#edf4fd" />
|
||||
<rect x="57" y="58" width="60" height="40" rx="12" fill="#64b6f7"/>
|
||||
<rect x="125" y="46" width="110" height="64" rx="14" fill="#389bf7" opacity="0.11" />
|
||||
<rect x="136" y="60" width="60" height="41" rx="10" fill="#b8e1ff"/>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- 版权信息 -->
|
||||
<div class="copyright">
|
||||
© 2024 Mete 管理系统
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
placeholder="密码"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<div class="login-panel">
|
||||
<h2 class="login-title">欢迎登录</h2>
|
||||
<div class="login-desc">请填写您的账号信息</div>
|
||||
<div class="form-group icon-input-group">
|
||||
<span class="input-icon">
|
||||
<!-- 租户图标 -->
|
||||
<svg width="19" height="19" viewBox="0 0 20 20" fill="none">
|
||||
<rect x="2.2" y="4.2" width="15.6" height="11.6" rx="2" stroke="#4da1ff" stroke-width="1.4"/>
|
||||
<rect x="6" y="8" width="8" height="4" rx="1" fill="#b9dbff"/>
|
||||
<rect x="6.7" y="8.7" width="6.6" height="2.6" rx="0.7" fill="#fff"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
v-model="tenant"
|
||||
type="text"
|
||||
placeholder="租户名称"
|
||||
autocomplete="organization"
|
||||
class="input input-with-icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group icon-input-group">
|
||||
<span class="input-icon">
|
||||
<!-- 用户图标 -->
|
||||
<svg width="19" height="19" viewBox="0 0 20 20" fill="none">
|
||||
<circle cx="10" cy="7" r="3.2" stroke="#4da1ff" stroke-width="1.4"/>
|
||||
<ellipse cx="10" cy="14.1" rx="5.5" ry="3.3" stroke="#4da1ff" stroke-width="1.4"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
placeholder="用户名"
|
||||
autocomplete="username"
|
||||
class="input input-with-icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group icon-input-group">
|
||||
<span class="input-icon">
|
||||
<!-- 密码图标 -->
|
||||
<svg width="19" height="19" viewBox="0 0 20 20" fill="none">
|
||||
<rect x="3" y="8" width="14" height="7" rx="2" stroke="#4da1ff" stroke-width="1.4"/>
|
||||
<circle cx="10" cy="11.5" r="1.5" stroke="#4da1ff" stroke-width="1.2"/>
|
||||
<rect x="7" y="5" width="6" height="3" rx="1.5" stroke="#4da1ff" stroke-width="1"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input
|
||||
v-model="password"
|
||||
:type="passwordVisible ? 'text' : 'password'"
|
||||
placeholder="密码"
|
||||
autocomplete="current-password"
|
||||
class="input input-with-icon"
|
||||
/>
|
||||
<span class="visible-btn" @click="passwordVisible = !passwordVisible" :title="passwordVisible ? '隐藏密码' : '显示密码'">
|
||||
<svg v-if="passwordVisible" width="20" height="20" fill="none" viewBox="0 0 20 20">
|
||||
<!-- 可见(eye)图标 -->
|
||||
<path d="M2 10c2-4 5-6 8-6s6 2 8 6c-2 4-5 6-8 6s-6-2-8-6z" stroke="#7bb7fa" stroke-width="1.4" fill="#eef5ff"/>
|
||||
<circle cx="10" cy="10" r="2.5" stroke="#3794f7" stroke-width="1.4" fill="#fff"/>
|
||||
</svg>
|
||||
<svg v-else width="20" height="20" fill="none" viewBox="0 0 20 20">
|
||||
<!-- 不可见(eye-off)图标 -->
|
||||
<path d="M2 10c2-4 5-6 8-6s6 2 8 6c-2 4-5 6-8 6s-6-2-8-6z" stroke="#b7c7db" stroke-width="1.3" fill="#f2f6fd"/>
|
||||
<path d="M5 15L15 5" stroke="#b7c7db" stroke-width="1.2"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div class="remember-me-row">
|
||||
<label class="remember-me-label">
|
||||
<input type="checkbox" v-model="rememberMe" class="remember-me-checkbox" />
|
||||
<span>记住我</span>
|
||||
</label>
|
||||
<div class="action-links">
|
||||
<a class="register-link" @click.prevent="goRegister">注册账号</a>
|
||||
<span class="divider">|</span>
|
||||
<a class="forget-link" @click.prevent="goForget">忘记密码?</a>
|
||||
</div>
|
||||
</div>
|
||||
<transition name="fade">
|
||||
<div v-if="errorMsg" class="error-msg">{{ errorMsg }}</div>
|
||||
</transition>
|
||||
<button class="login-btn" @click="handleLogin" :disabled="loading">
|
||||
{{ loading ? '登录中...' : '登 录' }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="errorMsg" class="error-msg">{{ errorMsg }}</div>
|
||||
<button class="login-btn" @click="handleLogin" :disabled="loading">
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
</div>
|
||||
<!-- 背景光效 -->
|
||||
<div class="login-light light1"></div>
|
||||
<div class="login-light light2"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
.login-bg {
|
||||
min-height: 100vh;
|
||||
width: 100vw;
|
||||
background: linear-gradient(120deg, #e6f0ff 0%, #f5fcff 55%, #eaf6ff 100%);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
background: #f5f6fa;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.login-form {
|
||||
background: #fff;
|
||||
padding: 40px 32px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 16px rgba(0,0,0,0.09);
|
||||
min-width: 320px;
|
||||
.login-card {
|
||||
display: flex;
|
||||
min-width: 770px;
|
||||
background: rgba(255,255,255, 0.95);
|
||||
border-radius: 22px;
|
||||
box-shadow: 0 8px 36px 0 rgba(73,150,255,0.14), 0 1.5px 4px 0 rgba(30,42,79,0.05);
|
||||
overflow: hidden;
|
||||
z-index: 10;
|
||||
}
|
||||
.login-side {
|
||||
width: 320px;
|
||||
background: #52a8ff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 44px 16px 32px 16px;
|
||||
box-shadow: 4px 0 32px 0 rgba(189,231,255,0.13) inset;
|
||||
position: relative;
|
||||
}
|
||||
.login-form h2 {
|
||||
margin-bottom: 16px;
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 42px;
|
||||
user-select: none;
|
||||
}
|
||||
.brand-title {
|
||||
font-size: 25px;
|
||||
letter-spacing: 2px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
/* text-shadow: 0 0 6px #d4ecfc; */
|
||||
}
|
||||
.illus {
|
||||
margin-top: 30px;
|
||||
user-select: none;
|
||||
opacity: .95;
|
||||
}
|
||||
/* 版权信息样式 */
|
||||
.copyright {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
margin-top: auto;
|
||||
margin-bottom: 2px;
|
||||
letter-spacing: 0.2px;
|
||||
padding-top: 25px;
|
||||
user-select: none;
|
||||
}
|
||||
.login-panel {
|
||||
flex: 1;
|
||||
padding: 52px 54px 48px 54px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
min-width: 320px;
|
||||
}
|
||||
.login-title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #2560a9;
|
||||
text-align: left;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.login-desc {
|
||||
color: #7391c4;
|
||||
font-size: 15px;
|
||||
margin-bottom: 28px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 15px;
|
||||
position: relative;
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
|
||||
/* 输入框前置图标样式 */
|
||||
.icon-input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
.input-with-icon {
|
||||
padding-left: 36px !important;
|
||||
}
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
z-index: 2;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: #4da1ff;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.visible-btn {
|
||||
position: absolute;
|
||||
right: 11px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
cursor: pointer;
|
||||
z-index: 2;
|
||||
padding: 2px 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.82;
|
||||
user-select: none;
|
||||
}
|
||||
.visible-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
/* 防止密码输入框可见按钮和输入内容重叠 */
|
||||
.icon-input-group .input-with-icon {
|
||||
padding-right: 34px;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
padding: 12px 14px;
|
||||
font-size: 16px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 5px;
|
||||
border: 1.3px solid #d6e6fa;
|
||||
border-radius: 7px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 4px;
|
||||
transition: border 0.2s, box-shadow 0.2s;
|
||||
background: #f7fbfe;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.input:focus {
|
||||
border-color: #4da1ff;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px #e3f2ffb1;
|
||||
}
|
||||
|
||||
/* 记住我单选框 */
|
||||
.remember-me-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0;
|
||||
margin-top: 3px;
|
||||
min-height: 32px;
|
||||
font-size: 15px;
|
||||
color: #6d8eb8;
|
||||
user-select: none;
|
||||
}
|
||||
.remember-me-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.remember-me-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
accent-color: #4da1ff;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 16px;
|
||||
background: #005bea;
|
||||
padding: 13px 0;
|
||||
font-size: 17px;
|
||||
background: linear-gradient(90deg, #3494e6 0%, #52a8ff 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
border-radius: 7px;
|
||||
box-shadow: 0 2px 12px 0 rgba(81,173,255,0.13);
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
margin-top: 15px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
transition: background 0.2s, transform 0.13s;
|
||||
}
|
||||
.login-btn:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.login-btn:disabled {
|
||||
background: #8bbcfa;
|
||||
background: #b6dafc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error-msg {
|
||||
color: #e74c3c;
|
||||
color: #e4574a;
|
||||
background: #fdeceb;
|
||||
text-align: center;
|
||||
margin-bottom: 6px;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
padding: 7px 4px;
|
||||
margin-bottom: 2px;
|
||||
font-size: 14.5px;
|
||||
letter-spacing: .3px;
|
||||
animation: shake .28s;
|
||||
}
|
||||
@keyframes shake {
|
||||
0% { transform: translateX(0);}
|
||||
20% { transform: translateX(-6px);}
|
||||
40% { transform: translateX(6px);}
|
||||
60% { transform: translateX(-2px);}
|
||||
80% { transform: translateX(2px);}
|
||||
100% { transform: translateX(0);}
|
||||
}
|
||||
|
||||
/* 注册、忘记密码链接 */
|
||||
.action-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
font-size: 14.1px;
|
||||
color: #6592c3;
|
||||
margin-bottom: 0;
|
||||
min-height: 22px;
|
||||
}
|
||||
.action-links a {
|
||||
cursor: pointer;
|
||||
color: #407ad6;
|
||||
text-decoration: none;
|
||||
transition: color 0.16s;
|
||||
}
|
||||
.action-links a:hover {
|
||||
color: #165eec;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.action-links .divider {
|
||||
color: #bbd3ee;
|
||||
margin: 0 6px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.register-link {
|
||||
margin-right: 0px;
|
||||
}
|
||||
.forget-link {
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
/* 渐隐提示 */
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity .24s;
|
||||
}
|
||||
.fade-enter-from, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 炫彩光斑装饰 */
|
||||
.login-light {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
filter: blur(45px);
|
||||
opacity: 0.4;
|
||||
z-index: 1;
|
||||
}
|
||||
.light1 {
|
||||
width: 340px;
|
||||
height: 340px;
|
||||
top: -90px;
|
||||
left: -60px;
|
||||
background: radial-gradient(circle at 60% 50%, #55b7f988 0%, #e1e8fa11 95%);
|
||||
}
|
||||
.light2 {
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
right: -60px;
|
||||
bottom: -90px;
|
||||
background: radial-gradient(circle at 55% 60%, #f3e7ff99 0%, #daf3ff10 100%);
|
||||
}
|
||||
@media (max-width: 940px) {
|
||||
.login-card { min-width: 330px; flex-direction: column; }
|
||||
.login-side { width: 100%; min-width: 0; border-radius: 0 0 18px 18px;}
|
||||
.login-panel { padding: 30px 22px 34px 22px; min-width: 0; }
|
||||
.copyright { padding-top: 13px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
472
pc/src/views/system/files/categories.vue
Normal file
472
pc/src/views/system/files/categories.vue
Normal file
@ -0,0 +1,472 @@
|
||||
<template>
|
||||
<div class="categories-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">分类管理</h1>
|
||||
<div class="action-bar">
|
||||
<el-button type="primary" @click="showAddDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加分类
|
||||
</el-button>
|
||||
<el-button @click="$router.push('/files')">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回文件列表
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类列表 -->
|
||||
<div class="categories-list">
|
||||
<el-table
|
||||
:data="categories"
|
||||
v-loading="loading"
|
||||
style="width: 100%"
|
||||
:default-sort="{ prop: 'created_at', order: 'descending' }"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="分类名称" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="category-name">
|
||||
<el-tag :color="getCategoryColor(row.name)" effect="dark">
|
||||
{{ row.name }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="200" />
|
||||
<el-table-column prop="file_count" label="文件数量" width="120" sortable>
|
||||
<template #default="{ row }">
|
||||
<el-badge :value="row.file_count || 0" :max="99" class="item">
|
||||
<el-button size="small" @click="viewCategoryFiles(row)">查看文件</el-button>
|
||||
</el-badge>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="180" sortable>
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="updated_at" label="更新时间" width="180" sortable>
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.updated_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="updateCategoryStatus(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="editCategory(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="deleteCategory(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑分类对话框 -->
|
||||
<el-dialog
|
||||
v-model="showAddDialog"
|
||||
:title="editingCategory ? '编辑分类' : '添加分类'"
|
||||
width="500px"
|
||||
:before-close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="categoryFormRef"
|
||||
:model="categoryForm"
|
||||
:rules="categoryRules"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="分类名称" prop="name">
|
||||
<el-input v-model="categoryForm.name" placeholder="请输入分类名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
v-model="categoryForm.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入分类描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="颜色" prop="color">
|
||||
<el-color-picker v-model="categoryForm.color" />
|
||||
<span class="tip-text">选择分类显示颜色(可选)</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-switch v-model="categoryForm.status" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showAddDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitCategory">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 分类文件列表对话框 -->
|
||||
<el-dialog
|
||||
v-model="showFilesDialog"
|
||||
:title="currentCategory ? currentCategory.name + ' - 文件列表' : '文件列表'"
|
||||
width="800px"
|
||||
>
|
||||
<div v-if="currentCategory" class="category-files">
|
||||
<el-table :data="categoryFiles" style="width: 100%" v-loading="filesLoading">
|
||||
<el-table-column prop="file_name" label="文件名" min-width="200" />
|
||||
<el-table-column prop="original_name" label="原始文件名" min-width="200" />
|
||||
<el-table-column prop="file_size" label="大小" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ formatFileSize(row.file_size) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="upload_by" label="上传人" width="120" />
|
||||
<el-table-column prop="upload_time" label="上传时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.upload_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination" style="margin-top: 20px;">
|
||||
<el-pagination
|
||||
v-model:current-page="filesCurrentPage"
|
||||
v-model:page-size="filesPageSize"
|
||||
:total="totalFiles"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleFilesSizeChange"
|
||||
@current-change="handleFilesCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showFilesDialog = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { Plus, ArrowLeft } from '@element-plus/icons-vue'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const categories = ref<any[]>([])
|
||||
const showAddDialog = ref(false)
|
||||
const editingCategory = ref<any>(null)
|
||||
const showFilesDialog = ref(false)
|
||||
const currentCategory = ref<any>(null)
|
||||
const categoryFiles = ref<any[]>([])
|
||||
const filesLoading = ref(false)
|
||||
const filesCurrentPage = ref(1)
|
||||
const filesPageSize = ref(10)
|
||||
const totalFiles = ref(0)
|
||||
|
||||
// 表单引用
|
||||
const categoryFormRef = ref<FormInstance>()
|
||||
|
||||
// 分类表单
|
||||
const categoryForm = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
color: '#409EFF',
|
||||
status: 1
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const categoryRules: FormRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '分类名称长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
description: [
|
||||
{ max: 200, message: '描述长度不能超过 200 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 预定义分类颜色映射
|
||||
const categoryColors: Record<string, string> = {
|
||||
'文档': '#67C23A',
|
||||
'图片': '#E6A23C',
|
||||
'视频': '#F56C6C',
|
||||
'音频': '#909399',
|
||||
'其他': '#409EFF'
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadCategories()
|
||||
})
|
||||
|
||||
// 方法
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
// 模拟加载分类数据
|
||||
categories.value = [
|
||||
{
|
||||
id: 1,
|
||||
name: '文档',
|
||||
description: '各种文档文件',
|
||||
file_count: 15,
|
||||
created_at: '2024-01-01 10:00:00',
|
||||
updated_at: '2024-01-10 15:30:00',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '图片',
|
||||
description: '图片文件',
|
||||
file_count: 8,
|
||||
created_at: '2024-01-02 09:00:00',
|
||||
updated_at: '2024-01-09 14:20:00',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '视频',
|
||||
description: '视频文件',
|
||||
file_count: 3,
|
||||
created_at: '2024-01-03 11:00:00',
|
||||
updated_at: '2024-01-08 16:45:00',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '音频',
|
||||
description: '音频文件',
|
||||
file_count: 5,
|
||||
created_at: '2024-01-04 08:30:00',
|
||||
updated_at: '2024-01-07 13:15:00',
|
||||
status: 1
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '其他',
|
||||
description: '其他类型文件',
|
||||
file_count: 12,
|
||||
created_at: '2024-01-05 14:00:00',
|
||||
updated_at: '2024-01-06 10:30:00',
|
||||
status: 1
|
||||
}
|
||||
]
|
||||
} catch (error) {
|
||||
console.error('加载分类失败:', error)
|
||||
ElMessage.error('加载分类失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getCategoryColor = (categoryName: string) => {
|
||||
return categoryColors[categoryName] || '#409EFF'
|
||||
}
|
||||
|
||||
const viewCategoryFiles = async (category: any) => {
|
||||
currentCategory.value = category
|
||||
showFilesDialog.value = true
|
||||
await loadCategoryFiles()
|
||||
}
|
||||
|
||||
const loadCategoryFiles = async () => {
|
||||
try {
|
||||
filesLoading.value = true
|
||||
// 模拟加载分类文件数据
|
||||
categoryFiles.value = [
|
||||
{
|
||||
id: 1,
|
||||
file_name: 'document.pdf',
|
||||
original_name: '项目文档.pdf',
|
||||
file_size: 2048576,
|
||||
upload_by: '管理员',
|
||||
upload_time: '2024-01-10 10:00:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
file_name: 'image.jpg',
|
||||
original_name: '产品图片.jpg',
|
||||
file_size: 1024576,
|
||||
upload_by: '用户A',
|
||||
upload_time: '2024-01-09 15:30:00'
|
||||
}
|
||||
]
|
||||
totalFiles.value = categoryFiles.value.length
|
||||
} catch (error) {
|
||||
console.error('加载分类文件失败:', error)
|
||||
ElMessage.error('加载分类文件失败')
|
||||
} finally {
|
||||
filesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const editCategory = (category: any) => {
|
||||
editingCategory.value = category
|
||||
Object.assign(categoryForm, category)
|
||||
showAddDialog.value = true
|
||||
}
|
||||
|
||||
const deleteCategory = async (category: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除分类 "${category.name}" 吗?此操作不可恢复。`,
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
|
||||
// 模拟删除操作
|
||||
categories.value = categories.value.filter(c => c.id !== category.id)
|
||||
ElMessage.success('删除成功')
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除分类失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateCategoryStatus = async (category: any) => {
|
||||
try {
|
||||
// 模拟更新状态操作
|
||||
ElMessage.success(`分类 "${category.name}" 状态已更新`)
|
||||
} catch (error) {
|
||||
console.error('更新状态失败:', error)
|
||||
ElMessage.error('更新状态失败')
|
||||
// 恢复原状态
|
||||
category.status = category.status === 1 ? 0 : 1
|
||||
}
|
||||
}
|
||||
|
||||
const submitCategory = async () => {
|
||||
if (!categoryFormRef.value) return
|
||||
|
||||
try {
|
||||
await categoryFormRef.value.validate()
|
||||
|
||||
if (editingCategory.value) {
|
||||
// 编辑分类
|
||||
const index = categories.value.findIndex(c => c.id === editingCategory.value.id)
|
||||
if (index !== -1) {
|
||||
Object.assign(categories.value[index], { ...categoryForm })
|
||||
}
|
||||
ElMessage.success('分类更新成功')
|
||||
} else {
|
||||
// 添加分类
|
||||
const newCategory = {
|
||||
id: Date.now(),
|
||||
...categoryForm,
|
||||
file_count: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
}
|
||||
categories.value.unshift(newCategory)
|
||||
ElMessage.success('分类添加成功')
|
||||
}
|
||||
|
||||
showAddDialog.value = false
|
||||
resetForm()
|
||||
} catch (error) {
|
||||
console.error('提交分类失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDialogClose = () => {
|
||||
showAddDialog.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
editingCategory.value = null
|
||||
categoryForm.name = ''
|
||||
categoryForm.description = ''
|
||||
categoryForm.color = '#409EFF'
|
||||
categoryForm.status = 1
|
||||
categoryFormRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const handleFilesSizeChange = (size: number) => {
|
||||
filesPageSize.value = size
|
||||
filesCurrentPage.value = 1
|
||||
loadCategoryFiles()
|
||||
}
|
||||
|
||||
const handleFilesCurrentChange = (page: number) => {
|
||||
filesCurrentPage.value = page
|
||||
loadCategoryFiles()
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.categories-container {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.categories-list {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.category-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
:deep(.el-badge__content) {
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
}
|
||||
</style>
|
||||
673
pc/src/views/system/files/index.vue
Normal file
673
pc/src/views/system/files/index.vue
Normal file
@ -0,0 +1,673 @@
|
||||
<template>
|
||||
<el-card class="box-card">
|
||||
<div class="header-bar">
|
||||
<h2>文件管理</h2>
|
||||
<!-- <el-button type="primary" @click="showProgramDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加程序
|
||||
</el-button> -->
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>正在加载程序数据...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="error-state">
|
||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="fetchPrograms">重试</el-button>
|
||||
</div>
|
||||
|
||||
<div v-else class="files-module fancy-bg">
|
||||
<!-- 顶部功能卡片区域 -->
|
||||
<div class="function-cards">
|
||||
<el-row :gutter="24">
|
||||
<el-col :xs="24" :sm="12" :lg="8">
|
||||
<el-card
|
||||
class="function-card card-gradient"
|
||||
shadow="hover"
|
||||
@click="$router.push('/files/list')"
|
||||
>
|
||||
<div class="card-content">
|
||||
<div class="card-icon">
|
||||
<i class="fa-solid fa-folder-open"></i>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<span class="card-title">文件列表</span>
|
||||
<span class="card-desc">查看和管理所有文件</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="8">
|
||||
<el-card
|
||||
class="function-card card-gradient"
|
||||
shadow="hover"
|
||||
@click="$router.push('/files/upload')"
|
||||
>
|
||||
<div class="card-content">
|
||||
<div class="card-icon upload">
|
||||
<i class="fa-solid fa-cloud-arrow-up"></i>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<span class="card-title">批量上传</span>
|
||||
<span class="card-desc">批量上传文件至系统</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="24" :sm="12" :lg="8">
|
||||
<el-card
|
||||
class="function-card card-gradient"
|
||||
shadow="hover"
|
||||
@click="$router.push('/files/categories')"
|
||||
>
|
||||
<div class="card-content">
|
||||
<div class="card-icon tags">
|
||||
<i class="fa-solid fa-layer-group"></i>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<span class="card-title">分类管理</span>
|
||||
<span class="card-desc">管理分类/标签</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="search-filter stylish-panel">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索文件名、原始文件名或分类"
|
||||
clearable
|
||||
@input="handleSearch"
|
||||
size="large"
|
||||
class="input-search"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-select
|
||||
v-model="filterCategory"
|
||||
placeholder="全部分类"
|
||||
clearable
|
||||
@change="handleFilter"
|
||||
size="large"
|
||||
class="select-category"
|
||||
>
|
||||
<el-option
|
||||
v-for="category in categories"
|
||||
:key="category"
|
||||
:label="category"
|
||||
:value="category"
|
||||
/>
|
||||
</el-select>
|
||||
|
||||
<el-switch
|
||||
v-model="showMyFiles"
|
||||
class="switch-mine"
|
||||
active-text="仅看我的文件"
|
||||
inactive-text="全部文件"
|
||||
@change="handleFilter"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 文件数据列表 -->
|
||||
<div class="file-list stylish-panel">
|
||||
<el-table
|
||||
:data="filteredFiles"
|
||||
v-loading="loading"
|
||||
highlight-current-row
|
||||
border
|
||||
:default-sort="{ prop: 'upload_time', order: 'descending' }"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="file_name" label="文件名" min-width="200">
|
||||
<template #default="{ row }">
|
||||
<div class="file-name">
|
||||
<el-icon class="icon-doc"><Document /></el-icon>
|
||||
<span>{{ row.file_name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="original_name"
|
||||
label="原始文件名"
|
||||
min-width="180"
|
||||
/>
|
||||
<el-table-column prop="category" label="分类" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag effect="dark" type="success">{{ row.category }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="file_size" label="大小" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag effect="plain">{{
|
||||
formatFileSize(row.file_size)
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="file_type" label="类型" width="90" />
|
||||
<el-table-column prop="upload_by" label="上传人" width="120" />
|
||||
<el-table-column
|
||||
prop="upload_time"
|
||||
label="上传时间"
|
||||
width="170"
|
||||
sortable
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span style="color: #8ea1d6">{{
|
||||
formatDate(row.upload_time)
|
||||
}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="170"
|
||||
fixed="right"
|
||||
align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tooltip content="查看详情">
|
||||
<el-button
|
||||
type="primary"
|
||||
circle
|
||||
plain
|
||||
size="small"
|
||||
@click="viewFile(row)"
|
||||
>
|
||||
<el-icon><View /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除">
|
||||
<el-button
|
||||
circle
|
||||
type="danger"
|
||||
plain
|
||||
size="small"
|
||||
@click="deleteFile(row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="totalFiles"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 上传文件对话框 -->
|
||||
<el-dialog
|
||||
v-model="showUploadDialog"
|
||||
title="上传文件"
|
||||
width="500px"
|
||||
:before-close="handleUploadClose"
|
||||
class="upload-dialog"
|
||||
>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
class="upload-demo"
|
||||
drag
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:data="{ category: uploadForm.category }"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
:before-upload="beforeUpload"
|
||||
:show-file-list="false"
|
||||
multiple
|
||||
>
|
||||
<el-icon
|
||||
class="el-icon--upload"
|
||||
style="font-size: 44px; color: #36b2fa"
|
||||
><upload-filled
|
||||
/></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em style="color: #409eff">点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持单个或批量上传,单文件大小不超过
|
||||
<span style="color: #f56c6c; font-weight: 600">10MB</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<div class="upload-form upform-padding">
|
||||
<el-form :model="uploadForm" label-width="80px">
|
||||
<el-form-item label="分类">
|
||||
<el-select
|
||||
v-model="uploadForm.category"
|
||||
placeholder="请选择分类"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="文档" value="文档" />
|
||||
<el-option label="图片" value="图片" />
|
||||
<el-option label="视频" value="视频" />
|
||||
<el-option label="音频" value="音频" />
|
||||
<el-option label="其他" value="其他" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否公开">
|
||||
<el-switch
|
||||
v-model="uploadForm.isPublic"
|
||||
active-color="#409EFF"
|
||||
inactive-color="#bfbfbf"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showUploadDialog = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitUpload">上传</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 文件详情对话框 -->
|
||||
<el-dialog
|
||||
v-model="showFileDetail"
|
||||
:title="currentFile ? currentFile.original_name : '文件详情'"
|
||||
width="650px"
|
||||
class="file-detail-dialog"
|
||||
>
|
||||
<div v-if="currentFile" class="file-detail">
|
||||
<el-descriptions :column="2" border size="large">
|
||||
<el-descriptions-item label="文件ID">
|
||||
<el-tag>{{ currentFile.id }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="文件名">
|
||||
<span class="detail-primary">{{ currentFile.file_name }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="原始文件名">{{
|
||||
currentFile.original_name
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="文件大小">
|
||||
<el-tag type="info">{{
|
||||
formatFileSize(currentFile.file_size)
|
||||
}}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="文件类型">{{
|
||||
currentFile.file_type
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="文件扩展名">{{
|
||||
currentFile.file_ext
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="分类">
|
||||
<el-tag type="success" effect="plain">{{
|
||||
currentFile.category
|
||||
}}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="上传人">
|
||||
<el-tag>{{ currentFile.upload_by }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="上传时间">{{
|
||||
formatDate(currentFile.upload_time)
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag
|
||||
:type="currentFile.status === 1 ? 'success' : 'danger'"
|
||||
effect="dark"
|
||||
>
|
||||
{{ currentFile.status === 1 ? "正常" : "已删除" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="是否公开">
|
||||
<el-tag
|
||||
:type="currentFile.is_public === 1 ? 'success' : 'info'"
|
||||
effect="plain"
|
||||
>
|
||||
{{ currentFile.is_public === 1 ? "公开" : "私有" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="detail-btns">
|
||||
<el-button type="primary" @click="downloadFile(currentFile)">
|
||||
<el-icon><Download /></el-icon> 下载文件
|
||||
</el-button>
|
||||
<el-button @click="copyFileUrl(currentFile)">
|
||||
<el-icon><Link /></el-icon> 复制链接
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showFileDetail = false">关闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage } from "element-plus";
|
||||
import {
|
||||
Download,
|
||||
Delete,
|
||||
Search,
|
||||
Document,
|
||||
View,
|
||||
Link,
|
||||
UploadFilled,
|
||||
} from "@element-plus/icons-vue";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const recentFiles = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: "项目报告.pdf",
|
||||
size: 2456789,
|
||||
type: "PDF",
|
||||
uploadTime: "2024-01-15 10:30:25",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "数据统计.xlsx",
|
||||
size: 123456,
|
||||
type: "Excel",
|
||||
uploadTime: "2024-01-14 14:20:10",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "产品图片.jpg",
|
||||
size: 345678,
|
||||
type: "Image",
|
||||
uploadTime: "2024-01-13 09:15:45",
|
||||
},
|
||||
]);
|
||||
|
||||
const getFileIcon = (type: string) => {
|
||||
const iconMap: Record<string, string> = {
|
||||
PDF: "fa-solid fa-file-pdf",
|
||||
Excel: "fa-solid fa-file-excel",
|
||||
Word: "fa-solid fa-file-word",
|
||||
Image: "fa-solid fa-file-image",
|
||||
Video: "fa-solid fa-file-video",
|
||||
Audio: "fa-solid fa-file-audio",
|
||||
Archive: "fa-solid fa-file-archive",
|
||||
};
|
||||
return iconMap[type] || "fa-solid fa-file";
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return "0 B";
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
// mock 下列数据和函数,由于原代码没提供全部上下文
|
||||
const categories = ref(["文档", "图片", "视频", "音频", "其他"]);
|
||||
const filteredFiles = ref([]);
|
||||
const loading = ref(false);
|
||||
const searchKeyword = ref("");
|
||||
const filterCategory = ref("");
|
||||
const showMyFiles = ref(false);
|
||||
const pageSize = ref(10);
|
||||
const currentPage = ref(1);
|
||||
const totalFiles = ref(0);
|
||||
const showUploadDialog = ref(false);
|
||||
|
||||
// 上传配置
|
||||
const uploadUrl = computed(() => {
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL ;
|
||||
return `${baseUrl}/api/files`;
|
||||
});
|
||||
|
||||
const uploadHeaders = computed(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
return {
|
||||
'Authorization': `Bearer ${token}`
|
||||
};
|
||||
});
|
||||
|
||||
const uploadForm = ref({
|
||||
category: "",
|
||||
isPublic: false,
|
||||
});
|
||||
const showFileDetail = ref(false);
|
||||
const currentFile = ref<any>(null);
|
||||
|
||||
const handleSearch = () => {};
|
||||
const handleFilter = () => {};
|
||||
const handleSizeChange = () => {};
|
||||
const handleCurrentChange = () => {};
|
||||
const handleUploadClose = () => {
|
||||
showUploadDialog.value = false;
|
||||
uploadForm.value.category = "";
|
||||
uploadForm.value.isPublic = false;
|
||||
};
|
||||
|
||||
const handleUploadSuccess = (response: any, file: any) => {
|
||||
ElMessage.success('文件上传成功!');
|
||||
// 关闭对话框
|
||||
showUploadDialog.value = false;
|
||||
// 可以刷新文件列表
|
||||
// loadFiles();
|
||||
};
|
||||
|
||||
const handleUploadError = (error: Error, file: any) => {
|
||||
ElMessage.error('文件上传失败:' + error.message);
|
||||
};
|
||||
|
||||
const beforeUpload = (file: File) => {
|
||||
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||
if (file.size > maxSize) {
|
||||
ElMessage.error('文件大小不能超过 10MB!');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const submitUpload = () => {
|
||||
// Element Plus 的 el-upload 组件会自动处理上传
|
||||
ElMessage.info('开始上传文件...');
|
||||
};
|
||||
const viewFile = (row: any) => {};
|
||||
const formatDate = (val: string | number) => val;
|
||||
const copyFileUrl = (file: any) => {};
|
||||
|
||||
const downloadFile = (file: any) => {
|
||||
// 这里实现下载逻辑
|
||||
console.log("下载文件:", file.name || file.file_name);
|
||||
};
|
||||
|
||||
const deleteFile = (file: any) => {
|
||||
// 这里实现删除逻辑
|
||||
console.log("删除文件:", file.name || file.file_name);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
console.log("文件管理模块已加载");
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fancy-bg {
|
||||
padding: 28px 14px 24px 14px;
|
||||
}
|
||||
.stylish-panel {
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 4px 14px 0 #e2eafe77;
|
||||
margin-bottom: 28px;
|
||||
padding: 28px 18px 24px 18px;
|
||||
}
|
||||
.function-cards {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.function-card {
|
||||
border: none !important;
|
||||
border-radius: 14px !important;
|
||||
box-shadow: 0 8px 24px 0 #bedcff11 !important;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.15s cubic-bezier(0.17, 0.67, 0.83, 0.67),
|
||||
box-shadow 0.18s;
|
||||
}
|
||||
.function-card:hover {
|
||||
transform: translateY(-4px) scale(1.032);
|
||||
box-shadow: 0 18px 48px 0 #6bbaf522 !important;
|
||||
}
|
||||
.card-gradient {
|
||||
background: linear-gradient(120deg, #ecf4ff 94%, #eff8ff 100%) !important;
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 26px 16px;
|
||||
min-height: 95px;
|
||||
}
|
||||
.card-icon {
|
||||
font-size: 34px;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #3178ea;
|
||||
background: linear-gradient(133deg, #d9eaff 82%, #f2f6fd 100%);
|
||||
border-radius: 12px;
|
||||
margin-right: 18px;
|
||||
transition: color 0.25s;
|
||||
}
|
||||
.card-icon.upload {
|
||||
color: #13c2c2;
|
||||
background: linear-gradient(145deg, #c5faea 58%, #d6f3ff 98%);
|
||||
}
|
||||
.card-icon.tags {
|
||||
color: #a048ff;
|
||||
background: linear-gradient(143deg, #e8dafe 88%, #f3e2fd 100%);
|
||||
}
|
||||
.card-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
color: #2c3547;
|
||||
letter-spacing: 1.5px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.card-desc {
|
||||
font-size: 13px;
|
||||
color: #6c7ea5;
|
||||
font-weight: 400;
|
||||
}
|
||||
.rc-title-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.recent-icon {
|
||||
font-size: 20px;
|
||||
color: #409eff;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.recent-title {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
color: #293a46;
|
||||
}
|
||||
.recent-table {
|
||||
border-radius: 6px !important;
|
||||
}
|
||||
.input-search {
|
||||
width: 300px !important;
|
||||
margin-right: 18px !important;
|
||||
box-shadow: 0 2px 6px 0 #dae7fd24;
|
||||
}
|
||||
.select-category {
|
||||
width: 170px !important;
|
||||
margin-right: 18px;
|
||||
}
|
||||
.switch-mine {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.file-list {
|
||||
margin-bottom: 28px;
|
||||
background: #fff;
|
||||
}
|
||||
.file-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.icon-doc {
|
||||
margin-right: 8px;
|
||||
color: #2090ff;
|
||||
}
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 12px;
|
||||
background: none;
|
||||
}
|
||||
.upload-dialog .el-dialog__body {
|
||||
padding-top: 20px;
|
||||
}
|
||||
.upform-padding {
|
||||
margin-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 20px;
|
||||
}
|
||||
.file-detail-dialog .el-dialog__body {
|
||||
padding-top: 18px;
|
||||
background: #fafcff;
|
||||
}
|
||||
.file-detail {
|
||||
max-height: 380px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.detail-primary {
|
||||
color: #416cf7;
|
||||
font-weight: 600;
|
||||
}
|
||||
.detail-btns {
|
||||
margin-top: 28px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 22px;
|
||||
}
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.stylish-panel,
|
||||
.file-list,
|
||||
.function-cards {
|
||||
padding: 10px !important;
|
||||
}
|
||||
.fancy-bg {
|
||||
padding: 10px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
369
pc/src/views/system/files/upload.vue
Normal file
369
pc/src/views/system/files/upload.vue
Normal file
@ -0,0 +1,369 @@
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">批量上传</h1>
|
||||
<div class="action-bar">
|
||||
<el-button @click="$router.push('/files')">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
返回文件列表
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 上传区域 -->
|
||||
<div class="upload-area">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
class="upload-demo"
|
||||
drag
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:on-success="handleUploadSuccess"
|
||||
:on-error="handleUploadError"
|
||||
:before-upload="beforeUpload"
|
||||
:on-change="handleFileChange"
|
||||
:on-remove="handleFileRemove"
|
||||
multiple
|
||||
:file-list="fileList"
|
||||
:auto-upload="false"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持批量上传,单个文件大小不超过 10MB,支持拖拽排序
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
<!-- 上传设置 -->
|
||||
<div class="upload-settings">
|
||||
<el-card header="上传设置">
|
||||
<el-form :model="uploadForm" label-width="100px">
|
||||
<el-form-item label="文件分类">
|
||||
<el-select v-model="uploadForm.category" placeholder="请选择分类">
|
||||
<el-option label="文档" value="文档" />
|
||||
<el-option label="图片" value="图片" />
|
||||
<el-option label="视频" value="视频" />
|
||||
<el-option label="音频" value="音频" />
|
||||
<el-option label="其他" value="其他" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否公开">
|
||||
<el-switch v-model="uploadForm.isPublic" />
|
||||
<span class="tip-text">公开文件所有用户可见,私有文件仅自己可见</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="覆盖同名">
|
||||
<el-switch v-model="uploadForm.overwrite" />
|
||||
<span class="tip-text">遇到同名文件时是否覆盖</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 文件列表预览 -->
|
||||
<div class="file-preview" v-if="fileList.length > 0">
|
||||
<el-card header="待上传文件列表">
|
||||
<el-table :data="fileList" style="width: 100%">
|
||||
<el-table-column prop="name" label="文件名" min-width="200" />
|
||||
<el-table-column label="大小" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatFileSize(row.size) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)" size="small">
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="{ row, $index }">
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="removeFile($index)"
|
||||
:disabled="row.status === 'uploading'"
|
||||
>
|
||||
移除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="upload-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="submitUpload"
|
||||
:loading="uploading"
|
||||
:disabled="fileList.length === 0"
|
||||
>
|
||||
{{ uploading ? '上传中...' : '开始上传' }}
|
||||
</el-button>
|
||||
<el-button @click="clearFiles" :disabled="fileList.length === 0">
|
||||
清空列表
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 上传进度 -->
|
||||
<div class="upload-progress" v-if="uploading">
|
||||
<el-card header="上传进度">
|
||||
<el-progress
|
||||
:percentage="uploadProgress"
|
||||
:status="uploadStatus"
|
||||
:stroke-width="8"
|
||||
/>
|
||||
<div class="progress-info">
|
||||
<span>已上传: {{ uploadedCount }}/{{ fileList.length }}</span>
|
||||
<span>成功率: {{ successRate }}%</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox, type UploadInstance, type UploadFile } from 'element-plus'
|
||||
import { ArrowLeft, UploadFilled } from '@element-plus/icons-vue'
|
||||
import { fileAPI } from '@/services/api'
|
||||
|
||||
const router = useRouter()
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
|
||||
// 响应式数据
|
||||
const fileList = ref<UploadFile[]>([])
|
||||
const uploading = ref(false)
|
||||
const uploadProgress = ref(0)
|
||||
const uploadedCount = ref(0)
|
||||
const successCount = ref(0)
|
||||
|
||||
// 上传表单
|
||||
const uploadForm = ref({
|
||||
category: '文档',
|
||||
isPublic: false,
|
||||
overwrite: false
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const uploadUrl = computed(() => {
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL
|
||||
return `${baseUrl}/api/files`
|
||||
})
|
||||
|
||||
const uploadHeaders = computed(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
return {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
const uploadStatus = computed(() => {
|
||||
if (uploadProgress.value === 100) return 'success'
|
||||
if (uploading.value) return undefined
|
||||
return 'exception'
|
||||
})
|
||||
|
||||
const successRate = computed(() => {
|
||||
if (uploadedCount.value === 0) return 0
|
||||
return Math.round((successCount.value / uploadedCount.value) * 100)
|
||||
})
|
||||
|
||||
// 方法
|
||||
const beforeUpload = (file: File) => {
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
if (!isLt10M) {
|
||||
ElMessage.error(`文件 "${file.name}" 大小不能超过 10MB!`)
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查是否已存在同名文件
|
||||
const exists = fileList.value.some(f => f.name === file.name)
|
||||
if (exists && !uploadForm.value.overwrite) {
|
||||
ElMessage.warning(`文件 "${file.name}" 已存在,请启用覆盖选项或重命名文件`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const handleFileChange = (file: UploadFile, fileList: UploadFile[]) => {
|
||||
fileList.value = fileList
|
||||
}
|
||||
|
||||
const handleFileRemove = (file: UploadFile, fileList: UploadFile[]) => {
|
||||
fileList.value = fileList
|
||||
}
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
fileList.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const clearFiles = () => {
|
||||
fileList.value = []
|
||||
uploadProgress.value = 0
|
||||
uploadedCount.value = 0
|
||||
successCount.value = 0
|
||||
}
|
||||
|
||||
const submitUpload = async () => {
|
||||
if (fileList.value.length === 0) {
|
||||
ElMessage.warning('请先选择要上传的文件')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
uploading.value = true
|
||||
uploadProgress.value = 0
|
||||
uploadedCount.value = 0
|
||||
successCount.value = 0
|
||||
|
||||
// 模拟批量上传进度
|
||||
const totalFiles = fileList.value.length
|
||||
let currentProgress = 0
|
||||
|
||||
for (let i = 0; i < fileList.value.length; i++) {
|
||||
const file = fileList.value[i]
|
||||
|
||||
try {
|
||||
// 这里应该调用实际的批量上传API
|
||||
// 暂时使用模拟上传
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
successCount.value++
|
||||
ElMessage.success(`文件 "${file.name}" 上传成功`)
|
||||
} catch (error) {
|
||||
console.error(`文件 "${file.name}" 上传失败:`, error)
|
||||
ElMessage.error(`文件 "${file.name}" 上传失败`)
|
||||
}
|
||||
|
||||
uploadedCount.value++
|
||||
currentProgress = Math.round((uploadedCount.value / totalFiles) * 100)
|
||||
uploadProgress.value = currentProgress
|
||||
}
|
||||
|
||||
if (successCount.value === totalFiles) {
|
||||
ElMessage.success('所有文件上传完成')
|
||||
} else {
|
||||
ElMessage.warning(`上传完成,成功 ${successCount.value}/${totalFiles} 个文件`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('批量上传失败:', error)
|
||||
ElMessage.error('批量上传失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleUploadSuccess = (response: any, file: UploadFile) => {
|
||||
if (response.success) {
|
||||
file.status = 'success'
|
||||
} else {
|
||||
file.status = 'fail'
|
||||
ElMessage.error(response.message || '上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleUploadError = (error: any, file: UploadFile) => {
|
||||
console.error('上传失败:', error)
|
||||
file.status = 'fail'
|
||||
ElMessage.error('上传失败')
|
||||
}
|
||||
|
||||
const getStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success': return 'success'
|
||||
case 'uploading': return 'warning'
|
||||
case 'fail': return 'danger'
|
||||
default: return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'success': return '成功'
|
||||
case 'uploading': return '上传中'
|
||||
case 'fail': return '失败'
|
||||
default: return '等待上传'
|
||||
}
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.upload-container {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.upload-area {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.upload-settings {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.upload-actions {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.progress-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
margin-left: 10px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
}
|
||||
</style>
|
||||
548
pc/src/views/system/menus/manager.vue
Normal file
548
pc/src/views/system/menus/manager.vue
Normal file
@ -0,0 +1,548 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>菜单管理</h2>
|
||||
<el-button type="primary" @click="handleAddMenu = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加菜单
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 树形表格 -->
|
||||
<el-table
|
||||
:data="menuTree"
|
||||
style="width: 100%"
|
||||
row-key="Id"
|
||||
border
|
||||
:tree-props="{
|
||||
children: 'children',
|
||||
hasChildren: 'hasChildren',
|
||||
}"
|
||||
:expand-row-keys="defaultExpandedKeys"
|
||||
>
|
||||
<el-table-column prop="Name" label="菜单名称" width="200">
|
||||
<template #default="scope">
|
||||
<div class="menu-item">
|
||||
<i
|
||||
v-if="scope.row.Icon"
|
||||
:class="scope.row.Icon"
|
||||
class="menu-icon"
|
||||
></i>
|
||||
<span>{{ scope.row.Name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="Path" label="路由地址"></el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="MenuType"
|
||||
label="菜单类型"
|
||||
width="120"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-tag :type="getMenuTypeTagType(scope.row.MenuType)">
|
||||
{{ getMenuTypeName(scope.row.MenuType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column
|
||||
prop="Order"
|
||||
label="排序"
|
||||
width="80"
|
||||
align="center"
|
||||
></el-table-column>
|
||||
|
||||
<el-table-column prop="Status" label="状态" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.Status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="handleStatusChange(scope.row)"
|
||||
:disabled="!scope.row.Id"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="280" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
@click="handleAddSubMenu(scope.row)"
|
||||
:disabled="scope.row.MenuType === 3"
|
||||
>
|
||||
<el-icon><CirclePlus /></el-icon>
|
||||
<span>子菜单</span>
|
||||
</el-button>
|
||||
|
||||
<el-button size="small" text @click="handleEditMenu(scope.row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
<span>编辑</span>
|
||||
</el-button>
|
||||
|
||||
<el-button
|
||||
size="small"
|
||||
text
|
||||
type="danger"
|
||||
@click="handleDeleteMenu(scope.row)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
<span>删除</span>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 添加/编辑菜单对话框 -->
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px">
|
||||
<el-form
|
||||
:model="currentMenu"
|
||||
label-width="100px"
|
||||
:rules="formRules"
|
||||
ref="menuFormRef"
|
||||
>
|
||||
<el-form-item label="父级菜单" prop="ParentId">
|
||||
<el-cascader
|
||||
v-model="currentMenu.ParentId"
|
||||
:options="parentMenuOptions"
|
||||
:props="cascaderProps"
|
||||
placeholder="请选择父级菜单"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单名称" prop="Name">
|
||||
<el-input v-model="currentMenu.Name" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="菜单类型" prop="MenuType">
|
||||
<el-radio-group v-model="currentMenu.MenuType" style="width: 100%">
|
||||
<el-radio-button :label="1">普通菜单</el-radio-button>
|
||||
<el-radio-button :label="2">分组菜单</el-radio-button>
|
||||
<el-radio-button :label="3">按钮菜单</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
label="路由地址"
|
||||
prop="Path"
|
||||
v-if="currentMenu.MenuType !== 3"
|
||||
>
|
||||
<el-input v-model="currentMenu.Path" placeholder="例如:/system" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
label="组件路径"
|
||||
prop="ComponentPath"
|
||||
v-if="currentMenu.MenuType === 1"
|
||||
>
|
||||
<el-input
|
||||
v-model="currentMenu.ComponentPath"
|
||||
placeholder="例如:@/views/settings/index.vue"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="图标" prop="Icon">
|
||||
<el-input
|
||||
v-model="currentMenu.Icon"
|
||||
placeholder="例如:fas fa-tachometer-alt"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序" prop="Order">
|
||||
<el-input-number
|
||||
v-model="currentMenu.Order"
|
||||
:min="0"
|
||||
placeholder="数字越小越靠前"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="权限标识" prop="Permission">
|
||||
<el-input
|
||||
v-model="currentMenu.Permission"
|
||||
placeholder="请输入权限标识"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="saveMenu">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox, ElForm } from "element-plus";
|
||||
import { Plus, CirclePlus, Edit, Delete } from "@element-plus/icons-vue";
|
||||
import { getAllMenus, updateMenuStatus, createMenu, updateMenu, deleteMenu } from "@/api/menu";
|
||||
|
||||
// 定义菜单数据类型(匹配后端返回的 Pascal 命名格式)
|
||||
interface Menu {
|
||||
Id: number;
|
||||
Name: string;
|
||||
Path: string;
|
||||
ParentId: number;
|
||||
Icon: string;
|
||||
Order: number;
|
||||
Status: 0 | 1;
|
||||
ComponentPath: string;
|
||||
IsExternal: 0 | 1;
|
||||
ExternalUrl: string;
|
||||
MenuType: 1 | 2 | 3; // 1:普通菜单 2:分组菜单 3:按钮菜单
|
||||
Permission: string;
|
||||
CreateTime: string;
|
||||
UpdateTime: string;
|
||||
children?: Menu[];
|
||||
hasChildren?: boolean;
|
||||
}
|
||||
|
||||
// 菜单树形数据
|
||||
const menuTree = ref<Menu[]>([]);
|
||||
|
||||
// 默认展开的菜单ID
|
||||
const defaultExpandedKeys = ref<number[]>([]);
|
||||
|
||||
// 对话框状态
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref("");
|
||||
const menuFormRef = ref<InstanceType<typeof ElForm>>();
|
||||
|
||||
// 当前操作的菜单
|
||||
const currentMenu = ref<Partial<Menu>>({
|
||||
Id: 0,
|
||||
Name: "",
|
||||
Path: "",
|
||||
ParentId: 0,
|
||||
Icon: "",
|
||||
Order: 0,
|
||||
Status: 1,
|
||||
ComponentPath: "",
|
||||
IsExternal: 0,
|
||||
ExternalUrl: "",
|
||||
MenuType: 1,
|
||||
Permission: "",
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = ref({
|
||||
Name: [{ required: true, message: "请输入菜单名称", trigger: "blur" }],
|
||||
MenuType: [{ required: true, message: "请选择菜单类型", trigger: "change" }],
|
||||
Order: [{ required: true, message: "请输入排序号", trigger: "blur" }],
|
||||
});
|
||||
|
||||
// 级联选择器配置(匹配 Pascal 命名)
|
||||
const cascaderProps = ref({
|
||||
value: "Id",
|
||||
label: "Name",
|
||||
children: "children",
|
||||
checkStrictly: true,
|
||||
emitPath: false,
|
||||
});
|
||||
|
||||
// 父级菜单选项
|
||||
const parentMenuOptions = ref<Menu[]>([]);
|
||||
|
||||
// 获取所有菜单并构建树形结构
|
||||
const fetchMenus = async () => {
|
||||
try {
|
||||
// 使用 getAllMenus 接口获取菜单(接口返回后端格式,需映射为 Pascal 命名并补充默认字段)
|
||||
const result = await getAllMenus();
|
||||
if (result.success) {
|
||||
// getAllMenus返回的data里每一项key是小写下划线式(见后端),需要转为Pascal命名
|
||||
const data = result.data.map((item: any) => ({
|
||||
Id: item.id,
|
||||
Name: item.name,
|
||||
Path: item.path,
|
||||
ParentId: item.parentId,
|
||||
Icon: item.icon,
|
||||
Order: item.order,
|
||||
Status: 1, // 默认开启(后端返回的只查Status=1的)
|
||||
ComponentPath: item.componentPath || "",
|
||||
IsExternal: item.isExternal || 0,
|
||||
ExternalUrl: item.externalUrl || "",
|
||||
MenuType: 1, // 如果后端未返回MenuType,默认填1
|
||||
Permission: "", // 后端未返回时默认空
|
||||
CreateTime: "",
|
||||
UpdateTime: "",
|
||||
}));
|
||||
|
||||
const tree = buildMenuTree(data);
|
||||
menuTree.value = tree;
|
||||
|
||||
// 设置默认展开顶级菜单(ParentId=0)
|
||||
defaultExpandedKeys.value = tree
|
||||
.filter((menu) => menu.ParentId === 0)
|
||||
.map((menu) => menu.Id);
|
||||
|
||||
// 构建父级菜单选项(添加顶级菜单选项)
|
||||
parentMenuOptions.value = [
|
||||
{
|
||||
Id: 0,
|
||||
Name: "顶级菜单",
|
||||
MenuType: 1,
|
||||
children: [],
|
||||
} as Menu,
|
||||
...tree,
|
||||
];
|
||||
} else {
|
||||
ElMessage.error("获取菜单失败: " + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("获取菜单数据失败: " + (error as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
// 构建菜单树(处理父子关系)
|
||||
const buildMenuTree = (menuList: Menu[]): Menu[] => {
|
||||
const menuMap = new Map<number, Menu>();
|
||||
|
||||
// 初始化所有菜单,添加children和hasChildren字段
|
||||
menuList.forEach((menu) => {
|
||||
menuMap.set(menu.Id, { ...menu, children: [], hasChildren: false });
|
||||
});
|
||||
|
||||
// 构建父子关系
|
||||
const tree: Menu[] = [];
|
||||
menuList.forEach((menu) => {
|
||||
const current = menuMap.get(menu.Id)!;
|
||||
|
||||
if (menu.ParentId === 0) {
|
||||
// 顶级菜单直接加入根节点
|
||||
tree.push(current);
|
||||
} else {
|
||||
// 子菜单挂载到父菜单
|
||||
const parent = menuMap.get(menu.ParentId);
|
||||
if (parent) {
|
||||
parent.children?.push(current);
|
||||
parent.hasChildren = true; // 标记有子菜单
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return tree;
|
||||
};
|
||||
|
||||
// 获取菜单类型名称
|
||||
const getMenuTypeName = (type: number) => {
|
||||
const typeMap = { 1: "普通菜单", 2: "分组菜单", 3: "按钮菜单" };
|
||||
return typeMap[type as keyof typeof typeMap] || "未知类型";
|
||||
};
|
||||
|
||||
// 获取菜单类型标签样式
|
||||
const getMenuTypeTagType = (type: number) => {
|
||||
const typeMap = { 1: "primary", 2: "success", 3: "info" };
|
||||
return typeMap[type as keyof typeof typeMap] || "default";
|
||||
};
|
||||
|
||||
// 处理状态变更
|
||||
const handleStatusChange = async (menu: Menu) => {
|
||||
try {
|
||||
const result = await updateMenuStatus(menu.Id, menu.Status);
|
||||
if (!result.success) {
|
||||
ElMessage.error(result.message);
|
||||
// 状态更新失败时回滚
|
||||
menu.Status = menu.Status === 1 ? 0 : 1;
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("更新状态失败: " + (error as Error).message);
|
||||
menu.Status = menu.Status === 1 ? 0 : 1;
|
||||
}
|
||||
};
|
||||
|
||||
// 添加顶级菜单
|
||||
const handleAddMenu = () => {
|
||||
dialogTitle.value = "添加菜单";
|
||||
currentMenu.value = {
|
||||
Id: 0,
|
||||
Name: "",
|
||||
Path: "",
|
||||
ParentId: 0,
|
||||
Icon: "",
|
||||
Order: 0,
|
||||
Status: 1,
|
||||
ComponentPath: "",
|
||||
IsExternal: 0,
|
||||
ExternalUrl: "",
|
||||
MenuType: 1,
|
||||
Permission: "",
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 添加子菜单
|
||||
const handleAddSubMenu = (parentMenu: Menu) => {
|
||||
dialogTitle.value = `添加子菜单 (父菜单: ${parentMenu.Name})`;
|
||||
currentMenu.value = {
|
||||
Id: 0,
|
||||
Name: "",
|
||||
Path: "",
|
||||
ParentId: parentMenu.Id,
|
||||
Icon: "",
|
||||
Order: 0,
|
||||
Status: 1,
|
||||
ComponentPath: "",
|
||||
IsExternal: 0,
|
||||
ExternalUrl: "",
|
||||
MenuType: parentMenu.MenuType === 2 ? 1 : parentMenu.MenuType,
|
||||
Permission: "",
|
||||
};
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑菜单
|
||||
const handleEditMenu = (menu: Menu) => {
|
||||
dialogTitle.value = "编辑菜单";
|
||||
currentMenu.value = { ...menu };
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除菜单
|
||||
const handleDeleteMenu = (menu: Menu) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除菜单 "${menu.Name}" 吗?${menu.hasChildren ? "其下所有子菜单也将被删除。" : ""}`,
|
||||
"确认删除",
|
||||
{
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
const result = await deleteMenu(menu.Id);
|
||||
if (result.success) {
|
||||
ElMessage.success("删除成功");
|
||||
fetchMenus();
|
||||
} else {
|
||||
ElMessage.error("删除失败: " + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("删除失败: " + (error as Error).message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 保存菜单
|
||||
const saveMenu = async () => {
|
||||
// 表单验证
|
||||
if (!menuFormRef.value) return;
|
||||
const valid = await menuFormRef.value.validate();
|
||||
if (!valid) return;
|
||||
|
||||
try {
|
||||
if (currentMenu.value.Id === 0) {
|
||||
// 新增菜单
|
||||
const result = await createMenu(currentMenu.value as Menu);
|
||||
if (result.success) {
|
||||
ElMessage.success("菜单添加成功");
|
||||
dialogVisible.value = false;
|
||||
fetchMenus();
|
||||
} else {
|
||||
ElMessage.error("添加失败: " + result.message);
|
||||
}
|
||||
} else {
|
||||
// 编辑菜单
|
||||
const result = await updateMenu(
|
||||
currentMenu.value.Id!,
|
||||
currentMenu.value as Menu
|
||||
);
|
||||
if (result.success) {
|
||||
ElMessage.success("更新成功");
|
||||
dialogVisible.value = false;
|
||||
fetchMenus();
|
||||
} else {
|
||||
ElMessage.error("更新失败: " + result.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("操作失败: " + (error as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时加载菜单
|
||||
onMounted(() => {
|
||||
fetchMenus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #f2f3f5;
|
||||
}
|
||||
|
||||
.card-header span {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 表格核心样式 */
|
||||
:deep(.el-table) {
|
||||
border-radius: 0;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
:deep(.el-table__body td) {
|
||||
padding: 12px 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 展开图标与菜单内容对齐 */
|
||||
:deep(.el-table__expand-icon) {
|
||||
margin: 0 !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:deep(.el-table__expand-icon-cell) {
|
||||
padding: 0 8px !important;
|
||||
}
|
||||
|
||||
/* 菜单项样式 */
|
||||
.menu-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
font-size: 16px;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 隐藏无子女菜单的展开图标 */
|
||||
:deep(.el-table__expand-icon--hidden) {
|
||||
visibility: hidden;
|
||||
width: 24px;
|
||||
}
|
||||
:deep(.el-table__expand-icon) {
|
||||
margin-right: 8px !important;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 对话框样式精简 */
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 20px;
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
112
pc/src/views/system/permissions/index.vue
Normal file
112
pc/src/views/system/permissions/index.vue
Normal file
@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>权限管理</h2>
|
||||
<el-button type="primary" @click="handleAddPermission = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加权限
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<el-table :data="permissions" style="width: 100%">
|
||||
<el-table-column prop="name" label="权限名称" width="180" align="center" />
|
||||
<el-table-column prop="code" label="权限代码" width="200" align="center" />
|
||||
<el-table-column prop="description" label="权限描述" align="center" />
|
||||
<el-table-column prop="type" label="权限类型" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.type === 'menu' ? 'primary' : 'warning'">
|
||||
{{ scope.row.type === "menu" ? "菜单权限" : "功能权限" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleEdit(scope.row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button 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 } from "vue";
|
||||
|
||||
// const loading = ref(false);
|
||||
// const error = ref("");
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
interface Permission {
|
||||
id: number;
|
||||
name: string;
|
||||
code: string;
|
||||
description: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const permissions = ref<Permission[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: "用户管理",
|
||||
code: "user:manage",
|
||||
description: "用户的增删改查权限",
|
||||
type: "menu",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "角色管理",
|
||||
code: "role:manage",
|
||||
description: "角色的增删改查权限",
|
||||
type: "menu",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "数据导出",
|
||||
code: "data:export",
|
||||
description: "数据导出功能权限",
|
||||
type: "function",
|
||||
},
|
||||
]);
|
||||
|
||||
const handleAddPermission = () => {
|
||||
console.log("添加权限");
|
||||
};
|
||||
|
||||
const handleEdit = (permission: Permission) => {
|
||||
console.log("编辑权限:", permission);
|
||||
};
|
||||
|
||||
const handleDelete = (permission: Permission) => {
|
||||
console.log("删除权限:", permission);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.permissions-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
245
pc/src/views/system/programs/index.vue
Normal file
245
pc/src/views/system/programs/index.vue
Normal file
@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>程序管理</h2>
|
||||
<el-button type="primary" @click="showProgramDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加程序
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>正在加载程序数据...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="error-state">
|
||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="fetchPrograms">重试</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 程序列表 -->
|
||||
<div v-else>
|
||||
<el-table :data="programs" stripe style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="name" label="程序名称" min-width="160" align="center" />
|
||||
<el-table-column prop="type" label="类型" width="100" align="center" />
|
||||
<el-table-column prop="owner" label="负责人" width="120" align="center" />
|
||||
<el-table-column prop="createdAt" label="创建时间" width="170" align="center" />
|
||||
<el-table-column prop="remark" label="备注" min-width="140" align="center" />
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(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>
|
||||
|
||||
<!-- 新增/编辑程序弹窗 -->
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑程序' : '添加程序'"
|
||||
v-model="showProgramDialog"
|
||||
width="420px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
:model="programForm"
|
||||
:rules="formRules"
|
||||
ref="programFormRef"
|
||||
label-width="90px"
|
||||
>
|
||||
<el-form-item label="程序名称" prop="name">
|
||||
<el-input v-model="programForm.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-select v-model="programForm.type" placeholder="选择类型">
|
||||
<el-option label="Web" value="Web" />
|
||||
<el-option label="服务" value="Service" />
|
||||
<el-option label="工具" value="Tool" />
|
||||
<el-option label="其他" value="Other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="owner">
|
||||
<el-input v-model="programForm.owner" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="programForm.remark" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showProgramDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitProgramForm"> 保存 </el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
type FormInstance,
|
||||
type FormRules,
|
||||
} from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
|
||||
// 数据与状态
|
||||
const programs = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
const showProgramDialog = ref(false);
|
||||
const isEditing = ref(false);
|
||||
const programForm = reactive({
|
||||
id: null,
|
||||
name: "",
|
||||
type: "",
|
||||
owner: "",
|
||||
remark: "",
|
||||
});
|
||||
const programFormRef = ref<FormInstance>();
|
||||
|
||||
const formRules: FormRules = {
|
||||
name: [
|
||||
{ required: true, message: "请输入程序名称", trigger: "blur" },
|
||||
{ min: 2, max: 32, message: "名称长度2-32字符", trigger: "blur" },
|
||||
],
|
||||
type: [{ required: true, message: "请选择类型", trigger: "change" }],
|
||||
owner: [{ required: true, message: "请输入负责人", trigger: "blur" }],
|
||||
};
|
||||
|
||||
async function fetchPrograms() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
// TODO: 替换为真实API
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
// 假数据
|
||||
const all = [
|
||||
{
|
||||
id: 1,
|
||||
name: "门户网站",
|
||||
type: "Web",
|
||||
owner: "张三",
|
||||
createdAt: "2023-11-08 09:22:53",
|
||||
remark: "官网",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "自动备份服务",
|
||||
type: "Service",
|
||||
owner: "李四",
|
||||
createdAt: "2024-01-16 15:40:01",
|
||||
remark: "每日凌晨自动执行",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "运维工具",
|
||||
type: "Tool",
|
||||
owner: "王五",
|
||||
createdAt: "2023-12-02 12:51:29",
|
||||
remark: "",
|
||||
},
|
||||
// ...更多
|
||||
];
|
||||
total.value = all.length;
|
||||
programs.value = all.slice(
|
||||
(page.value - 1) * pageSize.value,
|
||||
page.value * pageSize.value
|
||||
);
|
||||
} catch (err: any) {
|
||||
error.value = err.message || "获取程序列表失败";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const handlePageChange = (val: number) => {
|
||||
page.value = val;
|
||||
fetchPrograms();
|
||||
};
|
||||
|
||||
function resetProgramForm() {
|
||||
programForm.id = null;
|
||||
programForm.name = "";
|
||||
programForm.type = "";
|
||||
programForm.owner = "";
|
||||
programForm.remark = "";
|
||||
}
|
||||
|
||||
function handleEdit(row: any) {
|
||||
isEditing.value = true;
|
||||
programForm.id = row.id;
|
||||
programForm.name = row.name;
|
||||
programForm.type = row.type;
|
||||
programForm.owner = row.owner;
|
||||
programForm.remark = row.remark;
|
||||
showProgramDialog.value = true;
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除程序「${row.name}」? 删除后不可恢复。`,
|
||||
"警告",
|
||||
{ type: "warning" }
|
||||
);
|
||||
// TODO: 替换为真实API
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
ElMessage.success("删除成功");
|
||||
fetchPrograms();
|
||||
} catch {
|
||||
// 取消删除
|
||||
}
|
||||
}
|
||||
|
||||
async function submitProgramForm() {
|
||||
await programFormRef.value?.validate();
|
||||
loading.value = true;
|
||||
try {
|
||||
if (isEditing.value) {
|
||||
// TODO: 替换为真实API更新
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
ElMessage.success("程序更新成功");
|
||||
} else {
|
||||
// TODO: 替换为真实API新增
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
ElMessage.success("程序添加成功");
|
||||
}
|
||||
showProgramDialog.value = false;
|
||||
fetchPrograms();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || "操作失败,请重试");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchPrograms();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
290
pc/src/views/system/roles/index.vue
Normal file
290
pc/src/views/system/roles/index.vue
Normal file
@ -0,0 +1,290 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>角色管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="showRoleDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加角色
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>正在加载角色数据...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="error-state">
|
||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="fetchRoles">重试</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 角色列表 -->
|
||||
<div v-else>
|
||||
<el-table :data="roles" stripe style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="label" label="角色名称" align="center" />
|
||||
<el-table-column prop="value" label="角色标识" align="center" />
|
||||
<el-table-column prop="remark" label="备注" align="center" />
|
||||
<el-table-column label="操作" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(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>
|
||||
|
||||
<!-- 新增/编辑角色弹窗 -->
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑角色' : '添加角色'"
|
||||
v-model="showRoleDialog"
|
||||
width="420px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
:model="roleForm"
|
||||
:rules="formRules"
|
||||
ref="roleFormRef"
|
||||
label-width="90px"
|
||||
>
|
||||
<el-form-item label="角色名称" prop="label">
|
||||
<el-input
|
||||
v-model="roleForm.label"
|
||||
:disabled="isEditing && roleForm.value === 'admin'"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色标识" prop="value">
|
||||
<el-input
|
||||
v-model="roleForm.value"
|
||||
:disabled="isEditing && roleForm.value === 'admin'"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="roleForm.remark" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showRoleDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitRoleForm"> 保存 </el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import {
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
type FormInstance,
|
||||
type FormRules,
|
||||
} from "element-plus";
|
||||
|
||||
const roles = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
const showRoleDialog = ref(false);
|
||||
const isEditing = ref(false);
|
||||
const roleForm = reactive({
|
||||
id: null,
|
||||
label: "",
|
||||
value: "",
|
||||
remark: "",
|
||||
});
|
||||
const roleFormRef = ref<FormInstance>();
|
||||
|
||||
const formRules: FormRules = {
|
||||
label: [
|
||||
{ required: true, message: "请输入角色名称", trigger: "blur" },
|
||||
{ min: 2, max: 24, message: "名称长度2-24字符", trigger: "blur" },
|
||||
],
|
||||
value: [
|
||||
{ required: true, message: "请输入角色标识", trigger: "blur" },
|
||||
{
|
||||
pattern: /^[a-zA-Z0-9_]+$/,
|
||||
message: "只能包含字母、数字或下划线",
|
||||
trigger: "blur",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async function fetchRoles() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
// TODO: 替换为真实API
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
// 假数据
|
||||
const all = [
|
||||
{ id: 1, label: "系统管理员", value: "admin", remark: "拥有全部权限" },
|
||||
{ id: 2, label: "普通用户", value: "user", remark: "普通访问权限" },
|
||||
{ id: 3, label: "运营", value: "ops", remark: "运营相关权限" },
|
||||
// ...更多
|
||||
];
|
||||
total.value = all.length;
|
||||
roles.value = all.slice(
|
||||
(page.value - 1) * pageSize.value,
|
||||
page.value * pageSize.value
|
||||
);
|
||||
} catch (err: any) {
|
||||
error.value = err.message || "获取角色列表失败";
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const handlePageChange = (val: number) => {
|
||||
page.value = val;
|
||||
fetchRoles();
|
||||
};
|
||||
|
||||
function resetRoleForm() {
|
||||
roleForm.id = null;
|
||||
roleForm.label = "";
|
||||
roleForm.value = "";
|
||||
roleForm.remark = "";
|
||||
}
|
||||
|
||||
function handleEdit(row: any) {
|
||||
isEditing.value = true;
|
||||
roleForm.id = row.id;
|
||||
roleForm.label = row.label;
|
||||
roleForm.value = row.value;
|
||||
roleForm.remark = row.remark;
|
||||
showRoleDialog.value = true;
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除角色「${row.label}」? 删除后不可恢复。`,
|
||||
"警告",
|
||||
{ type: "warning" }
|
||||
);
|
||||
// TODO: 替换为真实API
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
ElMessage.success("删除成功");
|
||||
fetchRoles();
|
||||
} catch {
|
||||
// 取消删除
|
||||
}
|
||||
}
|
||||
|
||||
async function submitRoleForm() {
|
||||
// 表单校验
|
||||
await roleFormRef.value?.validate();
|
||||
loading.value = true;
|
||||
try {
|
||||
if (isEditing.value) {
|
||||
// TODO: 替换为真实API更新
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
ElMessage.success("角色更新成功");
|
||||
} else {
|
||||
// TODO: 替换为真实API新增
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
ElMessage.success("角色添加成功");
|
||||
}
|
||||
showRoleDialog.value = false;
|
||||
fetchRoles();
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || "操作失败,请重试");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRoles();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-management-module {
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
padding: 28px 22px 14px 18px;
|
||||
min-height: 500px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
font-size: 1.18rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
color: #24292f;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 220px;
|
||||
padding: 32px 0 16px 0;
|
||||
background: #f5f7fa;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 4px solid #e5e5e5;
|
||||
border-top: 4px solid #409eff;
|
||||
border-radius: 50%;
|
||||
animation: loading-spin 0.8s linear infinite;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
@keyframes loading-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
padding-top: 18px !important;
|
||||
}
|
||||
|
||||
.el-form .el-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.el-form .el-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.role-management-module {
|
||||
padding: 10px 4px;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
row-gap: 8px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
0
pc/src/views/system/tenant/components/audit.vue
Normal file
0
pc/src/views/system/tenant/components/audit.vue
Normal file
0
pc/src/views/system/tenant/components/detail.vue
Normal file
0
pc/src/views/system/tenant/components/detail.vue
Normal file
0
pc/src/views/system/tenant/components/edit.vue
Normal file
0
pc/src/views/system/tenant/components/edit.vue
Normal file
646
pc/src/views/system/tenant/index.vue
Normal file
646
pc/src/views/system/tenant/index.vue
Normal file
@ -0,0 +1,646 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>租户管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button @click="fetchTenants" title="刷新租户列表" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showAddTenantDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加租户
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>正在加载租户数据...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="error" class="error-state">
|
||||
<el-alert title="加载失败" :message="error" type="error" show-icon />
|
||||
<el-button type="primary" @click="fetchTenants">重试</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 租户列表 -->
|
||||
<div v-else>
|
||||
<el-table
|
||||
:data="tenants"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="name" label="租户名称" min-width="120" />
|
||||
<el-table-column prop="code" label="租户编码" width="120" />
|
||||
<el-table-column prop="owner" label="负责人" width="100" />
|
||||
<el-table-column prop="created_at" label="创建时间" width="150" />
|
||||
<el-table-column label="审核状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getAuditStatusType(row.audit_status)">
|
||||
{{ getAuditStatusText(row.audit_status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleView(row)">
|
||||
<el-icon><View /></el-icon>
|
||||
查看
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="success"
|
||||
v-if="row.audit_status === 'pending'"
|
||||
@click="handleAuditDialog(row)"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
审核
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
@click="handleEdit(row)"
|
||||
v-if="row.audit_status !== 'approved'"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
v-if="row.audit_status !== 'approved'"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</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>
|
||||
|
||||
<!-- 租户详情查看弹窗 -->
|
||||
<el-dialog
|
||||
title="租户详情"
|
||||
v-model="showViewDialog"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="tenant-detail">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="租户ID">
|
||||
{{ currentTenant.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="租户名称">
|
||||
{{ currentTenant.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="租户编码">
|
||||
{{ currentTenant.code }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">
|
||||
{{ currentTenant.owner }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">
|
||||
{{ currentTenant.phone || '未设置' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">
|
||||
{{ currentTenant.email || '未设置' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="审核状态">
|
||||
<el-tag :type="getAuditStatusType(currentTenant.audit_status)">
|
||||
{{ getAuditStatusText(currentTenant.audit_status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="currentTenant.status === 'enabled' ? 'success' : 'info'">
|
||||
{{ currentTenant.status === 'enabled' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ currentTenant.created_at }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ currentTenant.updated_at || '未更新' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ currentTenant.remark || '无' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showViewDialog = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 审核弹窗 -->
|
||||
<el-dialog
|
||||
title="租户审核"
|
||||
v-model="showAuditDialog"
|
||||
width="700px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div class="audit-content">
|
||||
<!-- 租户信息展示 -->
|
||||
<div class="tenant-info-section">
|
||||
<h3>租户信息</h3>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="租户ID">
|
||||
{{ currentTenant.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="租户名称">
|
||||
{{ currentTenant.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="租户编码">
|
||||
{{ currentTenant.code }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">
|
||||
{{ currentTenant.owner }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">
|
||||
{{ currentTenant.phone || '未设置' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">
|
||||
{{ currentTenant.email || '未设置' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="currentTenant.status === 'enabled' ? 'success' : 'info'">
|
||||
{{ currentTenant.status === 'enabled' ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ currentTenant.created_at }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ currentTenant.remark || '无' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 审核表单 -->
|
||||
<div class="audit-form-section">
|
||||
<h3>审核信息</h3>
|
||||
<el-form :model="auditForm" :rules="auditRules" ref="auditFormRef" label-width="100px">
|
||||
<el-form-item label="审核结果" prop="result">
|
||||
<el-radio-group v-model="auditForm.result">
|
||||
<el-radio value="approved">通过</el-radio>
|
||||
<el-radio value="rejected">拒绝</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="审核意见" prop="comment">
|
||||
<el-input
|
||||
v-model="auditForm.comment"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入审核意见"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showAuditDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitAudit">提交审核</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增/编辑租户弹窗 -->
|
||||
<el-dialog
|
||||
:title="isEditing ? '编辑租户' : '添加租户'"
|
||||
v-model="showAddTenantDialog"
|
||||
width="450px"
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form :model="tenantForm" :rules="formRules" ref="tenantFormRef" label-width="90px">
|
||||
<el-form-item label="租户名称" prop="name">
|
||||
<el-input v-model="tenantForm.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="租户编码" prop="code">
|
||||
<el-input v-model="tenantForm.code" />
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="owner">
|
||||
<el-input v-model="tenantForm.owner" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话" prop="phone">
|
||||
<el-input v-model="tenantForm.phone" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="tenantForm.email" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="tenantForm.status">
|
||||
<el-option label="启用" value="enabled" />
|
||||
<el-option label="禁用" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="tenantForm.remark" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showAddTenantDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="submitTenantForm">
|
||||
保存
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
|
||||
import { Plus, Check, View, Edit, Delete } from "@element-plus/icons-vue";
|
||||
import {
|
||||
getAllTenants,
|
||||
createTenant,
|
||||
updateTenant,
|
||||
deleteTenant,
|
||||
auditTenant,
|
||||
getTenantDetail,
|
||||
} from "@/api/tenant";
|
||||
|
||||
const tenants = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
// 弹窗状态
|
||||
const showAddTenantDialog = ref(false);
|
||||
const showViewDialog = ref(false);
|
||||
const showAuditDialog = ref(false);
|
||||
const isEditing = ref(false);
|
||||
|
||||
// 当前查看的租户
|
||||
const currentTenant = ref<any>({});
|
||||
|
||||
// 审核表单
|
||||
const auditForm = reactive({
|
||||
result: '',
|
||||
comment: ''
|
||||
});
|
||||
const auditFormRef = ref<FormInstance>();
|
||||
|
||||
// 表单数据
|
||||
const tenantForm = reactive({
|
||||
id: null,
|
||||
name: "",
|
||||
code: "",
|
||||
owner: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
status: "enabled",
|
||||
remark: ""
|
||||
});
|
||||
const tenantFormRef = ref<FormInstance>();
|
||||
|
||||
const formRules: FormRules = {
|
||||
name: [{ required: true, message: "请输入租户名称", trigger: "blur" }],
|
||||
code: [{ required: true, message: "请输入租户编码", trigger: "blur" }],
|
||||
owner: [{ required: true, message: "请输入负责人", trigger: "blur" }],
|
||||
phone: [{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号", trigger: "blur" }],
|
||||
email: [{ type: "email", message: "请输入正确的邮箱", trigger: "blur" }],
|
||||
status: [{ required: true, message: "请选择状态", trigger: "change" }]
|
||||
};
|
||||
|
||||
const auditRules: FormRules = {
|
||||
result: [{ required: true, message: "请选择审核结果", trigger: "change" }],
|
||||
comment: [{ required: true, message: "请输入审核意见", trigger: "blur" }]
|
||||
};
|
||||
|
||||
async function fetchTenants() {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
try {
|
||||
const res = await getAllTenants();
|
||||
// 后端返回格式: { success: true, message: "...", data: [...] }
|
||||
if (res.success && res.data) {
|
||||
const allTenants = Array.isArray(res.data) ? res.data : [];
|
||||
// 处理字段名映射:后端返回 create_time,前端显示 created_at
|
||||
tenants.value = allTenants.map((tenant: any) => ({
|
||||
...tenant,
|
||||
created_at: tenant.create_time || tenant.created_at,
|
||||
updated_at: tenant.update_time || tenant.updated_at,
|
||||
}));
|
||||
total.value = tenants.value.length;
|
||||
} else {
|
||||
error.value = res.message || "获取租户列表失败";
|
||||
tenants.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} catch (err: any) {
|
||||
error.value = err.message || "获取租户列表失败";
|
||||
tenants.value = [];
|
||||
total.value = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const handlePageChange = (val: number) => {
|
||||
page.value = val;
|
||||
fetchTenants();
|
||||
};
|
||||
|
||||
// 审核状态相关方法
|
||||
function getAuditStatusType(status: string): 'warning' | 'success' | 'danger' | 'info' {
|
||||
const statusMap: Record<string, 'warning' | 'success' | 'danger' | 'info'> = {
|
||||
pending: 'warning',
|
||||
approved: 'success',
|
||||
rejected: 'danger'
|
||||
};
|
||||
return statusMap[status] || 'info';
|
||||
}
|
||||
|
||||
function getAuditStatusText(status: string) {
|
||||
const statusMap: Record<string, string> = {
|
||||
pending: '待审核',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝'
|
||||
};
|
||||
return statusMap[status] || '未知';
|
||||
}
|
||||
|
||||
// 查看租户详情(只读)
|
||||
async function handleView(row: any) {
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await getTenantDetail(row.id);
|
||||
if (res.success && res.data) {
|
||||
const tenant = res.data;
|
||||
currentTenant.value = {
|
||||
...tenant,
|
||||
created_at: tenant.create_time || tenant.created_at,
|
||||
updated_at: tenant.update_time || tenant.updated_at,
|
||||
};
|
||||
showViewDialog.value = true;
|
||||
} else {
|
||||
ElMessage.error(res.message || "获取租户详情失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || "获取租户详情失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 打开审核弹窗
|
||||
function handleAuditDialog(row: any) {
|
||||
currentTenant.value = { ...row };
|
||||
// 重置审核表单
|
||||
auditForm.result = '';
|
||||
auditForm.comment = '';
|
||||
showAuditDialog.value = true;
|
||||
}
|
||||
|
||||
// 提交审核
|
||||
async function submitAudit() {
|
||||
await auditFormRef.value?.validate();
|
||||
loading.value = true;
|
||||
try {
|
||||
const auditData = {
|
||||
audit_status: auditForm.result,
|
||||
audit_comment: auditForm.comment,
|
||||
audit_by: getCurrentUser(),
|
||||
};
|
||||
|
||||
const res = await auditTenant(currentTenant.value.id, auditData);
|
||||
if (res.success) {
|
||||
const action = auditForm.result === 'approved' ? '通过' : '拒绝';
|
||||
ElMessage.success(`审核${action}成功`);
|
||||
showAuditDialog.value = false;
|
||||
// 重新获取租户列表
|
||||
await fetchTenants();
|
||||
} else {
|
||||
ElMessage.error(res.message || "审核失败,请重试");
|
||||
}
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.message || "审核失败,请重试");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前用户
|
||||
function getCurrentUser() {
|
||||
const userStr = localStorage.getItem("user");
|
||||
if (userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr);
|
||||
return user.username || user.name || user.userName || "system";
|
||||
} catch {
|
||||
return "system";
|
||||
}
|
||||
}
|
||||
return "system";
|
||||
}
|
||||
|
||||
function resetTenantForm() {
|
||||
tenantForm.id = null;
|
||||
tenantForm.name = "";
|
||||
tenantForm.code = "";
|
||||
tenantForm.owner = "";
|
||||
tenantForm.phone = "";
|
||||
tenantForm.email = "";
|
||||
tenantForm.status = "enabled";
|
||||
tenantForm.remark = "";
|
||||
}
|
||||
|
||||
function handleEdit(row: any) {
|
||||
isEditing.value = true;
|
||||
tenantForm.id = row.id;
|
||||
tenantForm.name = row.name;
|
||||
tenantForm.code = row.code;
|
||||
tenantForm.owner = row.owner;
|
||||
tenantForm.phone = row.phone || "";
|
||||
tenantForm.email = row.email || "";
|
||||
tenantForm.status = row.status;
|
||||
tenantForm.remark = row.remark || "";
|
||||
showAddTenantDialog.value = true;
|
||||
}
|
||||
|
||||
async function handleDelete(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要删除租户「${row.name}」? 删除后不可恢复。`,
|
||||
"警告",
|
||||
{ type: "warning" }
|
||||
);
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await deleteTenant(row.id);
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
await fetchTenants();
|
||||
} else {
|
||||
ElMessage.error(res.message || "删除失败");
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || "删除失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} catch {
|
||||
// 取消删除
|
||||
}
|
||||
}
|
||||
|
||||
function submitTenantForm() {
|
||||
(tenantFormRef.value as FormInstance).validate(async (valid) => {
|
||||
if (!valid) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const tenantData = {
|
||||
name: tenantForm.name,
|
||||
code: tenantForm.code,
|
||||
owner: tenantForm.owner,
|
||||
phone: tenantForm.phone || "",
|
||||
email: tenantForm.email || "",
|
||||
status: tenantForm.status,
|
||||
remark: tenantForm.remark || "",
|
||||
};
|
||||
|
||||
let res;
|
||||
if (isEditing.value && tenantForm.id) {
|
||||
// 编辑租户
|
||||
res = await updateTenant(tenantForm.id, tenantData);
|
||||
} else {
|
||||
// 新增租户
|
||||
res = await createTenant(tenantData);
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
if (isEditing.value) {
|
||||
ElMessage.success("租户信息已更新");
|
||||
} else {
|
||||
ElMessage.success("租户添加成功");
|
||||
}
|
||||
showAddTenantDialog.value = false;
|
||||
resetTenantForm();
|
||||
isEditing.value = false;
|
||||
await fetchTenants();
|
||||
} else {
|
||||
ElMessage.error(res.message || "操作失败,请重试");
|
||||
}
|
||||
} catch (err: any) {
|
||||
ElMessage.error(err.message || "操作失败,请重试");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchTenants();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tenant-management-module {
|
||||
padding: 20px;
|
||||
min-height: 600px;
|
||||
background: var(--card-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: var(--transition-base);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.loading-state, .error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40px 0;
|
||||
}
|
||||
.loading-spinner {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 4px solid var(--border-color);
|
||||
border-left-color: var(--primary-color);
|
||||
border-radius: var(--border-radius-full);
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@keyframes spin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.tenant-detail {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.tenant-detail .el-descriptions {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.tenant-detail .el-descriptions-item__label {
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.tenant-detail .el-descriptions-item__content {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.audit-content {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.tenant-info-section,
|
||||
.audit-form-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.tenant-info-section h3,
|
||||
.audit-form-section h3 {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.audit-form-section .el-form {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.audit-form-section .el-radio-group {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.audit-form-section .el-radio {
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
||||
144
pc/src/views/system/users/index.vue
Normal file
144
pc/src/views/system/users/index.vue
Normal file
@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>用户管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAddUser = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加用户
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<el-table :data="users" style="width: 100%">
|
||||
<el-table-column
|
||||
prop="username"
|
||||
label="用户名"
|
||||
width="150"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="email"
|
||||
label="邮箱"
|
||||
align="center"
|
||||
min-width="200"
|
||||
/>
|
||||
<el-table-column prop="role" label="角色" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.role === 'admin' ? 'danger' : 'primary'">
|
||||
{{ scope.row.role === "admin" ? "管理员" : "普通用户" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="scope.row.status === 'active' ? 'success' : 'danger'"
|
||||
>
|
||||
{{ scope.row.status === "active" ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="lastLogin"
|
||||
label="最后登录"
|
||||
width="180"
|
||||
align="center"
|
||||
/>
|
||||
<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="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 } from "vue";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
role: string;
|
||||
status: string;
|
||||
lastLogin: string;
|
||||
}
|
||||
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
const users = ref<User[]>([
|
||||
{
|
||||
id: 1,
|
||||
username: "admin",
|
||||
email: "admin@example.com",
|
||||
role: "admin",
|
||||
status: "active",
|
||||
lastLogin: "2025-01-20 10:30:00",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: "user1",
|
||||
email: "user1@example.com",
|
||||
role: "user",
|
||||
status: "active",
|
||||
lastLogin: "2025-01-19 15:45:00",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: "user2",
|
||||
email: "user2@example.com",
|
||||
role: "user",
|
||||
status: "inactive",
|
||||
lastLogin: "2025-01-18 09:20:00",
|
||||
},
|
||||
]);
|
||||
|
||||
const handleAddUser = () => {
|
||||
console.log("添加用户");
|
||||
};
|
||||
|
||||
const handleEdit = (user: User) => {
|
||||
console.log("编辑用户:", user);
|
||||
};
|
||||
|
||||
const handleDelete = (user: User) => {
|
||||
console.log("删除用户:", user);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -16,19 +16,21 @@ type AuthController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
// Login 处理登录请求
|
||||
// Login 处理登录请求(支持租户模式,使用租户名称)
|
||||
func (c *AuthController) Login() {
|
||||
var username, password string
|
||||
var username, password, tenantName string
|
||||
|
||||
// 优先尝试从URL参数获取(Apifox测试方式)
|
||||
username = c.GetString("username")
|
||||
password = c.GetString("password")
|
||||
tenantName = c.GetString("tenant_name")
|
||||
|
||||
// 如果URL参数为空,尝试从JSON请求体获取(前端方式)
|
||||
if username == "" || password == "" {
|
||||
if username == "" || password == "" || tenantName == "" {
|
||||
var loginData struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
TenantName string `json:"tenant_name"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal(c.Ctx.Input.RequestBody, &loginData)
|
||||
@ -44,31 +46,43 @@ func (c *AuthController) Login() {
|
||||
|
||||
username = loginData.Username
|
||||
password = loginData.Password
|
||||
tenantName = loginData.TenantName
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if tenantName == "" {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "租户名称不能为空",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 添加日志调试
|
||||
fmt.Println("接收到的登录请求:")
|
||||
fmt.Println("用户名:", username)
|
||||
fmt.Println("密码:", password)
|
||||
fmt.Println("租户名称:", tenantName)
|
||||
|
||||
// 验证用户
|
||||
fmt.Println("开始验证用户:", username)
|
||||
user, err := models.ValidateUser(username, password)
|
||||
// 验证用户(先验证租户,再验证租户下的用户)
|
||||
fmt.Println("开始验证用户:", username, "租户:", tenantName)
|
||||
user, err := models.ValidateUser(username, password, tenantName)
|
||||
fmt.Println("验证结果:", err)
|
||||
if user != nil {
|
||||
fmt.Println("用户信息:ID=", user.Id, "Username=", user.Username, "Salt=", user.Salt)
|
||||
fmt.Println("用户信息:ID=", user.Id, "Username=", user.Username, "TenantId=", user.TenantId)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// 登录失败
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "用户名或密码错误",
|
||||
"message": err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
} else {
|
||||
// 使用models包中的GenerateToken函数生成token
|
||||
tokenString, err := models.GenerateToken(user.Id, user.Username)
|
||||
// 使用models包中的GenerateToken函数生成token(包含租户ID)
|
||||
tokenString, err := models.GenerateToken(user.Id, user.Username, user.TenantId)
|
||||
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
@ -85,11 +99,12 @@ func (c *AuthController) Login() {
|
||||
"accessToken": tokenString,
|
||||
"token": tokenString, // 兼容性
|
||||
"user": map[string]interface{}{
|
||||
"id": user.Id,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"nickname": user.Nickname,
|
||||
"id": user.Id,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"nickname": user.Nickname,
|
||||
"tenant_id": user.TenantId,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -99,14 +114,35 @@ func (c *AuthController) Login() {
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// ResetPassword 重置用户密码
|
||||
// ResetPassword 重置用户密码(支持租户模式)
|
||||
func (c *AuthController) ResetPassword() {
|
||||
// 获取请求参数
|
||||
username := c.GetString("username")
|
||||
superPassword := c.GetString("superPassword")
|
||||
tenantId, _ := c.GetInt("tenant_id", 0)
|
||||
|
||||
// 如果URL参数中没有租户ID,尝试从JSON请求体获取
|
||||
if tenantId == 0 {
|
||||
var resetData struct {
|
||||
Username string `json:"username"`
|
||||
SuperPassword string `json:"superPassword"`
|
||||
TenantId int `json:"tenant_id"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &resetData); err == nil {
|
||||
username = resetData.Username
|
||||
superPassword = resetData.SuperPassword
|
||||
tenantId = resetData.TenantId
|
||||
}
|
||||
}
|
||||
|
||||
if tenantId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{"success": false, "message": "租户ID不能为空"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 调用模型方法
|
||||
err := models.ResetPassword(username, superPassword)
|
||||
err := models.ResetPassword(username, superPassword, tenantId)
|
||||
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"success": false, "message": err.Error()}
|
||||
@ -117,15 +153,38 @@ func (c *AuthController) ResetPassword() {
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// ChangePassword 修改用户密码
|
||||
// ChangePassword 修改用户密码(支持租户模式)
|
||||
func (c *AuthController) ChangePassword() {
|
||||
// 获取请求参数
|
||||
username := c.GetString("username")
|
||||
oldPassword := c.GetString("oldPassword")
|
||||
newPassword := c.GetString("newPassword")
|
||||
tenantId, _ := c.GetInt("tenant_id", 0)
|
||||
|
||||
// 如果URL参数中没有租户ID,尝试从JSON请求体获取
|
||||
if tenantId == 0 {
|
||||
var changeData struct {
|
||||
Username string `json:"username"`
|
||||
OldPassword string `json:"oldPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
TenantId int `json:"tenant_id"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &changeData); err == nil {
|
||||
username = changeData.Username
|
||||
oldPassword = changeData.OldPassword
|
||||
newPassword = changeData.NewPassword
|
||||
tenantId = changeData.TenantId
|
||||
}
|
||||
}
|
||||
|
||||
if tenantId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{"success": false, "message": "租户ID不能为空"}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 调用模型方法
|
||||
err := models.ChangePassword(username, oldPassword, newPassword)
|
||||
err := models.ChangePassword(username, oldPassword, newPassword, tenantId)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{"success": false, "message": err.Error()}
|
||||
} else {
|
||||
@ -144,9 +203,11 @@ func (c *AuthController) Logout() {
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// FindAllUsers 获取所有用户
|
||||
// FindAllUsers 获取所有用户(支持按租户过滤)
|
||||
func (c *AuthController) FindAllUsers() {
|
||||
users := models.FindAllUsers()
|
||||
// 从查询参数获取租户ID(可选)
|
||||
tenantId, _ := c.GetInt("tenant_id", 0)
|
||||
users := models.FindAllUsers(tenantId)
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "获取用户列表成功",
|
||||
@ -155,10 +216,12 @@ func (c *AuthController) FindAllUsers() {
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetUserByUsername 通过用户名查询用户信息
|
||||
// GetUserByUsername 通过用户名查询用户信息(支持租户模式)
|
||||
func (c *AuthController) GetUserByUsername() {
|
||||
// 获取请求参数中的用户名
|
||||
// 获取请求参数中的用户名和租户ID
|
||||
username := c.GetString("username")
|
||||
tenantId, _ := c.GetInt("tenant_id", 0)
|
||||
|
||||
if username == "" {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
@ -169,8 +232,18 @@ func (c *AuthController) GetUserByUsername() {
|
||||
return
|
||||
}
|
||||
|
||||
if tenantId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "租户ID不能为空",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 调用模型层方法查询用户
|
||||
user, err := models.GetUserByUsername(username) // 假设models层有这个方法
|
||||
user, err := models.GetUserByUsername(username, tenantId)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
@ -188,19 +261,19 @@ func (c *AuthController) GetUserByUsername() {
|
||||
"code": 0,
|
||||
"message": "查询成功",
|
||||
"data": map[string]interface{}{
|
||||
"id": user.Id,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"nickname": user.Nickname,
|
||||
// 其他需要返回的用户字段
|
||||
"id": user.Id,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"avatar": user.Avatar,
|
||||
"nickname": user.Nickname,
|
||||
"tenant_id": user.TenantId,
|
||||
},
|
||||
}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AddUser 添加新用户
|
||||
// AddUser 添加新用户(支持租户模式)
|
||||
func (c *AuthController) AddUser() {
|
||||
// 定义接收用户数据的结构体(与JSON请求体对应)
|
||||
var userData struct {
|
||||
@ -209,6 +282,7 @@ func (c *AuthController) AddUser() {
|
||||
Email string `json:"email"`
|
||||
Nickname string `json:"nickname"`
|
||||
Avatar string `json:"avatar"`
|
||||
TenantId int `json:"tenant_id"`
|
||||
}
|
||||
|
||||
// 解析请求体JSON数据
|
||||
@ -242,6 +316,15 @@ func (c *AuthController) AddUser() {
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if userData.TenantId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "租户ID不能为空",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 调用模型层方法添加用户(传递参数,接收新用户对象)
|
||||
newUser, err := models.AddUser(
|
||||
@ -250,6 +333,7 @@ func (c *AuthController) AddUser() {
|
||||
userData.Email,
|
||||
userData.Nickname,
|
||||
userData.Avatar,
|
||||
userData.TenantId,
|
||||
)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
@ -262,26 +346,28 @@ func (c *AuthController) AddUser() {
|
||||
"code": 0,
|
||||
"message": "用户添加成功",
|
||||
"data": map[string]interface{}{
|
||||
"id": newUser.Id,
|
||||
"username": newUser.Username,
|
||||
"email": newUser.Email,
|
||||
"nickname": newUser.Nickname,
|
||||
"avatar": newUser.Avatar,
|
||||
"id": newUser.Id,
|
||||
"username": newUser.Username,
|
||||
"email": newUser.Email,
|
||||
"nickname": newUser.Nickname,
|
||||
"avatar": newUser.Avatar,
|
||||
"tenant_id": newUser.TenantId,
|
||||
},
|
||||
}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateUser 更新用户信息
|
||||
// UpdateUser 更新用户信息(支持租户模式)
|
||||
func (c *AuthController) UpdateUser() {
|
||||
// 定义接收更新数据的结构体
|
||||
var updateData struct {
|
||||
Id int `json:"id"` // 必须包含用户ID,用于定位要更新的用户
|
||||
Username string `json:"username"` // 可选更新字段
|
||||
Email string `json:"email"` // 可选更新字段
|
||||
Nickname string `json:"nickname"` // 可选更新字段
|
||||
Avatar string `json:"avatar"` // 可选更新字段
|
||||
Id int `json:"id"` // 必须包含用户ID,用于定位要更新的用户
|
||||
Username string `json:"username"` // 可选更新字段
|
||||
Email string `json:"email"` // 可选更新字段
|
||||
Nickname string `json:"nickname"` // 可选更新字段
|
||||
Avatar string `json:"avatar"` // 可选更新字段
|
||||
TenantId int `json:"tenant_id"` // 必须包含租户ID,用于验证用户归属
|
||||
}
|
||||
|
||||
// 解析请求体JSON
|
||||
@ -296,7 +382,7 @@ func (c *AuthController) UpdateUser() {
|
||||
return
|
||||
}
|
||||
|
||||
// 校验必要参数(用户ID不能为空)
|
||||
// 校验必要参数(用户ID和租户ID不能为空)
|
||||
if updateData.Id == 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
@ -306,6 +392,15 @@ func (c *AuthController) UpdateUser() {
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if updateData.TenantId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "租户ID不能为空",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 调用模型层方法更新用户
|
||||
updatedUser, err := models.UpdateUser(
|
||||
@ -314,6 +409,7 @@ func (c *AuthController) UpdateUser() {
|
||||
updateData.Email,
|
||||
updateData.Nickname,
|
||||
updateData.Avatar,
|
||||
updateData.TenantId,
|
||||
)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
@ -326,39 +422,44 @@ func (c *AuthController) UpdateUser() {
|
||||
"code": 0,
|
||||
"message": "用户更新成功",
|
||||
"data": map[string]interface{}{
|
||||
"id": updatedUser.Id,
|
||||
"username": updatedUser.Username,
|
||||
"email": updatedUser.Email,
|
||||
"nickname": updatedUser.Nickname,
|
||||
"avatar": updatedUser.Avatar,
|
||||
"id": updatedUser.Id,
|
||||
"username": updatedUser.Username,
|
||||
"email": updatedUser.Email,
|
||||
"nickname": updatedUser.Nickname,
|
||||
"avatar": updatedUser.Avatar,
|
||||
"tenant_id": updatedUser.TenantId,
|
||||
},
|
||||
}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
// DeleteUser 删除用户(支持租户模式)
|
||||
func (c *AuthController) DeleteUser() {
|
||||
// 获取要删除的用户ID(从URL参数或请求体中获取)
|
||||
// 获取要删除的用户ID和租户ID(从URL参数或请求体中获取)
|
||||
userId, err := c.GetInt("id") // 从URL参数获取,如 /user?id=1
|
||||
if err != nil {
|
||||
tenantId, _ := c.GetInt("tenant_id", 0)
|
||||
|
||||
if err != nil || tenantId == 0 {
|
||||
// 若URL参数获取失败,尝试从JSON请求体获取
|
||||
var deleteData struct {
|
||||
Id int `json:"id"`
|
||||
Id int `json:"id"`
|
||||
TenantId int `json:"tenant_id"`
|
||||
}
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &deleteData); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "用户ID获取失败: " + err.Error(),
|
||||
"message": "用户ID或租户ID获取失败: " + err.Error(),
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
userId = deleteData.Id
|
||||
tenantId = deleteData.TenantId
|
||||
}
|
||||
|
||||
// 校验用户ID
|
||||
// 校验用户ID和租户ID
|
||||
if userId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
@ -368,9 +469,18 @@ func (c *AuthController) DeleteUser() {
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
if tenantId <= 0 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
"message": "租户ID不能为空",
|
||||
"data": nil,
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 调用模型层方法删除用户
|
||||
err = models.DeleteUser(userId)
|
||||
err = models.DeleteUser(userId, tenantId)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"code": 1,
|
||||
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
@ -111,6 +112,77 @@ func (c *MenuController) UpdateMenu() {
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateMenuStatus 更新菜单状态
|
||||
func (c *MenuController) UpdateMenuStatus() {
|
||||
id, err := c.GetInt(":id")
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "参数错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 解析请求体获取 status 字段
|
||||
var req struct {
|
||||
Status int8 `json:"status"`
|
||||
}
|
||||
|
||||
// 尝试从请求体获取数据
|
||||
requestBody := c.Ctx.Input.RequestBody
|
||||
if len(requestBody) > 0 {
|
||||
// 有请求体,从 JSON 解析
|
||||
if err := json.Unmarshal(requestBody, &req); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "请求参数错误",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 如果请求体为空,尝试从表单参数或查询参数获取
|
||||
statusValue, err := c.GetInt("status", -1)
|
||||
if err != nil || statusValue == -1 {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "请求参数错误:缺少 status 参数",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
req.Status = int8(statusValue)
|
||||
}
|
||||
|
||||
// 先查询该菜单,确保存在
|
||||
o := orm.NewOrm()
|
||||
menu := models.Menu{Id: id}
|
||||
if err = o.Read(&menu); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "菜单不存在",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
menu.Status = req.Status
|
||||
if _, err := o.Update(&menu, "Status"); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "更新菜单状态失败",
|
||||
}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "菜单状态更新成功",
|
||||
}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteMenu 删除菜单
|
||||
func (c *MenuController) DeleteMenu() {
|
||||
id, err := c.GetInt(":id")
|
||||
|
||||
208
server/controllers/tenant.go
Normal file
208
server/controllers/tenant.go
Normal file
@ -0,0 +1,208 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"server/models"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
type TenantController struct {
|
||||
web.Controller
|
||||
}
|
||||
|
||||
// GetAllTenants 获取所有租户
|
||||
func (c *TenantController) GetAllTenants() {
|
||||
tenants, err := models.GetTenantList()
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "获取租户列表失败: " + err.Error(),
|
||||
}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "获取租户列表成功",
|
||||
"data": tenants,
|
||||
}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// CreateTenant 新增租户
|
||||
func (c *TenantController) CreateTenant() {
|
||||
var tenant models.Tenant
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &tenant); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "解析租户数据失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
err := models.CreateTenant(&tenant)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "创建租户失败: " + err.Error(),
|
||||
}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "租户创建成功",
|
||||
"data": tenant,
|
||||
}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// UpdateTenant 编辑租户
|
||||
func (c *TenantController) UpdateTenant() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "无效的租户ID",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &data); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "解析更新数据失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
err = models.UpdateTenant(id, data)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "编辑租户失败: " + err.Error(),
|
||||
}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "租户编辑成功",
|
||||
}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// DeleteTenant 删除租户
|
||||
func (c *TenantController) DeleteTenant() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "无效的租户ID",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
err = models.DeleteTenant(id)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "删除租户失败: " + err.Error(),
|
||||
}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "租户删除成功",
|
||||
}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// AuditTenant 审核租户
|
||||
func (c *TenantController) AuditTenant() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "无效的租户ID",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 JSON 请求体
|
||||
var auditData struct {
|
||||
AuditStatus string `json:"audit_status"`
|
||||
AuditComment string `json:"audit_comment"`
|
||||
AuditBy string `json:"audit_by"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(c.Ctx.Input.RequestBody, &auditData); err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "解析审核数据失败: " + err.Error(),
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
if auditData.AuditStatus == "" {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "审核状态不能为空",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
|
||||
err = models.AuditTenant(id, auditData.AuditStatus, auditData.AuditComment, auditData.AuditBy)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "审核租户失败: " + err.Error(),
|
||||
}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "审核成功",
|
||||
}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
|
||||
// GetTenantDetail 查看租户详情
|
||||
func (c *TenantController) GetTenantDetail() {
|
||||
idStr := c.Ctx.Input.Param(":id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "无效的租户ID",
|
||||
}
|
||||
c.ServeJSON()
|
||||
return
|
||||
}
|
||||
tenant, err := models.GetTenantById(id)
|
||||
if err != nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "获取租户详情失败: " + err.Error(),
|
||||
}
|
||||
} else if tenant == nil {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "租户不存在",
|
||||
}
|
||||
} else {
|
||||
c.Data["json"] = map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "获取租户详情成功",
|
||||
"data": tenant,
|
||||
}
|
||||
}
|
||||
c.ServeJSON()
|
||||
}
|
||||
119
server/database/README_TENANTS.md
Normal file
119
server/database/README_TENANTS.md
Normal file
@ -0,0 +1,119 @@
|
||||
# 租户表数据库创建说明
|
||||
|
||||
## 创建步骤
|
||||
|
||||
### 方法1:执行独立的 SQL 文件(推荐)
|
||||
```bash
|
||||
# 在 MySQL 中执行
|
||||
mysql -u root -p your_database < server/database/yz_tenants.sql
|
||||
```
|
||||
|
||||
### 方法2:手动执行 SQL
|
||||
```sql
|
||||
-- 1. 进入 MySQL
|
||||
mysql -u root -p your_database
|
||||
|
||||
-- 2. 执行 SQL 脚本
|
||||
SOURCE server/database/yz_tenants.sql;
|
||||
```
|
||||
|
||||
### 方法3:在 MySQL 客户端中复制粘贴
|
||||
直接打开 `server/database/yz_tenants.sql` 文件,复制所有内容,在 MySQL 客户端中执行。
|
||||
|
||||
## 创建的表
|
||||
|
||||
**yz_tenants** - 租户管理表
|
||||
|
||||
### 表结构说明
|
||||
|
||||
#### 基本信息字段
|
||||
- `id` - 租户ID(主键,自增)
|
||||
- `name` - 租户名称(必填)
|
||||
- `code` - 租户编码(必填,唯一)
|
||||
- `owner` - 负责人(必填)
|
||||
- `phone` - 联系电话(可选)
|
||||
- `email` - 邮箱地址(可选)
|
||||
|
||||
#### 状态字段
|
||||
- `status` - 状态:`enabled`(启用)或 `disabled`(禁用)
|
||||
- `audit_status` - 审核状态:
|
||||
- `pending` - 待审核
|
||||
- `approved` - 已通过
|
||||
- `rejected` - 已拒绝
|
||||
|
||||
#### 审核信息字段
|
||||
- `audit_comment` - 审核意见(可选)
|
||||
- `audit_by` - 审核人(可选)
|
||||
- `audit_time` - 审核时间(可选)
|
||||
|
||||
#### 其他字段
|
||||
- `remark` - 备注(可选)
|
||||
- `create_time` - 创建时间(自动)
|
||||
- `update_time` - 更新时间(自动)
|
||||
- `create_by` - 创建人(可选)
|
||||
- `update_by` - 更新人(可选)
|
||||
|
||||
### 索引说明
|
||||
- `uk_code` - 租户编码唯一索引
|
||||
- `idx_name` - 租户名称索引
|
||||
- `idx_owner` - 负责人索引
|
||||
- `idx_status` - 状态索引
|
||||
- `idx_audit_status` - 审核状态索引
|
||||
- `idx_create_time` - 创建时间索引
|
||||
|
||||
## 测试数据
|
||||
|
||||
SQL 文件中包含 10 条测试数据,涵盖了以下场景:
|
||||
1. **默认租户** - 已通过审核的系统默认租户
|
||||
2. **示例租户A** - 已通过审核的演示租户
|
||||
3. **示例租户B** - 待审核的租户
|
||||
4. **新申请租户C** - 待审核的新申请租户
|
||||
5. **已拒绝租户D** - 被拒绝的租户示例
|
||||
6. **企业租户E** - 已通过审核的企业级租户
|
||||
7. **测试租户F** - 已通过审核的测试环境租户
|
||||
8. **禁用租户G** - 已通过审核但被禁用的租户
|
||||
9. **小公司租户H** - 待审核的小型公司
|
||||
10. **个人开发者I** - 已通过审核的个人开发者账户
|
||||
|
||||
## 验证创建
|
||||
|
||||
```sql
|
||||
-- 查看租户表结构
|
||||
DESC yz_tenants;
|
||||
|
||||
-- 查看所有租户数据
|
||||
SELECT * FROM yz_tenants;
|
||||
|
||||
-- 查看特定状态的租户
|
||||
SELECT * FROM yz_tenants WHERE audit_status = 'pending';
|
||||
SELECT * FROM yz_tenants WHERE status = 'enabled';
|
||||
|
||||
-- 查看租户统计
|
||||
SELECT
|
||||
audit_status,
|
||||
COUNT(*) as count
|
||||
FROM yz_tenants
|
||||
GROUP BY audit_status;
|
||||
```
|
||||
|
||||
## 如果表已存在
|
||||
|
||||
如果想重新创建表(会清空现有数据):
|
||||
```sql
|
||||
DROP TABLE IF EXISTS yz_tenants;
|
||||
```
|
||||
然后再执行创建脚本。
|
||||
|
||||
## 与其他表的关系
|
||||
|
||||
租户表是系统中重要的基础表:
|
||||
- `yz_files` 表中的 `tenant_id` 字段引用租户编码(VARCHAR 类型,不是外键)
|
||||
- 未来可能会在 `yz_users` 表中添加 `tenant_id` 字段来关联租户
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **租户编码唯一性**:`code` 字段设置了唯一索引,确保每个租户编码都是唯一的
|
||||
2. **审核流程**:新创建的租户默认 `audit_status` 为 `pending`(待审核)
|
||||
3. **状态管理**:租户可以同时拥有 `status` 和 `audit_status` 两个状态字段,分别控制启用状态和审核状态
|
||||
4. **时间字段**:`create_time` 和 `update_time` 会自动管理,无需手动设置
|
||||
|
||||
@ -99,6 +99,44 @@ CREATE TABLE IF NOT EXISTS yz_program_info (
|
||||
CONSTRAINT yz_fk_program_category FOREIGN KEY (category_id) REFERENCES yz_program_category (category_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='程序信息表';
|
||||
|
||||
-- 检查并创建租户表(如果不存在)
|
||||
CREATE TABLE IF NOT EXISTS yz_tenants (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '租户ID',
|
||||
|
||||
-- 基本信息
|
||||
name VARCHAR(100) NOT NULL COMMENT '租户名称',
|
||||
code VARCHAR(50) NOT NULL COMMENT '租户编码(唯一)',
|
||||
owner VARCHAR(50) NOT NULL COMMENT '负责人',
|
||||
phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话',
|
||||
email VARCHAR(100) DEFAULT NULL COMMENT '邮箱地址',
|
||||
|
||||
-- 状态信息
|
||||
status VARCHAR(20) DEFAULT 'enabled' COMMENT '状态:enabled-启用,disabled-禁用',
|
||||
audit_status VARCHAR(20) DEFAULT 'pending' COMMENT '审核状态:pending-待审核,approved-已通过,rejected-已拒绝',
|
||||
|
||||
-- 审核信息
|
||||
audit_comment TEXT DEFAULT NULL COMMENT '审核意见',
|
||||
audit_by VARCHAR(50) DEFAULT NULL COMMENT '审核人',
|
||||
audit_time DATETIME DEFAULT NULL COMMENT '审核时间',
|
||||
|
||||
-- 其他信息
|
||||
remark TEXT DEFAULT NULL COMMENT '备注',
|
||||
|
||||
-- 时间戳
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
create_by VARCHAR(50) DEFAULT NULL COMMENT '创建人',
|
||||
update_by VARCHAR(50) DEFAULT NULL COMMENT '更新人',
|
||||
|
||||
-- 索引
|
||||
UNIQUE KEY uk_code (code),
|
||||
INDEX idx_name (name),
|
||||
INDEX idx_owner (owner),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_audit_status (audit_status),
|
||||
INDEX idx_create_time (create_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户表';
|
||||
|
||||
-- 检查并创建文件表(如果不存在)
|
||||
CREATE TABLE IF NOT EXISTS yz_files (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '文件ID',
|
||||
|
||||
@ -107,7 +107,49 @@ CREATE TABLE yz_program_info (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='程序信息表';
|
||||
|
||||
-- =============================================
|
||||
-- 4. 文件管理相关表
|
||||
-- 4. 租户管理相关表
|
||||
-- =============================================
|
||||
|
||||
-- 创建租户表
|
||||
CREATE TABLE yz_tenants (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '租户ID',
|
||||
|
||||
-- 基本信息
|
||||
name VARCHAR(100) NOT NULL COMMENT '租户名称',
|
||||
code VARCHAR(50) NOT NULL COMMENT '租户编码(唯一)',
|
||||
owner VARCHAR(50) NOT NULL COMMENT '负责人',
|
||||
phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话',
|
||||
email VARCHAR(100) DEFAULT NULL COMMENT '邮箱地址',
|
||||
|
||||
-- 状态信息
|
||||
status VARCHAR(20) DEFAULT 'enabled' COMMENT '状态:enabled-启用,disabled-禁用',
|
||||
audit_status VARCHAR(20) DEFAULT 'pending' COMMENT '审核状态:pending-待审核,approved-已通过,rejected-已拒绝',
|
||||
|
||||
-- 审核信息
|
||||
audit_comment TEXT DEFAULT NULL COMMENT '审核意见',
|
||||
audit_by VARCHAR(50) DEFAULT NULL COMMENT '审核人',
|
||||
audit_time DATETIME DEFAULT NULL COMMENT '审核时间',
|
||||
|
||||
-- 其他信息
|
||||
remark TEXT DEFAULT NULL COMMENT '备注',
|
||||
|
||||
-- 时间戳
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
create_by VARCHAR(50) DEFAULT NULL COMMENT '创建人',
|
||||
update_by VARCHAR(50) DEFAULT NULL COMMENT '更新人',
|
||||
|
||||
-- 索引
|
||||
UNIQUE KEY uk_code (code),
|
||||
INDEX idx_name (name),
|
||||
INDEX idx_owner (owner),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_audit_status (audit_status),
|
||||
INDEX idx_create_time (create_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户表';
|
||||
|
||||
-- =============================================
|
||||
-- 5. 文件管理相关表
|
||||
-- =============================================
|
||||
|
||||
-- 创建文件表
|
||||
@ -146,9 +188,15 @@ CREATE TABLE yz_files (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文件表';
|
||||
|
||||
-- =============================================
|
||||
-- 5. 插入初始数据
|
||||
-- 6. 插入初始数据
|
||||
-- =============================================
|
||||
|
||||
-- 插入默认租户数据
|
||||
INSERT INTO yz_tenants (name, code, owner, phone, email, status, audit_status, audit_comment, audit_by, audit_time, remark, create_by) VALUES
|
||||
('默认租户', 'default', 'admin', '13800138000', 'admin@yunzer.com', 'enabled', 'approved', '系统默认租户,自动通过审核', 'system', NOW(), '系统默认租户,用于初始化数据', 'system'),
|
||||
('示例租户A', 'demo-a', '张三', '13900139000', 'zhangsan@demo.com', 'enabled', 'approved', '资料完整,审核通过', 'admin', DATE_SUB(NOW(), INTERVAL 30 DAY), '演示租户A,用于展示功能', 'admin'),
|
||||
('示例租户B', 'demo-b', '李四', '13700137000', 'lisi@demo.com', 'enabled', 'pending', NULL, NULL, NULL, '待审核租户,资料已提交', 'admin');
|
||||
|
||||
-- 插入默认管理员用户
|
||||
-- 注意:实际使用时需要生成真实的加密密码和盐值
|
||||
INSERT INTO yz_users (username, password, salt, email, nickname, role, status, create_by) VALUES
|
||||
|
||||
111
server/database/yz_tenants.sql
Normal file
111
server/database/yz_tenants.sql
Normal file
@ -0,0 +1,111 @@
|
||||
-- 创建租户表
|
||||
-- 创建时间: 2025
|
||||
-- 描述: 云泽系统租户管理表
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- 检查并创建租户表(如果不存在)
|
||||
CREATE TABLE IF NOT EXISTS yz_tenants (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '租户ID',
|
||||
|
||||
-- 基本信息
|
||||
name VARCHAR(100) NOT NULL COMMENT '租户名称',
|
||||
code VARCHAR(50) NOT NULL COMMENT '租户编码(唯一)',
|
||||
owner VARCHAR(50) NOT NULL COMMENT '负责人',
|
||||
phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话',
|
||||
email VARCHAR(100) DEFAULT NULL COMMENT '邮箱地址',
|
||||
|
||||
-- 状态信息
|
||||
status VARCHAR(20) DEFAULT 'enabled' COMMENT '状态:enabled-启用,disabled-禁用',
|
||||
audit_status VARCHAR(20) DEFAULT 'pending' COMMENT '审核状态:pending-待审核,approved-已通过,rejected-已拒绝',
|
||||
|
||||
-- 审核信息
|
||||
audit_comment TEXT DEFAULT NULL COMMENT '审核意见',
|
||||
audit_by VARCHAR(50) DEFAULT NULL COMMENT '审核人',
|
||||
audit_time DATETIME DEFAULT NULL COMMENT '审核时间',
|
||||
|
||||
-- 其他信息
|
||||
remark TEXT DEFAULT NULL COMMENT '备注',
|
||||
|
||||
-- 时间戳
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
create_by VARCHAR(50) DEFAULT NULL COMMENT '创建人',
|
||||
update_by VARCHAR(50) DEFAULT NULL COMMENT '更新人',
|
||||
|
||||
-- 索引
|
||||
UNIQUE KEY uk_code (code),
|
||||
INDEX idx_name (name),
|
||||
INDEX idx_owner (owner),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_audit_status (audit_status),
|
||||
INDEX idx_create_time (create_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='租户表';
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- =============================================
|
||||
-- 插入测试数据
|
||||
-- =============================================
|
||||
|
||||
-- 清空现有测试数据(可选,注释掉以保留现有数据)
|
||||
-- DELETE FROM yz_tenants WHERE id > 0;
|
||||
|
||||
-- 插入测试租户数据
|
||||
INSERT INTO yz_tenants (
|
||||
name,
|
||||
code,
|
||||
owner,
|
||||
phone,
|
||||
email,
|
||||
status,
|
||||
audit_status,
|
||||
audit_comment,
|
||||
audit_by,
|
||||
audit_time,
|
||||
remark,
|
||||
create_by
|
||||
) VALUES
|
||||
-- 默认租户(已通过审核)
|
||||
('默认租户', 'default', 'admin', '13800138000', 'admin@yunzer.com', 'enabled', 'approved', '系统默认租户,自动通过审核', 'system', NOW(), '系统默认租户,用于初始化数据', 'system'),
|
||||
|
||||
-- 示例租户A(已通过审核)
|
||||
('示例租户A', 'demo-a', '张三', '13900139000', 'zhangsan@demo.com', 'enabled', 'approved', '资料完整,审核通过', 'admin', DATE_SUB(NOW(), INTERVAL 30 DAY), '演示租户A,用于展示功能', 'admin'),
|
||||
|
||||
-- 示例租户B(待审核)
|
||||
('示例租户B', 'demo-b', '李四', '13700137000', 'lisi@demo.com', 'enabled', 'pending', NULL, NULL, NULL, '待审核租户,资料已提交', 'admin'),
|
||||
|
||||
-- 新申请租户C(待审核)
|
||||
('新申请租户C', 'new-tenant-c', '王五', '13600136000', 'wangwu@new.com', 'enabled', 'pending', NULL, NULL, NULL, '新申请的租户,等待审核', 'admin'),
|
||||
|
||||
-- 已拒绝租户D
|
||||
('已拒绝租户D', 'rejected-tenant', '赵六', '13500135000', 'zhaoliu@reject.com', 'disabled', 'rejected', '申请资料不完整,缺少必要信息', 'admin', DATE_SUB(NOW(), INTERVAL 10 DAY), '申请被拒绝的租户示例', 'admin'),
|
||||
|
||||
-- 企业租户E(已通过)
|
||||
('企业租户E', 'enterprise-e', '陈七', '13400134000', 'chenqi@enterprise.com', 'enabled', 'approved', '企业级用户,认证通过', 'admin', DATE_SUB(NOW(), INTERVAL 15 DAY), '大型企业客户租户', 'admin'),
|
||||
|
||||
-- 测试租户F(已通过)
|
||||
('测试租户F', 'test-f', '刘八', '13300133000', 'liuba@test.com', 'enabled', 'approved', '测试环境使用,已通过', 'admin', DATE_SUB(NOW(), INTERVAL 5 DAY), '测试环境租户', 'admin'),
|
||||
|
||||
-- 禁用租户G
|
||||
('禁用租户G', 'disabled-g', '周九', '13200132000', 'zhoujiu@disabled.com', 'disabled', 'approved', '已通过审核但被禁用', 'admin', DATE_SUB(NOW(), INTERVAL 20 DAY), '已禁用的租户示例', 'admin'),
|
||||
|
||||
-- 小公司租户H(待审核)
|
||||
('小公司租户H', 'small-h', '吴十', '13100131000', 'wushi@small.com', 'enabled', 'pending', NULL, NULL, NULL, '小型公司申请,等待审核', 'admin'),
|
||||
|
||||
-- 个人开发者租户I(已通过)
|
||||
('个人开发者I', 'developer-i', '郑十一', '13000130000', 'zhengshiyi@dev.com', 'enabled', 'approved', '个人开发者账户,审核通过', 'admin', DATE_SUB(NOW(), INTERVAL 8 DAY), '个人开发者租户', 'admin');
|
||||
|
||||
-- 查询验证
|
||||
SELECT 'Tenants table created and test data inserted successfully!' as message;
|
||||
SELECT COUNT(*) as total_tenants FROM yz_tenants;
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
code,
|
||||
status,
|
||||
audit_status,
|
||||
create_time
|
||||
FROM yz_tenants
|
||||
ORDER BY id;
|
||||
@ -14,9 +14,10 @@ func main() {
|
||||
models.Init()
|
||||
|
||||
// CORS配置已移至router.go中统一管理
|
||||
// 确保请求体被正确读取
|
||||
// 确保请求体被正确读取(包括 POST、PUT、PATCH)
|
||||
beego.InsertFilter("*", beego.BeforeRouter, func(ctx *context.Context) {
|
||||
if ctx.Input.Method() == "PUT" || ctx.Input.Method() == "POST" {
|
||||
method := ctx.Input.Method()
|
||||
if method == "PUT" || method == "POST" || method == "PATCH" {
|
||||
ctx.Input.CopyBody(1024 * 1024) // 1MB 缓冲区
|
||||
}
|
||||
})
|
||||
|
||||
@ -14,18 +14,20 @@ var jwtSecret = []byte("yunzer_jwt_secret_key") // 在实际应用中应从配
|
||||
type Claims struct {
|
||||
UserID int `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
TenantId int `json:"tenant_id"` // 租户ID
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateToken 生成JWT token
|
||||
func GenerateToken(userID int, username string) (string, error) {
|
||||
func GenerateToken(userID int, username string, tenantId int) (string, error) {
|
||||
// 设置token过期时间
|
||||
expirationTime := time.Now().Add(24 * time.Hour) // 24小时后过期
|
||||
|
||||
// 创建claims
|
||||
claims := &Claims{
|
||||
UserID: userID,
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
TenantId: tenantId,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
@ -63,4 +65,4 @@ func ParseToken(tokenString string) (*Claims, error) {
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,6 +69,18 @@ func UpdateMenu(menu *Menu) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateMenuStatus 更新菜单状态
|
||||
func UpdateMenuStatus(id int, status int8) error {
|
||||
o := orm.NewOrm()
|
||||
menu := Menu{Id: id}
|
||||
if err := o.Read(&menu); err != nil {
|
||||
return err
|
||||
}
|
||||
menu.Status = status
|
||||
_, err := o.Update(&menu, "Status")
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteMenu 删除菜单
|
||||
func DeleteMenu(id int) error {
|
||||
o := orm.NewOrm()
|
||||
|
||||
127
server/models/tenant.go
Normal file
127
server/models/tenant.go
Normal file
@ -0,0 +1,127 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/beego/beego/v2/client/orm"
|
||||
)
|
||||
|
||||
type Tenant struct {
|
||||
Id int `orm:"pk;auto" json:"id"`
|
||||
Name string `orm:"size(100)" json:"name"`
|
||||
Code string `orm:"size(50);unique" json:"code"`
|
||||
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"` // enabled, disabled
|
||||
AuditStatus string `orm:"size(20);default(pending)" json:"audit_status"` // pending, approved, rejected
|
||||
AuditComment string `orm:"type(text);null" json:"audit_comment"`
|
||||
AuditBy string `orm:"size(50);null" json:"audit_by"`
|
||||
AuditTime *time.Time `orm:"null;type(datetime)" json:"audit_time"`
|
||||
Remark string `orm:"type(text);null" json:"remark"`
|
||||
CreateTime time.Time `orm:"auto_now_add;type(datetime)" json:"create_time"`
|
||||
UpdateTime time.Time `orm:"auto_now;type(datetime)" json:"update_time"`
|
||||
DeleteTime *time.Time `orm:"null;type(datetime)" json:"delete_time"`
|
||||
CreateBy string `orm:"size(50);null" json:"create_by"`
|
||||
UpdateBy string `orm:"size(50);null" json:"update_by"`
|
||||
}
|
||||
|
||||
// TableName 设置表名
|
||||
func (t *Tenant) TableName() string {
|
||||
return "yz_tenants"
|
||||
}
|
||||
|
||||
func init() {
|
||||
orm.RegisterModel(new(Tenant))
|
||||
}
|
||||
|
||||
// GetTenantList 获取所有租户(软删除:只返回未删除的)
|
||||
func GetTenantList() ([]Tenant, error) {
|
||||
o := orm.NewOrm()
|
||||
var tenants []Tenant
|
||||
// 使用原生 SQL 查询 delete_time IS NULL 的记录(更可靠)
|
||||
_, err := o.Raw("SELECT * FROM yz_tenants WHERE delete_time IS NULL ORDER BY id DESC").QueryRows(&tenants)
|
||||
return tenants, err
|
||||
}
|
||||
|
||||
// CreateTenant 新建租户
|
||||
func CreateTenant(tenant *Tenant) error {
|
||||
o := orm.NewOrm()
|
||||
_, err := o.Insert(tenant)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateTenant 编辑租户(只能编辑未删除的)
|
||||
func UpdateTenant(id int, data map[string]interface{}) error {
|
||||
o := orm.NewOrm()
|
||||
// 先检查租户是否存在且未删除
|
||||
tenant := Tenant{}
|
||||
err := o.Raw("SELECT * FROM yz_tenants WHERE id = ? AND delete_time IS NULL", id).QueryRow(&tenant)
|
||||
if err == orm.ErrNoRows {
|
||||
return errors.New("租户不存在或已被删除")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 更新记录
|
||||
_, err = o.QueryTable(new(Tenant)).Filter("Id", id).Update(data)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteTenant 软删除租户(设置 delete_time)
|
||||
func DeleteTenant(id int) error {
|
||||
o := orm.NewOrm()
|
||||
deleteTime := time.Now()
|
||||
_, err := o.QueryTable(new(Tenant)).Filter("Id", id).Update(orm.Params{"delete_time": deleteTime})
|
||||
return err
|
||||
}
|
||||
|
||||
// AuditTenant 审核租户(只能审核未删除的)
|
||||
func AuditTenant(id int, auditStatus, auditComment, auditBy string) error {
|
||||
o := orm.NewOrm()
|
||||
tenant := Tenant{}
|
||||
// 先检查租户是否存在且未删除
|
||||
err := o.Raw("SELECT * FROM yz_tenants WHERE id = ? AND delete_time IS NULL", id).QueryRow(&tenant)
|
||||
if err == orm.ErrNoRows {
|
||||
return errors.New("租户不存在或已被删除")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
tenant.AuditStatus = auditStatus
|
||||
tenant.AuditComment = auditComment
|
||||
tenant.AuditBy = auditBy
|
||||
tenant.AuditTime = &now
|
||||
|
||||
_, err = o.Update(&tenant, "AuditStatus", "AuditComment", "AuditBy", "AuditTime")
|
||||
return err
|
||||
}
|
||||
|
||||
// GetTenantById 根据ID获取租户详情(只返回未删除的)
|
||||
func GetTenantById(id int) (*Tenant, error) {
|
||||
o := orm.NewOrm()
|
||||
tenant := Tenant{}
|
||||
// 使用原生 SQL 查询,只返回未删除的记录
|
||||
err := o.Raw("SELECT * FROM yz_tenants WHERE id = ? AND delete_time IS NULL", id).QueryRow(&tenant)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &tenant, err
|
||||
}
|
||||
|
||||
// GetTenantByName 根据名称获取租户详情(只返回未删除的)
|
||||
func GetTenantByName(name string) (*Tenant, error) {
|
||||
o := orm.NewOrm()
|
||||
tenant := Tenant{}
|
||||
// 使用原生 SQL 查询,只返回未删除的记录
|
||||
err := o.Raw("SELECT * FROM yz_tenants WHERE name = ? AND delete_time IS NULL", name).QueryRow(&tenant)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, errors.New("租户不存在")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tenant, nil
|
||||
}
|
||||
@ -16,7 +16,8 @@ import (
|
||||
// User 用户模型,增加Salt字段存储每个用户的唯一盐值
|
||||
type User struct {
|
||||
Id int `orm:"auto"`
|
||||
Username string `orm:"unique"`
|
||||
TenantId int `orm:"column(tenant_id);default(0)" json:"tenant_id"` // 租户ID
|
||||
Username string // 用户名不再全局唯一,而是在租户内唯一(tenant_id + username 的组合唯一)
|
||||
Password string // 存储加密后的密码
|
||||
Salt string // 存储该用户的唯一盐值
|
||||
Email string
|
||||
@ -66,15 +67,14 @@ func verifyPassword(password, salt, storedHash string) bool {
|
||||
return hash == storedHash
|
||||
}
|
||||
|
||||
// ResetPassword 重置用户密码
|
||||
func ResetPassword(username, superPassword string) error {
|
||||
// ResetPassword 重置用户密码(支持租户模式)
|
||||
func ResetPassword(username, superPassword string, tenantId int) error {
|
||||
if superPassword != "Lzq920103" {
|
||||
return fmt.Errorf("超级密码错误")
|
||||
}
|
||||
|
||||
o := orm.NewOrm()
|
||||
user := &User{Username: username}
|
||||
err := o.Read(user, "Username")
|
||||
user, err := GetUserByUsername(username, tenantId)
|
||||
if err != nil {
|
||||
return fmt.Errorf("用户不存在: %v", err)
|
||||
}
|
||||
@ -102,9 +102,9 @@ func ResetPassword(username, superPassword string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangePassword 修改用户密码
|
||||
func ChangePassword(username, oldPassword, newPassword string) error {
|
||||
user, err := GetUserByUsername(username)
|
||||
// ChangePassword 修改用户密码(支持租户模式)
|
||||
func ChangePassword(username, oldPassword, newPassword string, tenantId int) error {
|
||||
user, err := GetUserByUsername(username, tenantId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -124,22 +124,32 @@ func ChangePassword(username, oldPassword, newPassword string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// FindAllUsers 获取所有用户
|
||||
func FindAllUsers() []*User {
|
||||
// FindAllUsers 获取所有用户(支持按租户过滤)
|
||||
func FindAllUsers(tenantId int) []*User {
|
||||
o := orm.NewOrm()
|
||||
var users []*User
|
||||
_, err := o.QueryTable("yz_users").All(&users)
|
||||
if err != nil {
|
||||
return []*User{}
|
||||
if tenantId > 0 {
|
||||
// 按租户ID查询
|
||||
_, err := o.Raw("SELECT * FROM yz_users WHERE tenant_id = ?", tenantId).QueryRows(&users)
|
||||
if err != nil {
|
||||
return []*User{}
|
||||
}
|
||||
} else {
|
||||
// 查询所有用户
|
||||
_, err := o.QueryTable("yz_users").All(&users)
|
||||
if err != nil {
|
||||
return []*User{}
|
||||
}
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
// GetUserByUsername 根据用户名获取用户
|
||||
func GetUserByUsername(username string) (*User, error) {
|
||||
// GetUserByUsername 根据用户名获取用户(支持租户隔离)
|
||||
func GetUserByUsername(username string, tenantId int) (*User, error) {
|
||||
o := orm.NewOrm()
|
||||
user := &User{Username: username}
|
||||
err := o.Read(user, "Username")
|
||||
user := &User{}
|
||||
// 使用原生 SQL 查询,考虑租户ID
|
||||
err := o.Raw("SELECT * FROM yz_users WHERE username = ? AND tenant_id = ?", username, tenantId).QueryRow(user)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, errors.New("用户不存在")
|
||||
}
|
||||
@ -149,24 +159,74 @@ func GetUserByUsername(username string) (*User, error) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// ValidateUser 验证用户登录信息
|
||||
func ValidateUser(username, password string) (*User, error) {
|
||||
user, err := GetUserByUsername(username)
|
||||
// ValidateUser 验证用户登录信息(支持租户模式,根据租户名称)
|
||||
// 先验证租户是否存在且有效,再验证租户下的用户
|
||||
func ValidateUser(username, password string, tenantName string) (*User, error) {
|
||||
o := orm.NewOrm()
|
||||
|
||||
// 1. 根据租户名称查询租户(只查询未删除的)
|
||||
var tenant struct {
|
||||
Id int
|
||||
Status string
|
||||
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)
|
||||
if err == orm.ErrNoRows {
|
||||
// 租户不存在(数据库中根本没有这个名称)
|
||||
return nil, errors.New("租户不存在")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询租户失败: %v", err)
|
||||
}
|
||||
|
||||
// 检查租户是否被删除(软删除)
|
||||
if tenant.DeleteTime != nil {
|
||||
// delete_time 不为 NULL,说明已被删除
|
||||
return nil, errors.New("租户已被删除")
|
||||
}
|
||||
|
||||
// 检查租户状态
|
||||
if tenant.Status == "disabled" {
|
||||
return nil, errors.New("租户已被禁用")
|
||||
}
|
||||
|
||||
if tenant.Status != "enabled" {
|
||||
return nil, fmt.Errorf("租户状态异常: %s", tenant.Status)
|
||||
}
|
||||
|
||||
tenantId := tenant.Id
|
||||
|
||||
// 2. 获取租户下的用户
|
||||
user, err := GetUserByUsername(username, tenantId)
|
||||
if err != nil {
|
||||
// 用户不存在或查询失败
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 验证密码
|
||||
if verifyPassword(password, user.Salt, user.Password) {
|
||||
return user, nil
|
||||
}
|
||||
return nil, errors.New("密码不正确")
|
||||
}
|
||||
|
||||
// AddUser 向数据库添加新用户(模型层核心方法)
|
||||
func AddUser(username, password, email, nickname, avatar string) (*User, error) {
|
||||
// 1. 检查用户是否已存在(避免用户名重复)
|
||||
existingUser, err := GetUserByUsername(username)
|
||||
// AddUser 向数据库添加新用户(模型层核心方法,支持租户模式)
|
||||
func AddUser(username, password, email, nickname, avatar string, tenantId int) (*User, error) {
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("验证租户失败: %v", err)
|
||||
}
|
||||
if !tenantExists {
|
||||
return nil, fmt.Errorf("租户不存在或已被禁用")
|
||||
}
|
||||
|
||||
// 2. 检查该租户下用户是否已存在(避免用户名重复,但不同租户可以有相同的用户名)
|
||||
existingUser, err := GetUserByUsername(username, tenantId)
|
||||
if err == nil && existingUser != nil {
|
||||
return nil, fmt.Errorf("用户名已存在")
|
||||
return nil, fmt.Errorf("该租户下用户名已存在")
|
||||
}
|
||||
if err != nil && err.Error() != "用户不存在" { // 排除"用户不存在"的正常错误
|
||||
return nil, fmt.Errorf("查询用户失败: %v", err)
|
||||
@ -186,6 +246,7 @@ func AddUser(username, password, email, nickname, avatar string) (*User, error)
|
||||
|
||||
// 4. 构建用户对象
|
||||
user := &User{
|
||||
TenantId: tenantId,
|
||||
Username: username,
|
||||
Password: hashedPassword, // 存储加密后的密码
|
||||
Salt: salt, // 存储盐值(用于后续验证)
|
||||
@ -194,8 +255,7 @@ func AddUser(username, password, email, nickname, avatar string) (*User, error)
|
||||
Avatar: avatar,
|
||||
}
|
||||
|
||||
// 5. 插入数据库
|
||||
o := orm.NewOrm()
|
||||
// 5. 插入数据库(使用之前定义的 o)
|
||||
_, err = o.Insert(user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("数据库插入失败: %v", err)
|
||||
@ -205,22 +265,25 @@ func AddUser(username, password, email, nickname, avatar string) (*User, error)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// UpdateUser 更新用户信息(模型层方法)
|
||||
func UpdateUser(id int, username, email, nickname, avatar string) (*User, error) {
|
||||
// 1. 根据ID查询用户是否存在
|
||||
// UpdateUser 更新用户信息(模型层方法,支持租户模式)
|
||||
func UpdateUser(id int, username, email, nickname, avatar string, tenantId int) (*User, error) {
|
||||
// 1. 根据ID和租户ID查询用户是否存在(确保只能更新自己租户下的用户)
|
||||
o := orm.NewOrm()
|
||||
user := &User{Id: id}
|
||||
err := o.Read(user)
|
||||
user := &User{}
|
||||
err := o.Raw("SELECT * FROM yz_users WHERE id = ? AND tenant_id = ?", id, tenantId).QueryRow(user)
|
||||
if err == orm.ErrNoRows {
|
||||
return nil, fmt.Errorf("用户不存在或不属于该租户")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("用户不存在: %v", err)
|
||||
return nil, fmt.Errorf("查询用户失败: %v", err)
|
||||
}
|
||||
|
||||
// 2. 仅更新非空字段(避免覆盖原有值)
|
||||
if username != "" {
|
||||
// 若更新用户名,需检查新用户名是否已被占用
|
||||
existingUser, _ := GetUserByUsername(username)
|
||||
// 若更新用户名,需检查同一租户下新用户名是否已被占用
|
||||
existingUser, _ := GetUserByUsername(username, tenantId)
|
||||
if existingUser != nil && existingUser.Id != id {
|
||||
return nil, fmt.Errorf("用户名已被占用")
|
||||
return nil, fmt.Errorf("该租户下用户名已被占用")
|
||||
}
|
||||
user.Username = username
|
||||
}
|
||||
@ -243,14 +306,17 @@ func UpdateUser(id int, username, email, nickname, avatar string) (*User, error)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// DeleteUser 根据ID删除用户(模型层方法)
|
||||
func DeleteUser(id int) error {
|
||||
// DeleteUser 根据ID删除用户(模型层方法,支持租户模式)
|
||||
func DeleteUser(id int, tenantId int) error {
|
||||
o := orm.NewOrm()
|
||||
// 先查询用户是否存在
|
||||
user := &User{Id: id}
|
||||
err := o.Read(user)
|
||||
// 先查询用户是否存在且属于指定租户
|
||||
user := &User{}
|
||||
err := o.Raw("SELECT * FROM yz_users WHERE id = ? AND tenant_id = ?", id, tenantId).QueryRow(user)
|
||||
if err == orm.ErrNoRows {
|
||||
return fmt.Errorf("用户不存在或不属于该租户")
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("用户不存在: %v", err)
|
||||
return fmt.Errorf("查询用户失败: %v", err)
|
||||
}
|
||||
|
||||
// 执行删除操作
|
||||
|
||||
@ -14,7 +14,7 @@ func init() {
|
||||
beego.InsertFilter("*", beego.BeforeRouter, func(ctx *context.Context) {
|
||||
// 设置CORS头
|
||||
ctx.Output.Header("Access-Control-Allow-Origin", "*")
|
||||
ctx.Output.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
ctx.Output.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
|
||||
ctx.Output.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
|
||||
ctx.Output.Header("Access-Control-Allow-Credentials", "true")
|
||||
ctx.Output.Header("Access-Control-Max-Age", "86400")
|
||||
@ -72,6 +72,7 @@ func init() {
|
||||
beego.Router("/api/menu", &controllers.MenuController{}, "post:CreateMenu")
|
||||
beego.Router("/api/menu/:id", &controllers.MenuController{}, "put:UpdateMenu")
|
||||
beego.Router("/api/menu/:id", &controllers.MenuController{}, "delete:DeleteMenu")
|
||||
beego.Router("/api/menu/status/:id", &controllers.MenuController{}, "patch:UpdateMenuStatus")
|
||||
|
||||
// 程序分类路由 - 自动映射到 /api/programcategory/*
|
||||
beego.AutoRouter(&controllers.ProgramCategoryController{})
|
||||
@ -100,6 +101,14 @@ func init() {
|
||||
beego.Router("/api/knowledge/category/add", &controllers.KnowledgeController{}, "post:AddCategory")
|
||||
beego.Router("/api/knowledge/tag/add", &controllers.KnowledgeController{}, "post:AddTag")
|
||||
|
||||
//租户相关路由
|
||||
beego.Router("/api/tenant/list", &controllers.TenantController{}, "get:GetAllTenants")
|
||||
beego.Router("/api/tenant", &controllers.TenantController{}, "post:CreateTenant")
|
||||
beego.Router("/api/tenant/:id", &controllers.TenantController{}, "put:UpdateTenant")
|
||||
beego.Router("/api/tenant/:id", &controllers.TenantController{}, "delete:DeleteTenant")
|
||||
beego.Router("/api/tenant/:id/audit", &controllers.TenantController{}, "post:AuditTenant")
|
||||
beego.Router("/api/tenant/:id", &controllers.TenantController{}, "get:GetTenantDetail")
|
||||
|
||||
// 手动配置特殊路由(无法通过自动路由处理的)
|
||||
beego.Router("/api/allmenu", &controllers.MenuController{}, "get:GetAllMenus")
|
||||
beego.Router("/api/program-categories/public", &controllers.ProgramCategoryController{}, "get:GetProgramCategoriesPublic")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user