完成租户登录
This commit is contained in:
Generated
+539
@@ -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"
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
+63
-20
@@ -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,
|
||||
});
|
||||
}
|
||||
+3
-2
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
+5
-1
@@ -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);
|
||||
});
|
||||
|
||||
+78
-34
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
+423
-56
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user