修复用户和文件管理,增加文件预览
This commit is contained in:
parent
2023fe4b97
commit
88d2f527f4
420
package-lock.json
generated
Normal file
420
package-lock.json
generated
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
{
|
||||||
|
"name": "yunzer_go",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"docx-preview": "^0.3.7",
|
||||||
|
"vue3-pdf-app": "^1.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-string-parser": {
|
||||||
|
"version": "7.27.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||||
|
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
|
"version": "7.28.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||||
|
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/parser": {
|
||||||
|
"version": "7.28.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.5.tgz",
|
||||||
|
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.28.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"parser": "bin/babel-parser.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@babel/types": {
|
||||||
|
"version": "7.28.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.5.tgz",
|
||||||
|
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-string-parser": "^7.27.1",
|
||||||
|
"@babel/helper-validator-identifier": "^7.28.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
|
"version": "1.5.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-core": {
|
||||||
|
"version": "3.5.22",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.22.tgz",
|
||||||
|
"integrity": "sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.28.4",
|
||||||
|
"@vue/shared": "3.5.22",
|
||||||
|
"entities": "^4.5.0",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-dom": {
|
||||||
|
"version": "3.5.22",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz",
|
||||||
|
"integrity": "sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-core": "3.5.22",
|
||||||
|
"@vue/shared": "3.5.22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-sfc": {
|
||||||
|
"version": "3.5.22",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz",
|
||||||
|
"integrity": "sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.28.4",
|
||||||
|
"@vue/compiler-core": "3.5.22",
|
||||||
|
"@vue/compiler-dom": "3.5.22",
|
||||||
|
"@vue/compiler-ssr": "3.5.22",
|
||||||
|
"@vue/shared": "3.5.22",
|
||||||
|
"estree-walker": "^2.0.2",
|
||||||
|
"magic-string": "^0.30.19",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/compiler-ssr": {
|
||||||
|
"version": "3.5.22",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz",
|
||||||
|
"integrity": "sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-dom": "3.5.22",
|
||||||
|
"@vue/shared": "3.5.22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/reactivity": {
|
||||||
|
"version": "3.5.22",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.22.tgz",
|
||||||
|
"integrity": "sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/shared": "3.5.22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/runtime-core": {
|
||||||
|
"version": "3.5.22",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.22.tgz",
|
||||||
|
"integrity": "sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/reactivity": "3.5.22",
|
||||||
|
"@vue/shared": "3.5.22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/runtime-dom": {
|
||||||
|
"version": "3.5.22",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz",
|
||||||
|
"integrity": "sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/reactivity": "3.5.22",
|
||||||
|
"@vue/runtime-core": "3.5.22",
|
||||||
|
"@vue/shared": "3.5.22",
|
||||||
|
"csstype": "^3.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/server-renderer": {
|
||||||
|
"version": "3.5.22",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.22.tgz",
|
||||||
|
"integrity": "sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-ssr": "3.5.22",
|
||||||
|
"@vue/shared": "3.5.22"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "3.5.22"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/shared": {
|
||||||
|
"version": "3.5.22",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.22.tgz",
|
||||||
|
"integrity": "sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/core-util-is": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/docx-preview": {
|
||||||
|
"version": "0.3.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/docx-preview/-/docx-preview-0.3.7.tgz",
|
||||||
|
"integrity": "sha512-Lav69CTA/IYZPJTsKH7oYeoZjyg96N0wEJMNslGJnZJ+dMUZK85Lt5ASC79yUlD48ecWjuv+rkcmFt6EVPV0Xg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"jszip": ">=3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/estree-walker": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/immediate": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/jszip": {
|
||||||
|
"version": "3.10.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz",
|
||||||
|
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||||
|
"license": "(MIT OR GPL-3.0-or-later)",
|
||||||
|
"dependencies": {
|
||||||
|
"lie": "~3.3.0",
|
||||||
|
"pako": "~1.0.2",
|
||||||
|
"readable-stream": "~2.3.6",
|
||||||
|
"setimmediate": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lie": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"immediate": "~3.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/magic-string": {
|
||||||
|
"version": "0.30.21",
|
||||||
|
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
|
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nanoid": {
|
||||||
|
"version": "3.3.11",
|
||||||
|
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
|
"node_modules/picocolors": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/postcss": {
|
||||||
|
"version": "8.5.6",
|
||||||
|
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
|
||||||
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/postcss/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "tidelift",
|
||||||
|
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"nanoid": "^3.3.11",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"source-map-js": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10 || ^12 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/process-nextick-args": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/setimmediate": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/source-map-js": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/vue": {
|
||||||
|
"version": "3.5.22",
|
||||||
|
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.22.tgz",
|
||||||
|
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/compiler-dom": "3.5.22",
|
||||||
|
"@vue/compiler-sfc": "3.5.22",
|
||||||
|
"@vue/runtime-dom": "3.5.22",
|
||||||
|
"@vue/server-renderer": "3.5.22",
|
||||||
|
"@vue/shared": "3.5.22"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue3-pdf-app": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/vue3-pdf-app/-/vue3-pdf-app-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-qegWTIF4wYKiocZ3KreB70wRXhqSdXWbdERDyyKzT7d5PbjKbS9tD6vaKkCqh3PzTM84NyKPYrQ3iuwJb60YPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
package.json
Normal file
6
package.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"docx-preview": "^0.3.7",
|
||||||
|
"vue3-pdf-app": "^1.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -98,3 +98,42 @@ export function searchFiles(keyword) {
|
|||||||
params: { keyword },
|
params: { keyword },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文件
|
||||||
|
* @param {number|string} id 文件ID
|
||||||
|
* @returns {string} 下载URL
|
||||||
|
*/
|
||||||
|
export function downloadFile(id) {
|
||||||
|
return `/api/files/download/${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预览文件
|
||||||
|
* @param {number|string} id 文件ID
|
||||||
|
* @returns {string} 预览URL
|
||||||
|
*/
|
||||||
|
export function previewFile(id) {
|
||||||
|
return `/api/files/preview/${id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件是否可预览
|
||||||
|
* @param {string} fileExt 文件扩展名
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function canPreview(fileExt) {
|
||||||
|
const previewableExts = [
|
||||||
|
// 图片格式
|
||||||
|
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg',
|
||||||
|
// 文档格式
|
||||||
|
'.pdf', '.txt',
|
||||||
|
// Office文档格式(仅支持 .docx,不支持旧的 .doc、Excel、PPT)
|
||||||
|
'.docx',
|
||||||
|
// 视频格式
|
||||||
|
'.mp4', '.webm',
|
||||||
|
// 音频格式
|
||||||
|
'.mp3', '.wav'
|
||||||
|
];
|
||||||
|
return previewableExts.includes(fileExt.toLowerCase());
|
||||||
|
}
|
||||||
@ -16,6 +16,14 @@ export function getUserInfo(userId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//获取租户用户信息
|
||||||
|
export function getTenantUsers(tenantId) {
|
||||||
|
return request({
|
||||||
|
url: `/api/tenantUsers/${tenantId}`,
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 添加用户
|
// 添加用户
|
||||||
export function addUser(data) {
|
export function addUser(data) {
|
||||||
return request({
|
return request({
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import CommonHeader from '@/components/CommonHeader.vue';
|
|||||||
import { useTabsStore } from '@/stores';
|
import { useTabsStore } from '@/stores';
|
||||||
import { useRouter, useRoute } from 'vue-router';
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
import { ref, watch, reactive, nextTick, onMounted, computed } from 'vue';
|
import { ref, watch, reactive, nextTick, onMounted, computed } from 'vue';
|
||||||
import { More, Close, CircleClose } from '@element-plus/icons-vue'
|
import { More, Close, CircleClose, ArrowUp } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const tabsStore = useTabsStore();
|
const tabsStore = useTabsStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -451,6 +451,15 @@ const canCloseRight = computed(() => {
|
|||||||
</keep-alive>
|
</keep-alive>
|
||||||
</router-view>
|
</router-view>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 回到顶部按钮 -->
|
||||||
|
<el-backtop :target="'.right-main'" :visibility-height="300" :right="30" :bottom="50">
|
||||||
|
<div class="backtop-button">
|
||||||
|
<el-icon :size="20">
|
||||||
|
<ArrowUp />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</el-backtop>
|
||||||
</el-main>
|
</el-main>
|
||||||
</el-container>
|
</el-container>
|
||||||
</el-container>
|
</el-container>
|
||||||
@ -606,6 +615,31 @@ const canCloseRight = computed(() => {
|
|||||||
:deep(.el-menu){
|
:deep(.el-menu){
|
||||||
border-right: none !important;
|
border-right: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 回到顶部按钮样式
|
||||||
|
.backtop-button {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--el-color-primary);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 4px 12px rgba(64, 129, 255, 0.4);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--el-color-primary-light-3);
|
||||||
|
box-shadow: 0 6px 16px rgba(64, 129, 255, 0.5);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
|
|||||||
431
pc/src/views/system/files/FileCategory.vue
Normal file
431
pc/src/views/system/files/FileCategory.vue
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
<template>
|
||||||
|
<div class="category-container">
|
||||||
|
<!-- 顶部操作栏 -->
|
||||||
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" @click="showAddDialog = true">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
添加分类
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="refreshCategories">
|
||||||
|
<el-icon><RefreshRight /></el-icon>
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分类列表 -->
|
||||||
|
<div class="category-list">
|
||||||
|
<el-table :data="categories" v-loading="loading" style="width: 100%" stripe>
|
||||||
|
<el-table-column type="index" label="#" width="60" />
|
||||||
|
<el-table-column prop="name" label="分类名称" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="category-name">
|
||||||
|
<el-tag :color="row.color" effect="dark" size="large">
|
||||||
|
{{ row.name }}
|
||||||
|
</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="description" label="描述" min-width="250" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="file_count" label="文件数量" width="120" sortable>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-badge :value="row.file_count || 0" :max="999" class="item">
|
||||||
|
<el-button size="small" text type="primary">
|
||||||
|
{{ row.file_count || 0 }} 个文件
|
||||||
|
</el-button>
|
||||||
|
</el-badge>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="sort_order" label="排序" width="100" sortable>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag type="info">{{ row.sort_order || 0 }}</el-tag>
|
||||||
|
</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)"
|
||||||
|
:disabled="loading"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button size="small" type="primary" @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" show-alpha />
|
||||||
|
<span class="tip-text">选择分类显示颜色</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序" prop="sort_order">
|
||||||
|
<el-input-number
|
||||||
|
v-model="categoryForm.sort_order"
|
||||||
|
:min="0"
|
||||||
|
:max="999"
|
||||||
|
placeholder="排序值,越小越靠前"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-switch
|
||||||
|
v-model="categoryForm.status"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
active-text="启用"
|
||||||
|
inactive-text="禁用"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="handleDialogClose">取 消</el-button>
|
||||||
|
<el-button type="primary" @click="saveCategory" :loading="saving">
|
||||||
|
{{ saving ? '保存中...' : '确 定' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 统计信息卡片 -->
|
||||||
|
<div class="stats-cards">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :xs="24" :sm="12" :md="6">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon" style="background: #e6f7ff; color: #1890ff">
|
||||||
|
<el-icon><Grid /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-label">总分类数</div>
|
||||||
|
<div class="stat-value">{{ totalCategories }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="6">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon" style="background: #f0f9ff; color: #52c41a">
|
||||||
|
<el-icon><SuccessFilled /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-label">启用分类</div>
|
||||||
|
<div class="stat-value">{{ enabledCategories }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="6">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon" style="background: #fff7e6; color: #faad14">
|
||||||
|
<el-icon><Document /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-label">关联文件</div>
|
||||||
|
<div class="stat-value">{{ totalFiles }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :sm="12" :md="6">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon" style="background: #fff1f0; color: #f5222d">
|
||||||
|
<el-icon><CircleClose /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-label">禁用分类</div>
|
||||||
|
<div class="stat-value">{{ disabledCategories }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, reactive } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
RefreshRight,
|
||||||
|
Grid,
|
||||||
|
Document,
|
||||||
|
SuccessFilled,
|
||||||
|
CircleClose,
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
// Props & Emits
|
||||||
|
const emit = defineEmits(['category-change'])
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const loading = ref(false)
|
||||||
|
const saving = ref(false)
|
||||||
|
const showAddDialog = ref(false)
|
||||||
|
const editingCategory = ref<any>(null)
|
||||||
|
const categoryFormRef = ref<FormInstance>()
|
||||||
|
|
||||||
|
const categories = ref([
|
||||||
|
{ id: 1, name: '文档', description: '各类文档文件', color: '#409eff', file_count: 15, sort_order: 1, status: 1 },
|
||||||
|
{ id: 2, name: '图片', description: '图片和照片', color: '#67c23a', file_count: 32, sort_order: 2, status: 1 },
|
||||||
|
{ id: 3, name: '视频', description: '视频文件', color: '#e6a23c', file_count: 8, sort_order: 3, status: 1 },
|
||||||
|
{ id: 4, name: '音频', description: '音频文件', color: '#f56c6c', file_count: 5, sort_order: 4, status: 1 },
|
||||||
|
{ id: 5, name: '编辑器', description: '编辑器上传的文件', color: '#909399', file_count: 28, sort_order: 5, status: 1 },
|
||||||
|
{ id: 6, name: '其他', description: '其他类型文件', color: '#606266', file_count: 12, sort_order: 6, status: 1 },
|
||||||
|
])
|
||||||
|
|
||||||
|
const categoryForm = reactive({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
color: '#409eff',
|
||||||
|
sort_order: 0,
|
||||||
|
status: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
const categoryRules = reactive<FormRules>({
|
||||||
|
name: [
|
||||||
|
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||||
|
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
description: [
|
||||||
|
{ max: 200, message: '描述不能超过 200 个字符', trigger: 'blur' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
const totalCategories = computed(() => categories.value.length)
|
||||||
|
const enabledCategories = computed(() => categories.value.filter((c) => c.status === 1).length)
|
||||||
|
const disabledCategories = computed(() => categories.value.filter((c) => c.status === 0).length)
|
||||||
|
const totalFiles = computed(() => categories.value.reduce((sum, c) => sum + (c.file_count || 0), 0))
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
const refreshCategories = () => {
|
||||||
|
loading.value = true
|
||||||
|
// 这里应该调用API获取分类列表
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false
|
||||||
|
ElMessage.success('刷新成功')
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const editCategory = (row: any) => {
|
||||||
|
editingCategory.value = row
|
||||||
|
categoryForm.name = row.name
|
||||||
|
categoryForm.description = row.description || ''
|
||||||
|
categoryForm.color = row.color || '#409eff'
|
||||||
|
categoryForm.sort_order = row.sort_order || 0
|
||||||
|
categoryForm.status = row.status
|
||||||
|
showAddDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteCategory = async (row: any) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除分类「${row.name}」吗?该分类下有 ${row.file_count || 0} 个文件,删除后文件将归入"未分类"。`,
|
||||||
|
'警告',
|
||||||
|
{
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
// 这里应该调用API删除分类
|
||||||
|
setTimeout(() => {
|
||||||
|
const index = categories.value.findIndex((c) => c.id === row.id)
|
||||||
|
if (index > -1) {
|
||||||
|
categories.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
emit('category-change')
|
||||||
|
}, 500)
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err !== 'cancel') {
|
||||||
|
ElMessage.error(err.message || '删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCategoryStatus = (row: any) => {
|
||||||
|
loading.value = true
|
||||||
|
// 这里应该调用API更新状态
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false
|
||||||
|
ElMessage.success(row.status === 1 ? '已启用' : '已禁用')
|
||||||
|
emit('category-change')
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveCategory = async () => {
|
||||||
|
if (!categoryFormRef.value) return
|
||||||
|
|
||||||
|
await categoryFormRef.value.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 这里应该调用API保存分类
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
|
|
||||||
|
if (editingCategory.value) {
|
||||||
|
// 更新现有分类
|
||||||
|
Object.assign(editingCategory.value, categoryForm)
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
} else {
|
||||||
|
// 添加新分类
|
||||||
|
const newCategory = {
|
||||||
|
id: categories.value.length + 1,
|
||||||
|
...categoryForm,
|
||||||
|
file_count: 0,
|
||||||
|
}
|
||||||
|
categories.value.push(newCategory)
|
||||||
|
ElMessage.success('添加成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
showAddDialog.value = false
|
||||||
|
resetForm()
|
||||||
|
emit('category-change')
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error.message || '保存失败')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDialogClose = () => {
|
||||||
|
showAddDialog.value = false
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
editingCategory.value = null
|
||||||
|
categoryForm.name = ''
|
||||||
|
categoryForm.description = ''
|
||||||
|
categoryForm.color = '#409eff'
|
||||||
|
categoryForm.sort_order = 0
|
||||||
|
categoryForm.status = 1
|
||||||
|
categoryFormRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
refreshCategories()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.category-container {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-list {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-cards {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-text {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
909
pc/src/views/system/files/FileList.vue
Normal file
909
pc/src/views/system/files/FileList.vue
Normal file
@ -0,0 +1,909 @@
|
|||||||
|
<template>
|
||||||
|
<div class="file-list-container">
|
||||||
|
<!-- 搜索和筛选栏 -->
|
||||||
|
<div class="search-filter-bar">
|
||||||
|
<el-input
|
||||||
|
v-model="searchKeyword"
|
||||||
|
placeholder="搜索原始文件名"
|
||||||
|
clearable
|
||||||
|
@input="handleSearch"
|
||||||
|
size="large"
|
||||||
|
class="search-input"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
|
||||||
|
<el-select
|
||||||
|
v-model="filterCategory"
|
||||||
|
placeholder="全部分类"
|
||||||
|
clearable
|
||||||
|
@change="applyFilters"
|
||||||
|
size="large"
|
||||||
|
class="category-select"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category"
|
||||||
|
:label="category"
|
||||||
|
:value="category"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
@click="showUploadDialog = true"
|
||||||
|
>
|
||||||
|
<el-icon><UploadFilled /></el-icon>
|
||||||
|
快速上传
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-button
|
||||||
|
size="large"
|
||||||
|
@click="fetchFiles"
|
||||||
|
>
|
||||||
|
<el-icon><RefreshRight /></el-icon>
|
||||||
|
刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件表格 -->
|
||||||
|
<div class="file-table">
|
||||||
|
<el-table
|
||||||
|
:data="paginatedFiles"
|
||||||
|
v-loading="loading"
|
||||||
|
style="width: 100%"
|
||||||
|
stripe
|
||||||
|
:default-sort="{ prop: 'upload_time', order: 'descending' }"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" sortable />
|
||||||
|
<el-table-column label="文件名" min-width="250">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="file-name">
|
||||||
|
<el-icon class="icon-doc"><Document /></el-icon>
|
||||||
|
<span>{{ row.original_name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="file_type" label="类型" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag size="small">{{ row.file_type }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="file_size" label="大小" width="120" sortable>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatFileSize(row.file_size) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="category" label="分类" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag type="success" effect="plain">{{ row.category }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="upload_by" label="上传人" width="120" />
|
||||||
|
<el-table-column prop="upload_time" label="上传时间" width="180" sortable>
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatDate(row.upload_time) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="120" fixed="right" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tooltip :content="canPreviewFile(row.file_ext) ? '预览' : '下载'">
|
||||||
|
<el-button
|
||||||
|
:type="canPreviewFile(row.file_ext) ? 'success' : 'warning'"
|
||||||
|
circle
|
||||||
|
plain
|
||||||
|
size="small"
|
||||||
|
@click="handleFileAction(row)"
|
||||||
|
>
|
||||||
|
<el-icon>
|
||||||
|
<View v-if="canPreviewFile(row.file_ext)" />
|
||||||
|
<Download v-else />
|
||||||
|
</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="showPreviewDialog"
|
||||||
|
:width="isFullscreen ? '100%' : '80%'"
|
||||||
|
:fullscreen="isFullscreen"
|
||||||
|
:before-close="handlePreviewClose"
|
||||||
|
class="preview-dialog"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="preview-dialog-header">
|
||||||
|
<span class="preview-dialog-title">{{ previewFile?.original_name || '文件预览' }}</span>
|
||||||
|
<el-button
|
||||||
|
:icon="isFullscreen ? 'FullScreen' : 'FullScreen'"
|
||||||
|
circle
|
||||||
|
size="small"
|
||||||
|
@click="toggleFullscreen"
|
||||||
|
:title="isFullscreen ? '退出全屏' : '全屏'"
|
||||||
|
>
|
||||||
|
<el-icon v-if="isFullscreen"><DCaret /></el-icon>
|
||||||
|
<el-icon v-else><FullScreen /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-if="previewFile" class="preview-content">
|
||||||
|
<!-- 图片预览 -->
|
||||||
|
<div v-if="isImageFile(previewFile.file_ext)" class="preview-image-wrapper">
|
||||||
|
<el-image
|
||||||
|
:src="previewImageUrl"
|
||||||
|
fit="contain"
|
||||||
|
:preview-src-list="[previewImageUrl]"
|
||||||
|
:hide-on-click-modal="true"
|
||||||
|
:style="{ width: '100%', maxHeight: isFullscreen ? 'calc(100vh - 140px)' : '70vh' }"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- PDF预览 -->
|
||||||
|
<div v-else-if="isPdfFile(previewFile.file_ext)" :class="['preview-pdf-wrapper', { 'is-fullscreen': isFullscreen }]">
|
||||||
|
<VuePdfApp v-if="previewImageUrl" :pdf="previewImageUrl" />
|
||||||
|
</div>
|
||||||
|
<!-- Word文档预览 -->
|
||||||
|
<div v-else-if="isWordFile(previewFile.file_ext)" :class="['preview-word-wrapper', { 'is-fullscreen': isFullscreen }]">
|
||||||
|
<div ref="wordPreviewContainer" class="word-preview-container"></div>
|
||||||
|
</div>
|
||||||
|
<!-- 其他Office文档(Excel, PPT)暂不支持 -->
|
||||||
|
<div v-else-if="isOfficeFile(previewFile.file_ext)" class="preview-other">
|
||||||
|
<el-icon style="font-size: 64px; color: #909399"><Document /></el-icon>
|
||||||
|
<p>{{ getOfficeFileTypeName(previewFile.file_ext) }} 文件暂不支持在线预览</p>
|
||||||
|
<el-button type="primary" @click="downloadFile(previewFile)">
|
||||||
|
<el-icon><Download /></el-icon>
|
||||||
|
下载文件
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<!-- 其他文件显示提示 -->
|
||||||
|
<div v-else class="preview-other">
|
||||||
|
<el-icon style="font-size: 64px; color: #909399"><Document /></el-icon>
|
||||||
|
<p>该文件类型不支持在线预览</p>
|
||||||
|
<el-button type="primary" @click="downloadFile(previewFile)">
|
||||||
|
<el-icon><Download /></el-icon>
|
||||||
|
下载文件
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 快速上传对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="showUploadDialog"
|
||||||
|
title="快速上传"
|
||||||
|
width="500px"
|
||||||
|
:before-close="handleUploadClose"
|
||||||
|
>
|
||||||
|
<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"
|
||||||
|
multiple
|
||||||
|
>
|
||||||
|
<el-icon class="el-icon--upload" style="font-size: 44px; color: #36b2fa">
|
||||||
|
<UploadFilled />
|
||||||
|
</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">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
UploadFilled,
|
||||||
|
RefreshRight,
|
||||||
|
Document,
|
||||||
|
View,
|
||||||
|
Download,
|
||||||
|
Delete,
|
||||||
|
WarningFilled,
|
||||||
|
FullScreen,
|
||||||
|
DCaret,
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import {
|
||||||
|
getFilesByTenant,
|
||||||
|
deleteFile as deleteFileApi,
|
||||||
|
canPreview as canPreviewApi,
|
||||||
|
previewFile as previewFileApi,
|
||||||
|
downloadFile as downloadFileApi,
|
||||||
|
} from '@/api/file'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import { renderAsync } from 'docx-preview'
|
||||||
|
import VuePdfApp from 'vue3-pdf-app'
|
||||||
|
import 'vue3-pdf-app/dist/icons/main.css'
|
||||||
|
|
||||||
|
// Props & Emits
|
||||||
|
const emit = defineEmits(['refresh'])
|
||||||
|
|
||||||
|
// Store
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const allFiles = ref<any[]>([])
|
||||||
|
const categories = ref<string[]>([])
|
||||||
|
const filteredFiles = ref<any[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const searchKeyword = ref('')
|
||||||
|
const filterCategory = ref('')
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const totalFiles = ref(0)
|
||||||
|
const showUploadDialog = ref(false)
|
||||||
|
const showPreviewDialog = ref(false)
|
||||||
|
const previewFile = ref<any>(null)
|
||||||
|
const previewImageUrl = ref('')
|
||||||
|
const wordPreviewContainer = ref<HTMLElement | null>(null)
|
||||||
|
const isFullscreen = ref(false)
|
||||||
|
const uploadForm = ref({
|
||||||
|
category: '文档',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
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 paginatedFiles = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
|
const end = start + pageSize.value
|
||||||
|
return filteredFiles.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取当前租户ID
|
||||||
|
const getCurrentTenantId = () => {
|
||||||
|
if (authStore.user && authStore.user.tenant_id) {
|
||||||
|
return authStore.user.tenant_id
|
||||||
|
}
|
||||||
|
const userInfo = localStorage.getItem('userInfo')
|
||||||
|
if (userInfo) {
|
||||||
|
try {
|
||||||
|
const user = JSON.parse(userInfo)
|
||||||
|
return user.tenant_id || user.tenantId || 1
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse user info:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件列表
|
||||||
|
const fetchFiles = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const tenantId = getCurrentTenantId()
|
||||||
|
const res = await getFilesByTenant(tenantId)
|
||||||
|
|
||||||
|
if (res.success && res.data) {
|
||||||
|
const files = Array.isArray(res.data) ? res.data : []
|
||||||
|
allFiles.value = files.map((file: any) => ({
|
||||||
|
...file,
|
||||||
|
upload_time: file.upload_time || file.create_time,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 提取所有分类
|
||||||
|
const uniqueCategories = new Set<string>()
|
||||||
|
files.forEach((file: any) => {
|
||||||
|
if (file.category) {
|
||||||
|
uniqueCategories.add(file.category)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
categories.value = Array.from(uniqueCategories).sort()
|
||||||
|
|
||||||
|
applyFilters()
|
||||||
|
totalFiles.value = filteredFiles.value.length
|
||||||
|
emit('refresh')
|
||||||
|
} else {
|
||||||
|
allFiles.value = []
|
||||||
|
totalFiles.value = 0
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
ElMessage.error(err.message || '获取文件列表失败')
|
||||||
|
allFiles.value = []
|
||||||
|
totalFiles.value = 0
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用搜索和筛选
|
||||||
|
const applyFilters = () => {
|
||||||
|
let result = [...allFiles.value]
|
||||||
|
|
||||||
|
if (filterCategory.value) {
|
||||||
|
result = result.filter((file) => file.category === filterCategory.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchKeyword.value.trim()) {
|
||||||
|
const keyword = searchKeyword.value.toLowerCase()
|
||||||
|
result = result.filter(
|
||||||
|
(file) =>
|
||||||
|
file.original_name && file.original_name.toLowerCase().includes(keyword)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredFiles.value = result
|
||||||
|
totalFiles.value = result.length
|
||||||
|
|
||||||
|
if (currentPage.value > 1 && result.length === 0) {
|
||||||
|
currentPage.value = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = () => {
|
||||||
|
applyFilters()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
|
const handleSizeChange = (val: number) => {
|
||||||
|
pageSize.value = val
|
||||||
|
currentPage.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCurrentChange = (val: number) => {
|
||||||
|
currentPage.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传相关
|
||||||
|
const handleUploadClose = () => {
|
||||||
|
showUploadDialog.value = false
|
||||||
|
uploadForm.value.category = '文档'
|
||||||
|
}
|
||||||
|
|
||||||
|
const beforeUpload = (file: File) => {
|
||||||
|
const maxSize = 10 * 1024 * 1024
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
ElMessage.error('文件大小不能超过 10MB!')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUploadSuccess = (response: any, file: any) => {
|
||||||
|
if (response.success) {
|
||||||
|
ElMessage.success('文件上传成功!')
|
||||||
|
showUploadDialog.value = false
|
||||||
|
uploadForm.value.category = '文档'
|
||||||
|
fetchFiles()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(response.message || '文件上传失败!')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUploadError = (error: Error, file: any) => {
|
||||||
|
ElMessage.error('文件上传失败:' + (error.message || error))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件操作
|
||||||
|
const canPreviewFile = (fileExt: string) => {
|
||||||
|
return canPreviewApi(fileExt)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileAction = (file: any) => {
|
||||||
|
if (canPreviewFile(file.file_ext)) {
|
||||||
|
previewFileAction(file)
|
||||||
|
} else {
|
||||||
|
downloadFile(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为图片文件
|
||||||
|
const isImageFile = (fileExt: string) => {
|
||||||
|
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg']
|
||||||
|
return imageExts.includes(fileExt.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为PDF文件
|
||||||
|
const isPdfFile = (fileExt: string) => {
|
||||||
|
return fileExt.toLowerCase() === '.pdf'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为Office文档(Word, Excel, PPT)
|
||||||
|
const isOfficeFile = (fileExt: string) => {
|
||||||
|
const officeExts = ['.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx']
|
||||||
|
return officeExts.includes(fileExt.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为Word文档
|
||||||
|
const isWordFile = (fileExt: string) => {
|
||||||
|
const wordExts = ['.doc', '.docx']
|
||||||
|
return wordExts.includes(fileExt.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为Excel文档
|
||||||
|
const isExcelFile = (fileExt: string) => {
|
||||||
|
const excelExts = ['.xls', '.xlsx']
|
||||||
|
return excelExts.includes(fileExt.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为PPT文档
|
||||||
|
const isPptFile = (fileExt: string) => {
|
||||||
|
const pptExts = ['.ppt', '.pptx']
|
||||||
|
return pptExts.includes(fileExt.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取Office文件类型名称
|
||||||
|
const getOfficeFileTypeName = (fileExt: string) => {
|
||||||
|
if (isExcelFile(fileExt)) return 'Excel'
|
||||||
|
if (isPptFile(fileExt)) return 'PowerPoint'
|
||||||
|
return 'Office'
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewFileAction = async (file: any) => {
|
||||||
|
const baseUrl = import.meta.env.VITE_API_BASE_URL || window.location.origin
|
||||||
|
const previewUrl = `${baseUrl}${previewFileApi(file.id)}`
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
ElMessage.error('请先登录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 获取文件内容
|
||||||
|
const response = await fetch(previewUrl, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
// 尝试解析错误信息
|
||||||
|
let errorMessage = '预览失败'
|
||||||
|
const contentType = response.headers.get('content-type')
|
||||||
|
if (contentType && contentType.includes('application/json')) {
|
||||||
|
try {
|
||||||
|
const errorData = await response.json()
|
||||||
|
errorMessage = errorData.message || errorMessage
|
||||||
|
} catch {
|
||||||
|
// 如果不是JSON,使用默认错误信息
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ElMessage.error(errorMessage)
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await response.blob()
|
||||||
|
|
||||||
|
// Word 文档预览
|
||||||
|
if (isWordFile(file.file_ext)) {
|
||||||
|
previewFile.value = file
|
||||||
|
showPreviewDialog.value = true
|
||||||
|
|
||||||
|
// 等待 DOM 更新
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
if (wordPreviewContainer.value) {
|
||||||
|
// 清空容器
|
||||||
|
wordPreviewContainer.value.innerHTML = ''
|
||||||
|
|
||||||
|
// 使用 docx-preview 渲染
|
||||||
|
try {
|
||||||
|
await renderAsync(blob, wordPreviewContainer.value, undefined, {
|
||||||
|
className: 'docx-wrapper',
|
||||||
|
inWrapper: true,
|
||||||
|
ignoreWidth: false,
|
||||||
|
ignoreHeight: false,
|
||||||
|
renderHeaders: true,
|
||||||
|
renderFooters: true,
|
||||||
|
renderFootnotes: true,
|
||||||
|
renderEndnotes: true,
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Word 预览失败:', error)
|
||||||
|
ElMessage.error('Word 预览失败: ' + (error.message || error))
|
||||||
|
showPreviewDialog.value = false
|
||||||
|
previewFile.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// PDF 文档预览
|
||||||
|
else if (isPdfFile(file.file_ext)) {
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob)
|
||||||
|
previewFile.value = file
|
||||||
|
previewImageUrl.value = blobUrl
|
||||||
|
showPreviewDialog.value = true
|
||||||
|
}
|
||||||
|
// 图片预览
|
||||||
|
else if (isImageFile(file.file_ext)) {
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob)
|
||||||
|
previewFile.value = file
|
||||||
|
previewImageUrl.value = blobUrl
|
||||||
|
showPreviewDialog.value = true
|
||||||
|
}
|
||||||
|
// 其他Office文档(Excel, PPT)- 暂不支持在线预览
|
||||||
|
else if (isOfficeFile(file.file_ext)) {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`${getOfficeFileTypeName(file.file_ext)} 文件暂不支持在线预览,是否下载文件?`,
|
||||||
|
'提示',
|
||||||
|
{
|
||||||
|
confirmButtonText: '下载',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'info',
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
downloadFile(file)
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
// 其他可预览文件(视频、音频等),在新窗口打开
|
||||||
|
else {
|
||||||
|
const blobUrl = window.URL.createObjectURL(blob)
|
||||||
|
const previewWindow = window.open(blobUrl, '_blank')
|
||||||
|
|
||||||
|
if (!previewWindow) {
|
||||||
|
ElMessage.warning('无法打开预览窗口,请检查浏览器弹窗设置')
|
||||||
|
window.URL.revokeObjectURL(blobUrl)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听窗口关闭,清理blob URL
|
||||||
|
const checkWindow = setInterval(() => {
|
||||||
|
if (previewWindow.closed) {
|
||||||
|
window.URL.revokeObjectURL(blobUrl)
|
||||||
|
clearInterval(checkWindow)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('预览失败:', error)
|
||||||
|
ElMessage.error('预览失败: ' + (error.message || error))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePreviewClose = () => {
|
||||||
|
// 清理blob URL
|
||||||
|
if (previewImageUrl.value && previewImageUrl.value.startsWith('blob:')) {
|
||||||
|
window.URL.revokeObjectURL(previewImageUrl.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理 Word 预览容器
|
||||||
|
if (wordPreviewContainer.value) {
|
||||||
|
wordPreviewContainer.value.innerHTML = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
showPreviewDialog.value = false
|
||||||
|
previewFile.value = null
|
||||||
|
previewImageUrl.value = ''
|
||||||
|
isFullscreen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换全屏模式
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
isFullscreen.value = !isFullscreen.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadFile = (file: any) => {
|
||||||
|
const baseUrl = import.meta.env.VITE_API_BASE_URL || window.location.origin
|
||||||
|
const downloadUrl = `${baseUrl}${downloadFileApi(file.id)}`
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = downloadUrl
|
||||||
|
if (token) {
|
||||||
|
fetch(downloadUrl, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => response.blob())
|
||||||
|
.then((blob) => {
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
link.href = url
|
||||||
|
link.download = file.original_name || file.file_name
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
ElMessage.success('文件下载开始')
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
ElMessage.error('文件下载失败:' + err.message)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
link.download = file.original_name || file.file_name
|
||||||
|
document.body.appendChild(link)
|
||||||
|
link.click()
|
||||||
|
document.body.removeChild(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteFile = async (file: any) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除文件「${file.original_name || file.file_name}」吗?删除后不可恢复。`,
|
||||||
|
'警告',
|
||||||
|
{
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
const res = await deleteFileApi(file.id)
|
||||||
|
if (res.success) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
await fetchFiles()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.message || '删除失败')
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err !== 'cancel') {
|
||||||
|
ElMessage.error(err.message || '删除失败')
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化函数
|
||||||
|
const formatFileSize = (bytes: number | string | undefined | null): string => {
|
||||||
|
if (!bytes && bytes !== 0) return '0 B'
|
||||||
|
const bytesValue = typeof bytes === 'string' ? parseFloat(bytes) : Number(bytes)
|
||||||
|
if (isNaN(bytesValue) || bytesValue < 0) return '0 B'
|
||||||
|
if (bytesValue === 0) return '0 B'
|
||||||
|
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytesValue) / Math.log(k))
|
||||||
|
return parseFloat((bytesValue / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string | number | undefined | null): string => {
|
||||||
|
if (!dateStr) return '-'
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
if (isNaN(date.getTime())) return String(dateStr)
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生命周期
|
||||||
|
onMounted(() => {
|
||||||
|
fetchFiles()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
fetchFiles,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.file-list-container {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filter-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-select {
|
||||||
|
width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-table {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-doc {
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-form {
|
||||||
|
margin-top: 20px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-dialog-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-dialog-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-dialog :deep(.el-dialog__body) {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-dialog.is-fullscreen :deep(.el-dialog__body) {
|
||||||
|
height: calc(100vh - 120px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-pdf-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 70vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-fullscreen .preview-pdf-wrapper {
|
||||||
|
height: calc(100vh - 140px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-word-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 70vh;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-fullscreen .preview-word-wrapper {
|
||||||
|
height: calc(100vh - 140px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-preview-container {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 70vh;
|
||||||
|
padding: 20px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-preview-container :deep(.docx-wrapper) {
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.word-preview-container :deep(.docx) {
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
margin: 0 auto;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-other {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-other p {
|
||||||
|
margin: 0;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-upload-dragger) {
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-button--small span,.el-button span){
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
539
pc/src/views/system/files/FileUpload.vue
Normal file
539
pc/src/views/system/files/FileUpload.vue
Normal file
@ -0,0 +1,539 @@
|
|||||||
|
<template>
|
||||||
|
<div class="upload-container">
|
||||||
|
<!-- 上传区域 -->
|
||||||
|
<div class="upload-area">
|
||||||
|
<el-upload
|
||||||
|
ref="uploadRef"
|
||||||
|
class="upload-demo"
|
||||||
|
drag
|
||||||
|
:action="uploadUrl"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
:data="{ category: uploadForm.category }"
|
||||||
|
:on-success="handleUploadSuccess"
|
||||||
|
:on-error="handleUploadError"
|
||||||
|
:on-progress="handleUploadProgress"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:on-change="handleFileChange"
|
||||||
|
:on-remove="handleFileRemove"
|
||||||
|
multiple
|
||||||
|
:file-list="fileList"
|
||||||
|
:auto-upload="autoUpload"
|
||||||
|
>
|
||||||
|
<el-icon class="el-icon--upload" style="font-size: 64px; color: #409eff">
|
||||||
|
<UploadFilled />
|
||||||
|
</el-icon>
|
||||||
|
<div class="el-upload__text">
|
||||||
|
将文件拖到此处,或<em>点击选择文件</em>
|
||||||
|
</div>
|
||||||
|
<template #tip>
|
||||||
|
<div class="el-upload__tip">
|
||||||
|
支持批量上传,单个文件大小不超过
|
||||||
|
<span style="color: #f56c6c; font-weight: 600">10MB</span>
|
||||||
|
,支持拖拽排序
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上传设置 -->
|
||||||
|
<div class="upload-settings">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>上传设置</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<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-option label="其他" value="其他" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="自动上传">
|
||||||
|
<el-switch v-model="autoUpload" />
|
||||||
|
<span class="tip-text">开启后选择文件即自动上传</span>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件列表预览 -->
|
||||||
|
<div class="file-preview" v-if="fileList.length > 0">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>待上传文件列表({{ fileList.length }}个文件)</span>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
@click="clearFiles"
|
||||||
|
:disabled="uploading"
|
||||||
|
>
|
||||||
|
清空列表
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-table :data="fileList" style="width: 100%" max-height="400">
|
||||||
|
<el-table-column type="index" label="#" width="50" />
|
||||||
|
<el-table-column prop="name" label="文件名" min-width="200" show-overflow-tooltip />
|
||||||
|
<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 size="small">{{ getFileType(row.name) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="状态" width="120">
|
||||||
|
<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" fixed="right">
|
||||||
|
<template #default="{ row, $index }">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
@click="removeFile($index)"
|
||||||
|
:disabled="row.status === 'uploading' || uploading"
|
||||||
|
>
|
||||||
|
移除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<div class="upload-actions" v-if="!autoUpload">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="submitUpload"
|
||||||
|
:loading="uploading"
|
||||||
|
:disabled="fileList.length === 0"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<el-icon><Upload /></el-icon>
|
||||||
|
{{ uploading ? '上传中...' : '开始上传' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上传进度 -->
|
||||||
|
<div class="upload-progress" v-if="uploading || uploadedCount > 0">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<span>上传进度</span>
|
||||||
|
</template>
|
||||||
|
<el-progress
|
||||||
|
:percentage="uploadProgress"
|
||||||
|
:status="uploadStatus"
|
||||||
|
:stroke-width="20"
|
||||||
|
/>
|
||||||
|
<div class="progress-info">
|
||||||
|
<span>已上传: {{ uploadedCount }}/{{ totalCount }}</span>
|
||||||
|
<span>成功: {{ successCount }} 个</span>
|
||||||
|
<span>失败: {{ failedCount }} 个</span>
|
||||||
|
<span>成功率: {{ successRate }}%</span>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上传提示 -->
|
||||||
|
<div class="upload-tips">
|
||||||
|
<el-alert
|
||||||
|
title="上传提示"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
<li>支持批量拖拽上传文件</li>
|
||||||
|
<li>单个文件大小不超过 10MB</li>
|
||||||
|
<li>支持的文件类型:图片、文档、视频、音频等</li>
|
||||||
|
<li>建议先选择分类再上传文件</li>
|
||||||
|
<li>上传大文件时请保持网络稳定</li>
|
||||||
|
</ul>
|
||||||
|
</el-alert>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { ElMessage, type UploadInstance, type UploadFile } from 'element-plus'
|
||||||
|
import { UploadFilled, Upload } from '@element-plus/icons-vue'
|
||||||
|
import { uploadFile } from '@/api/file'
|
||||||
|
|
||||||
|
// Props & Emits
|
||||||
|
const emit = defineEmits(['upload-success'])
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
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 failedCount = ref(0)
|
||||||
|
const totalCount = ref(0)
|
||||||
|
const autoUpload = ref(false)
|
||||||
|
|
||||||
|
// 上传表单
|
||||||
|
const uploadForm = ref({
|
||||||
|
category: '文档',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
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 && failedCount.value === 0) return 'success'
|
||||||
|
if (failedCount.value > 0 && uploadedCount.value === totalCount.value) return 'exception'
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const successRate = computed(() => {
|
||||||
|
if (totalCount.value === 0) return 0
|
||||||
|
return Math.round((successCount.value / totalCount.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) {
|
||||||
|
ElMessage.warning(`文件 "${file.name}" 已存在`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileChange = (file: UploadFile, files: UploadFile[]) => {
|
||||||
|
fileList.value = files
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileRemove = (file: UploadFile, files: UploadFile[]) => {
|
||||||
|
fileList.value = files
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFile = (index: number) => {
|
||||||
|
fileList.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearFiles = () => {
|
||||||
|
fileList.value = []
|
||||||
|
uploadProgress.value = 0
|
||||||
|
uploadedCount.value = 0
|
||||||
|
successCount.value = 0
|
||||||
|
failedCount.value = 0
|
||||||
|
totalCount.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
|
||||||
|
failedCount.value = 0
|
||||||
|
totalCount.value = fileList.value.length
|
||||||
|
|
||||||
|
// 逐个上传文件,调用uploadFile API
|
||||||
|
for (let i = 0; i < fileList.value.length; i++) {
|
||||||
|
const fileItem = fileList.value[i]
|
||||||
|
const file = fileItem.raw as File
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
uploadedCount.value++
|
||||||
|
failedCount.value++
|
||||||
|
fileItem.status = 'fail'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fileItem.status = 'uploading'
|
||||||
|
|
||||||
|
// 创建FormData
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
formData.append('category', uploadForm.value.category)
|
||||||
|
|
||||||
|
// 调用uploadFile API
|
||||||
|
const response = await uploadFile(formData, {
|
||||||
|
category: uploadForm.value.category,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
fileItem.status = 'success'
|
||||||
|
successCount.value++
|
||||||
|
ElMessage.success(`文件 "${file.name}" 上传成功`)
|
||||||
|
} else {
|
||||||
|
fileItem.status = 'fail'
|
||||||
|
failedCount.value++
|
||||||
|
ElMessage.error(response.message || `文件 "${file.name}" 上传失败`)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
fileItem.status = 'fail'
|
||||||
|
failedCount.value++
|
||||||
|
console.error(`文件 "${file.name}" 上传失败:`, error)
|
||||||
|
ElMessage.error(`文件 "${file.name}" 上传失败: ${error.message || error}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadedCount.value++
|
||||||
|
uploadProgress.value = Math.round((uploadedCount.value / totalCount.value) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有文件上传完成
|
||||||
|
uploading.value = false
|
||||||
|
|
||||||
|
if (successCount.value === totalCount.value) {
|
||||||
|
ElMessage.success('所有文件上传完成')
|
||||||
|
emit('upload-success')
|
||||||
|
setTimeout(() => {
|
||||||
|
clearFiles()
|
||||||
|
}, 2000)
|
||||||
|
} else if (successCount.value > 0) {
|
||||||
|
ElMessage.warning(
|
||||||
|
`上传完成,成功 ${successCount.value}/${totalCount.value} 个文件`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ElMessage.error('所有文件上传失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量上传失败:', error)
|
||||||
|
ElMessage.error('批量上传失败')
|
||||||
|
uploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUploadProgress = (event: any, file: UploadFile, fileList: UploadFile[]) => {
|
||||||
|
// Element Plus upload组件的进度回调(用于自动上传模式)
|
||||||
|
if (autoUpload.value) {
|
||||||
|
// 自动上传模式下,进度由组件自己管理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUploadSuccess = (response: any, file: UploadFile) => {
|
||||||
|
uploadedCount.value++
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
file.status = 'success'
|
||||||
|
successCount.value++
|
||||||
|
ElMessage.success(`文件 "${file.name}" 上传成功`)
|
||||||
|
} else {
|
||||||
|
file.status = 'fail'
|
||||||
|
failedCount.value++
|
||||||
|
ElMessage.error(response.message || `文件 "${file.name}" 上传失败`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新进度
|
||||||
|
uploadProgress.value = Math.round((uploadedCount.value / totalCount.value) * 100)
|
||||||
|
|
||||||
|
// 所有文件上传完成
|
||||||
|
if (uploadedCount.value === totalCount.value) {
|
||||||
|
uploading.value = false
|
||||||
|
|
||||||
|
if (successCount.value === totalCount.value) {
|
||||||
|
ElMessage.success('所有文件上传完成')
|
||||||
|
emit('upload-success')
|
||||||
|
// 延迟清空列表
|
||||||
|
setTimeout(() => {
|
||||||
|
clearFiles()
|
||||||
|
}, 2000)
|
||||||
|
} else if (successCount.value > 0) {
|
||||||
|
ElMessage.warning(
|
||||||
|
`上传完成,成功 ${successCount.value}/${totalCount.value} 个文件`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ElMessage.error('所有文件上传失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUploadError = (error: any, file: UploadFile) => {
|
||||||
|
uploadedCount.value++
|
||||||
|
failedCount.value++
|
||||||
|
file.status = 'fail'
|
||||||
|
|
||||||
|
console.error('上传失败:', error)
|
||||||
|
ElMessage.error(`文件 "${file.name}" 上传失败`)
|
||||||
|
|
||||||
|
// 更新进度
|
||||||
|
uploadProgress.value = Math.round((uploadedCount.value / totalCount.value) * 100)
|
||||||
|
|
||||||
|
// 所有文件处理完成
|
||||||
|
if (uploadedCount.value === totalCount.value) {
|
||||||
|
uploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 getFileType = (fileName: string) => {
|
||||||
|
const ext = fileName.substring(fileName.lastIndexOf('.')).toLowerCase()
|
||||||
|
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
||||||
|
const docExts = ['.doc', '.docx', '.pdf', '.txt', '.xls', '.xlsx']
|
||||||
|
const videoExts = ['.mp4', '.avi', '.mov', '.wmv']
|
||||||
|
const audioExts = ['.mp3', '.wav', '.flac']
|
||||||
|
|
||||||
|
if (imageExts.includes(ext)) return '图片'
|
||||||
|
if (docExts.includes(ext)) return '文档'
|
||||||
|
if (videoExts.includes(ext)) return '视频'
|
||||||
|
if (audioExts.includes(ext)) return '音频'
|
||||||
|
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: #f5f7fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-area {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-upload-dragger) {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-settings {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-actions {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-progress {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
margin-top: 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-info span {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tips {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tips ul {
|
||||||
|
margin: 10px 0 0 20px;
|
||||||
|
padding: 0;
|
||||||
|
list-style: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-tips li {
|
||||||
|
margin: 6px 0;
|
||||||
|
color: #606266;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-text {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-upload__text em) {
|
||||||
|
color: #409eff;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@ -1,872 +1,130 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-card class="box-card">
|
<el-card class="files-management">
|
||||||
<div class="header-bar">
|
<template #header>
|
||||||
<h2>文件管理</h2>
|
<div class="card-header">
|
||||||
</div>
|
<h2>
|
||||||
|
<el-icon><Folder /></el-icon>
|
||||||
<div v-if="loading" class="loading-state">
|
文件管理系统
|
||||||
<div class="loading-spinner"></div>
|
</h2>
|
||||||
<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>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 文件数据列表 -->
|
|
||||||
<div class="file-list stylish-panel">
|
|
||||||
<el-table
|
|
||||||
:data="paginatedFiles"
|
|
||||||
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>
|
</div>
|
||||||
</template>
|
</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-tabs v-model="activeTab" class="file-tabs" @tab-change="handleTabChange">
|
||||||
<el-pagination
|
<!-- 文件列表 -->
|
||||||
v-model:current-page="currentPage"
|
<el-tab-pane label="文件列表" name="list">
|
||||||
v-model:page-size="pageSize"
|
<template #label>
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
<span class="custom-tabs-label">
|
||||||
:total="totalFiles"
|
<el-icon><List /></el-icon>
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
<span>文件列表</span>
|
||||||
@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>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
<FileList @refresh="handleRefresh" />
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
<!-- 文件详情对话框 -->
|
<!-- 批量上传 -->
|
||||||
<el-dialog
|
<el-tab-pane label="批量上传" name="upload">
|
||||||
v-model="showFileDetail"
|
<template #label>
|
||||||
:title="currentFile ? currentFile.original_name : '文件详情'"
|
<span class="custom-tabs-label">
|
||||||
width="650px"
|
<el-icon><Upload /></el-icon>
|
||||||
class="file-detail-dialog"
|
<span>批量上传</span>
|
||||||
>
|
|
||||||
<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>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
<FileUpload @upload-success="handleUploadSuccess" />
|
||||||
</div>
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 分类管理 -->
|
||||||
|
<el-tab-pane label="分类管理" name="category">
|
||||||
|
<template #label>
|
||||||
|
<span class="custom-tabs-label">
|
||||||
|
<el-icon><Grid /></el-icon>
|
||||||
|
<span>分类管理</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<FileCategory @category-change="handleCategoryChange" />
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from "vue";
|
import { ref } from 'vue'
|
||||||
import { useRouter } from "vue-router";
|
import { Folder, List, Upload, Grid } from '@element-plus/icons-vue'
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage } from 'element-plus'
|
||||||
import {
|
import FileList from './FileList.vue'
|
||||||
Download,
|
import FileUpload from './FileUpload.vue'
|
||||||
Delete,
|
import FileCategory from './FileCategory.vue'
|
||||||
Search,
|
|
||||||
Document,
|
|
||||||
View,
|
|
||||||
Link,
|
|
||||||
UploadFilled,
|
|
||||||
} from "@element-plus/icons-vue";
|
|
||||||
import { getFilesByTenant, getFileById, deleteFile as deleteFileApi, searchFiles } from "@/api/file";
|
|
||||||
import { useAuthStore } from "@/stores/auth";
|
|
||||||
|
|
||||||
const router = useRouter();
|
// 当前激活的标签页
|
||||||
const authStore = useAuthStore();
|
const activeTab = ref<string>('list')
|
||||||
|
|
||||||
// 文件列表数据
|
// 标签页切换处理
|
||||||
const allFiles = ref<any[]>([]);
|
const handleTabChange = (tabName: string | number) => {
|
||||||
const categories = ref<string[]>([]);
|
console.log('切换到标签页:', tabName)
|
||||||
const filteredFiles = ref<any[]>([]);
|
|
||||||
const loading = ref(false);
|
|
||||||
const error = ref("");
|
|
||||||
const searchKeyword = ref("");
|
|
||||||
const filterCategory = ref("");
|
|
||||||
const pageSize = ref(10);
|
|
||||||
const currentPage = ref(1);
|
|
||||||
const totalFiles = ref(0);
|
|
||||||
const showUploadDialog = ref(false);
|
|
||||||
|
|
||||||
// 获取当前租户ID
|
|
||||||
const getCurrentTenantId = () => {
|
|
||||||
if (authStore.user && authStore.user.tenant_id) {
|
|
||||||
return authStore.user.tenant_id;
|
|
||||||
}
|
|
||||||
const userInfo = localStorage.getItem('userInfo');
|
|
||||||
if (userInfo) {
|
|
||||||
try {
|
|
||||||
const user = JSON.parse(userInfo);
|
|
||||||
return user.tenant_id || user.tenantId || 0;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse user info:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取文件列表
|
|
||||||
async function fetchFiles() {
|
|
||||||
loading.value = true;
|
|
||||||
error.value = "";
|
|
||||||
try {
|
|
||||||
const tenantId = getCurrentTenantId();
|
|
||||||
const res = await getFilesByTenant(tenantId);
|
|
||||||
|
|
||||||
if (res.success && res.data) {
|
|
||||||
const files = Array.isArray(res.data) ? res.data : [];
|
|
||||||
allFiles.value = files.map((file: any) => ({
|
|
||||||
...file,
|
|
||||||
upload_time: file.upload_time || file.create_time,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// 提取所有分类
|
|
||||||
const uniqueCategories = new Set<string>();
|
|
||||||
files.forEach((file: any) => {
|
|
||||||
if (file.category) {
|
|
||||||
uniqueCategories.add(file.category);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
categories.value = Array.from(uniqueCategories).sort();
|
|
||||||
|
|
||||||
applyFilters();
|
|
||||||
totalFiles.value = filteredFiles.value.length;
|
|
||||||
} else {
|
|
||||||
error.value = res.message || "获取文件列表失败";
|
|
||||||
allFiles.value = [];
|
|
||||||
totalFiles.value = 0;
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
error.value = err.message || "获取文件列表失败";
|
|
||||||
allFiles.value = [];
|
|
||||||
totalFiles.value = 0;
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 应用搜索和筛选
|
// 刷新文件列表
|
||||||
function applyFilters() {
|
const handleRefresh = () => {
|
||||||
let result = [...allFiles.value];
|
ElMessage.success('文件列表已刷新')
|
||||||
|
|
||||||
// 分类筛选
|
|
||||||
if (filterCategory.value) {
|
|
||||||
result = result.filter((file) => file.category === filterCategory.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关键词搜索
|
// 上传成功后的处理
|
||||||
if (searchKeyword.value.trim()) {
|
const handleUploadSuccess = () => {
|
||||||
const keyword = searchKeyword.value.toLowerCase();
|
// 切换回文件列表标签页并刷新
|
||||||
result = result.filter(
|
activeTab.value = 'list'
|
||||||
(file) =>
|
ElMessage.success('文件上传成功,已返回文件列表')
|
||||||
(file.file_name && file.file_name.toLowerCase().includes(keyword)) ||
|
|
||||||
(file.original_name && file.original_name.toLowerCase().includes(keyword)) ||
|
|
||||||
(file.category && file.category.toLowerCase().includes(keyword))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredFiles.value = result;
|
// 分类变更处理
|
||||||
totalFiles.value = result.length;
|
const handleCategoryChange = () => {
|
||||||
|
ElMessage.success('分类已更新')
|
||||||
// 重置到第一页
|
|
||||||
if (currentPage.value > 1 && result.length === 0) {
|
|
||||||
currentPage.value = 1;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化文件大小
|
|
||||||
const formatFileSize = (bytes: number | string | undefined | null): string => {
|
|
||||||
if (!bytes && bytes !== 0) return "0 B";
|
|
||||||
const bytesValue = typeof bytes === 'string' ? parseFloat(bytes) : Number(bytes);
|
|
||||||
if (isNaN(bytesValue) || bytesValue < 0) return "0 B";
|
|
||||||
if (bytesValue === 0) return "0 B";
|
|
||||||
|
|
||||||
const k = 1024;
|
|
||||||
const sizes = ["B", "KB", "MB", "GB"];
|
|
||||||
const i = Math.floor(Math.log(bytesValue) / Math.log(k));
|
|
||||||
return parseFloat((bytesValue / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
||||||
};
|
|
||||||
|
|
||||||
// 格式化日期
|
|
||||||
const formatDate = (dateStr: string | number | undefined | null): string => {
|
|
||||||
if (!dateStr) return "-";
|
|
||||||
const date = new Date(dateStr);
|
|
||||||
if (isNaN(date.getTime())) return String(dateStr);
|
|
||||||
const year = date.getFullYear();
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
||||||
const day = String(date.getDate()).padStart(2, "0");
|
|
||||||
const hours = String(date.getHours()).padStart(2, "0");
|
|
||||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
||||||
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
||||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 上传配置
|
|
||||||
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 = () => {
|
|
||||||
applyFilters();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 筛选处理
|
|
||||||
const handleFilter = () => {
|
|
||||||
fetchFiles();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 分页大小变化
|
|
||||||
const handleSizeChange = (val: number) => {
|
|
||||||
pageSize.value = val;
|
|
||||||
currentPage.value = 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 当前页变化
|
|
||||||
const handleCurrentChange = (val: number) => {
|
|
||||||
currentPage.value = val;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 上传对话框关闭
|
|
||||||
const handleUploadClose = () => {
|
|
||||||
showUploadDialog.value = false;
|
|
||||||
uploadForm.value.category = "";
|
|
||||||
uploadForm.value.isPublic = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 上传成功
|
|
||||||
const handleUploadSuccess = (response: any, file: any) => {
|
|
||||||
if (response.success) {
|
|
||||||
ElMessage.success('文件上传成功!');
|
|
||||||
showUploadDialog.value = false;
|
|
||||||
uploadForm.value.category = "";
|
|
||||||
uploadForm.value.isPublic = false;
|
|
||||||
fetchFiles();
|
|
||||||
} else {
|
|
||||||
ElMessage.error(response.message || '文件上传失败!');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 上传失败
|
|
||||||
const handleUploadError = (error: Error, file: any) => {
|
|
||||||
ElMessage.error('文件上传失败:' + (error.message || error));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 上传前校验
|
|
||||||
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 组件会自动处理上传
|
|
||||||
// 不需要额外处理
|
|
||||||
};
|
|
||||||
|
|
||||||
// 查看文件详情
|
|
||||||
const viewFile = async (row: any) => {
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
const res = await getFileById(row.id);
|
|
||||||
if (res.success && res.data) {
|
|
||||||
currentFile.value = res.data;
|
|
||||||
showFileDetail.value = true;
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.message || "获取文件详情失败");
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
ElMessage.error(err.message || "获取文件详情失败");
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 复制文件链接
|
|
||||||
const copyFileUrl = (file: any) => {
|
|
||||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || window.location.origin;
|
|
||||||
const fileUrl = `${baseUrl}/api/files/${file.id}/download`;
|
|
||||||
|
|
||||||
if (navigator.clipboard) {
|
|
||||||
navigator.clipboard.writeText(fileUrl).then(() => {
|
|
||||||
ElMessage.success('文件链接已复制到剪贴板');
|
|
||||||
}).catch(() => {
|
|
||||||
ElMessage.error('复制失败,请手动复制');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 降级方案
|
|
||||||
const textarea = document.createElement('textarea');
|
|
||||||
textarea.value = fileUrl;
|
|
||||||
document.body.appendChild(textarea);
|
|
||||||
textarea.select();
|
|
||||||
try {
|
|
||||||
document.execCommand('copy');
|
|
||||||
ElMessage.success('文件链接已复制到剪贴板');
|
|
||||||
} catch {
|
|
||||||
ElMessage.error('复制失败,请手动复制');
|
|
||||||
}
|
|
||||||
document.body.removeChild(textarea);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 下载文件
|
|
||||||
const downloadFile = (file: any) => {
|
|
||||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || window.location.origin;
|
|
||||||
const downloadUrl = `${baseUrl}/api/files/${file.id}/download`;
|
|
||||||
|
|
||||||
const token = localStorage.getItem('token');
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = downloadUrl;
|
|
||||||
if (token) {
|
|
||||||
// 如果需要带token下载,可以使用fetch然后创建blob
|
|
||||||
fetch(downloadUrl, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(response => response.blob())
|
|
||||||
.then(blob => {
|
|
||||||
const url = window.URL.createObjectURL(blob);
|
|
||||||
link.href = url;
|
|
||||||
link.download = file.original_name || file.file_name;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
window.URL.revokeObjectURL(url);
|
|
||||||
ElMessage.success('文件下载开始');
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
ElMessage.error('文件下载失败:' + err.message);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
link.download = file.original_name || file.file_name;
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除文件
|
|
||||||
const deleteFile = async (file: any) => {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm(
|
|
||||||
`确定要删除文件「${file.original_name || file.file_name}」吗?删除后不可恢复。`,
|
|
||||||
'警告',
|
|
||||||
{
|
|
||||||
type: 'warning',
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
const res = await deleteFileApi(file.id);
|
|
||||||
if (res.success) {
|
|
||||||
ElMessage.success('删除成功');
|
|
||||||
await fetchFiles();
|
|
||||||
} else {
|
|
||||||
ElMessage.error(res.message || '删除失败');
|
|
||||||
}
|
|
||||||
} catch (err: any) {
|
|
||||||
if (err !== 'cancel') {
|
|
||||||
ElMessage.error(err.message || '删除失败');
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// 分页计算
|
|
||||||
const paginatedFiles = computed(() => {
|
|
||||||
const start = (currentPage.value - 1) * pageSize.value;
|
|
||||||
const end = start + pageSize.value;
|
|
||||||
return filteredFiles.value.slice(start, end);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 更新表格数据源为分页后的数据
|
|
||||||
watch([filteredFiles, currentPage, pageSize], () => {
|
|
||||||
// filteredFiles 已经在 computed 中处理,但表格需要直接使用 paginatedFiles
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
fetchFiles();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.fancy-bg {
|
.files-management {
|
||||||
padding: 28px 14px 24px 14px;
|
min-height: calc(100vh - 120px);
|
||||||
}
|
}
|
||||||
.stylish-panel {
|
|
||||||
background: #fff;
|
.card-header {
|
||||||
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 26px 16px;
|
justify-content: space-between;
|
||||||
min-height: 95px;
|
|
||||||
}
|
}
|
||||||
.card-icon {
|
|
||||||
font-size: 34px;
|
.card-header h2 {
|
||||||
width: 54px;
|
margin: 0;
|
||||||
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;
|
font-size: 20px;
|
||||||
color: #409eff;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
.recent-title {
|
|
||||||
font-size: 17px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #293a46;
|
color: #303133;
|
||||||
}
|
|
||||||
.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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
.icon-doc {
|
|
||||||
margin-right: 8px;
|
.file-tabs {
|
||||||
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;
|
margin-top: 20px;
|
||||||
border-top: 1px solid #eee;
|
|
||||||
padding-top: 20px;
|
|
||||||
}
|
}
|
||||||
.file-detail-dialog .el-dialog__body {
|
|
||||||
padding-top: 18px;
|
.custom-tabs-label {
|
||||||
background: #fafcff;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
.file-detail {
|
|
||||||
max-height: 380px;
|
:deep(.el-tabs__header) {
|
||||||
overflow-y: auto;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
.detail-primary {
|
|
||||||
color: #416cf7;
|
:deep(.el-tabs__item) {
|
||||||
|
font-size: 15px;
|
||||||
|
padding: 0 20px;
|
||||||
|
height: 45px;
|
||||||
|
line-height: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__item.is-active) {
|
||||||
|
color: #409eff;
|
||||||
font-weight: 600;
|
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>
|
</style>
|
||||||
|
|||||||
@ -145,8 +145,8 @@
|
|||||||
<el-radio-button :value="3">权限按钮</el-radio-button>
|
<el-radio-button :value="3">权限按钮</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary);">
|
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary);">
|
||||||
<div>• 页面菜单:有路由和组件地址,可访问完整功能页面</div>
|
<div>• 页面菜单:有路由和组件地址,用于<span style="color: var(--el-color-primary);">页面管理</span></div>
|
||||||
<div>• 目录菜单:只有路由地址,用于组织结构,无实际页面</div>
|
<div>• 目录菜单:只有路由地址,用于<span style="color: var(--el-color-primary);">接口管理</span>和<span style="color: var(--el-color-primary);">目录管理</span></div>
|
||||||
<div>• 权限按钮:无路由和组件,仅用于权限控制</div>
|
<div>• 权限按钮:无路由和组件,仅用于权限控制</div>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|||||||
@ -128,7 +128,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="角色" v-if="dialogTitle !== '修改密码'">
|
<el-form-item label="角色" v-if="dialogTitle !== '修改密码'">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="form.roleId"
|
v-model="form.role"
|
||||||
placeholder="请选择角色"
|
placeholder="请选择角色"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
:loading="loadingRoles"
|
:loading="loadingRoles"
|
||||||
@ -167,6 +167,7 @@ import { ElMessage, ElMessageBox } from "element-plus";
|
|||||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||||
import {
|
import {
|
||||||
getAllUsers,
|
getAllUsers,
|
||||||
|
getTenantUsers,
|
||||||
addUser,
|
addUser,
|
||||||
editUser,
|
editUser,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
@ -269,8 +270,10 @@ const validateConfirmPassword = (password: string, confirmPassword: string) => {
|
|||||||
|
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
// 传tenantid给接口
|
||||||
|
let tenantId = getCurrentTenantId ? getCurrentTenantId() : null;
|
||||||
try {
|
try {
|
||||||
const res = await getAllUsers();
|
const res = await getTenantUsers(tenantId);
|
||||||
// 兼容接口返回的数据结构
|
// 兼容接口返回的数据结构
|
||||||
let userList: any[] = [];
|
let userList: any[] = [];
|
||||||
if (Array.isArray(res)) {
|
if (Array.isArray(res)) {
|
||||||
@ -286,14 +289,12 @@ const fetchUsers = async () => {
|
|||||||
users.value = userList.map((item: any) => {
|
users.value = userList.map((item: any) => {
|
||||||
// 查找角色名称
|
// 查找角色名称
|
||||||
let roleName = '';
|
let roleName = '';
|
||||||
let roleId = item.role_id || item.roleId || null;
|
let roleValue = item.role || null; // role 字段存储的是角色ID(数字)
|
||||||
if (roleId) {
|
|
||||||
const role = roleList.value.find(r => r.roleId === roleId);
|
if (roleValue) {
|
||||||
roleName = role ? role.roleName : '';
|
// 通过角色ID查找角色信息
|
||||||
} else if (item.role) {
|
const roleInfo = roleList.value.find(r => r.roleId === roleValue);
|
||||||
// 兼容旧的role字段,尝试从roleList中查找
|
roleName = roleInfo ? roleInfo.roleName : '';
|
||||||
const role = roleList.value.find(r => r.roleCode === item.role);
|
|
||||||
roleName = role ? role.roleName : (item.role === 'admin' ? '管理员' : '普通用户');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -301,8 +302,7 @@ const fetchUsers = async () => {
|
|||||||
username: item.username,
|
username: item.username,
|
||||||
nickname: item.nickname,
|
nickname: item.nickname,
|
||||||
email: item.email,
|
email: item.email,
|
||||||
role: item.role || '',
|
role: roleValue, // role 字段存储角色ID
|
||||||
roleId: roleId,
|
|
||||||
roleName: roleName,
|
roleName: roleName,
|
||||||
status: item.status || "active",
|
status: item.status || "active",
|
||||||
lastLoginTime: item.lastLoginTime
|
lastLoginTime: item.lastLoginTime
|
||||||
@ -343,13 +343,12 @@ const dialogVisible = ref(false);
|
|||||||
const dialogTitle = ref("");
|
const dialogTitle = ref("");
|
||||||
const isEdit = ref(false);
|
const isEdit = ref(false);
|
||||||
const form = ref<any>({
|
const form = ref<any>({
|
||||||
id: 0,
|
id: null,
|
||||||
username: "",
|
username: "",
|
||||||
nickname: "",
|
nickname: "",
|
||||||
password: "",
|
password: "",
|
||||||
email: "",
|
email: "",
|
||||||
roleId: null,
|
role: null, // role 字段存储角色ID
|
||||||
role: "",
|
|
||||||
status: "active",
|
status: "active",
|
||||||
tenant_id: null,
|
tenant_id: null,
|
||||||
});
|
});
|
||||||
@ -397,13 +396,12 @@ const handleAddUser = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
form.value = {
|
form.value = {
|
||||||
id: 0,
|
id: 0, // 添加用户时ID为0
|
||||||
username: "",
|
username: "",
|
||||||
nickname: "",
|
nickname: "",
|
||||||
password: "",
|
password: "",
|
||||||
email: "",
|
email: "",
|
||||||
roleId: null,
|
role: null, // role 字段存储角色ID
|
||||||
role: "",
|
|
||||||
status: "active",
|
status: "active",
|
||||||
tenant_id: tenantId,
|
tenant_id: tenantId,
|
||||||
};
|
};
|
||||||
@ -420,12 +418,15 @@ const handleEdit = async (user: User) => {
|
|||||||
// 获取当前租户ID
|
// 获取当前租户ID
|
||||||
const tenantId = getCurrentTenantId();
|
const tenantId = getCurrentTenantId();
|
||||||
|
|
||||||
// 处理角色ID,兼容旧数据
|
// 处理角色:data.role 存储的是角色ID(数字)
|
||||||
let roleId = data.role_id || data.roleId || null;
|
let roleValue = data.role || null;
|
||||||
if (!roleId && data.role) {
|
|
||||||
// 如果只有role代码,尝试从roleList中查找对应的roleId
|
// 处理状态:后端返回的是数字(0或1),前端使用字符串("active"或"inactive")
|
||||||
const role = roleList.value.find(r => r.roleCode === data.role);
|
let statusStr = "active";
|
||||||
roleId = role ? role.roleId : null;
|
if (typeof data.status === 'number') {
|
||||||
|
statusStr = data.status === 1 ? "active" : "inactive";
|
||||||
|
} else if (typeof data.status === 'string') {
|
||||||
|
statusStr = data.status;
|
||||||
}
|
}
|
||||||
|
|
||||||
form.value = {
|
form.value = {
|
||||||
@ -434,9 +435,8 @@ const handleEdit = async (user: User) => {
|
|||||||
nickname: data.nickname,
|
nickname: data.nickname,
|
||||||
password: "",
|
password: "",
|
||||||
email: data.email,
|
email: data.email,
|
||||||
roleId: roleId,
|
role: roleValue, // role 字段存储角色ID
|
||||||
role: data.role || "",
|
status: statusStr,
|
||||||
status: data.status || "active",
|
|
||||||
tenant_id: data.tenant_id || tenantId,
|
tenant_id: data.tenant_id || tenantId,
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -502,25 +502,31 @@ const submitForm = async () => {
|
|||||||
passwordError.value = res.message || "密码修改失败";
|
passwordError.value = res.message || "密码修改失败";
|
||||||
}
|
}
|
||||||
} else if (isEdit.value) {
|
} else if (isEdit.value) {
|
||||||
// 构建提交数据,优先使用roleId
|
// 检查ID是否存在
|
||||||
|
if (!form.value.id || form.value.id === 0) {
|
||||||
|
ElMessage.error("用户ID不能为空,请重新选择用户");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建提交数据
|
||||||
const submitData: any = {
|
const submitData: any = {
|
||||||
|
id: form.value.id,
|
||||||
username: form.value.username,
|
username: form.value.username,
|
||||||
nickname: form.value.nickname,
|
nickname: form.value.nickname,
|
||||||
email: form.value.email,
|
email: form.value.email,
|
||||||
status: form.value.status,
|
status: form.value.status,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (form.value.roleId) {
|
// role 字段存储角色ID
|
||||||
submitData.roleId = form.value.roleId;
|
if (form.value.role) {
|
||||||
} else if (form.value.role) {
|
submitData.role = form.value.role; // 后端接收 role 参数
|
||||||
// 兼容旧数据,如果没有roleId但有role代码,也传递
|
|
||||||
submitData.role = form.value.role;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.value.tenant_id) {
|
if (form.value.tenant_id) {
|
||||||
submitData.tenant_id = form.value.tenant_id;
|
submitData.tenant_id = form.value.tenant_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('编辑用户提交数据:', submitData); // 调试日志
|
||||||
await editUser(form.value.id, submitData);
|
await editUser(form.value.id, submitData);
|
||||||
ElMessage.success({
|
ElMessage.success({
|
||||||
message: "更新成功",
|
message: "更新成功",
|
||||||
@ -533,7 +539,7 @@ const submitForm = async () => {
|
|||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
} else {
|
} else {
|
||||||
// 构建提交数据,优先使用roleId
|
// 构建提交数据
|
||||||
const submitData: any = {
|
const submitData: any = {
|
||||||
username: form.value.username,
|
username: form.value.username,
|
||||||
nickname: form.value.nickname,
|
nickname: form.value.nickname,
|
||||||
@ -542,16 +548,16 @@ const submitForm = async () => {
|
|||||||
status: form.value.status,
|
status: form.value.status,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (form.value.roleId) {
|
// role 字段存储角色ID
|
||||||
submitData.roleId = form.value.roleId;
|
if (form.value.role) {
|
||||||
} else if (form.value.role) {
|
submitData.role = form.value.role; // 后端接收 role 参数
|
||||||
submitData.role = form.value.role;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.value.tenant_id) {
|
if (form.value.tenant_id) {
|
||||||
submitData.tenant_id = form.value.tenant_id;
|
submitData.tenant_id = form.value.tenant_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('添加用户提交数据:', submitData); // 调试日志
|
||||||
await addUser(submitData);
|
await addUser(submitData);
|
||||||
ElMessage.success({
|
ElMessage.success({
|
||||||
message: "添加成功",
|
message: "添加成功",
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -455,6 +460,12 @@ func (c *FileController) Post() {
|
|||||||
username = "unknown"
|
username = "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从JWT中间件获取租户ID
|
||||||
|
tenantID := "default"
|
||||||
|
if tid, ok := c.Ctx.Input.GetData("tenantId").(int); ok && tid > 0 {
|
||||||
|
tenantID = strconv.Itoa(tid)
|
||||||
|
}
|
||||||
|
|
||||||
// 获取上传的文件
|
// 获取上传的文件
|
||||||
file, header, err := c.GetFile("file")
|
file, header, err := c.GetFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -473,18 +484,91 @@ func (c *FileController) Post() {
|
|||||||
fileExt := strings.ToLower(filepath.Ext(originalName))
|
fileExt := strings.ToLower(filepath.Ext(originalName))
|
||||||
fileName := strings.TrimSuffix(originalName, fileExt)
|
fileName := strings.TrimSuffix(originalName, fileExt)
|
||||||
|
|
||||||
|
// 读取文件内容到内存以计算MD5(对于大文件可能需要优化)
|
||||||
|
fileData := make([]byte, fileSize)
|
||||||
|
n, err := file.Read(fileData)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "读取文件失败: " + err.Error(),
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if int64(n) != fileSize {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "读取文件不完整",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算文件MD5值
|
||||||
|
hash := md5.New()
|
||||||
|
hash.Write(fileData)
|
||||||
|
fileMD5 := hex.EncodeToString(hash.Sum(nil))
|
||||||
|
|
||||||
|
// 检查是否已存在相同MD5的文件
|
||||||
|
existingFile, err := models.GetFileByMD5AndTenant(fileMD5, tenantID)
|
||||||
|
if err == nil && existingFile != nil {
|
||||||
|
// 文件已存在,不保存文件,只创建数据库记录
|
||||||
|
// 生成日期路径(年/月/日)
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// 创建文件信息记录(使用已存在的文件路径)
|
||||||
|
fileInfo := models.FileInfo{
|
||||||
|
TenantID: tenantID,
|
||||||
|
UserID: userID,
|
||||||
|
FileName: fileName,
|
||||||
|
OriginalName: originalName,
|
||||||
|
FilePath: existingFile.FilePath, // 使用已存在文件的路径
|
||||||
|
FileURL: existingFile.FileURL, // 使用已存在文件的URL
|
||||||
|
FileSize: fileSize,
|
||||||
|
FileType: getFileTypeByExt(fileExt),
|
||||||
|
FileExt: fileExt,
|
||||||
|
MD5: fileMD5,
|
||||||
|
Category: c.GetString("category"),
|
||||||
|
Status: 1,
|
||||||
|
UploadBy: username,
|
||||||
|
UploadTime: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果分类为空,使用默认分类
|
||||||
|
if fileInfo.Category == "" {
|
||||||
|
fileInfo.Category = "未分类"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到数据库(只保存记录,不保存文件)
|
||||||
|
id, err := models.AddFile(&fileInfo)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "保存文件信息失败: " + err.Error(),
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInfo.ID = id
|
||||||
|
|
||||||
|
// 返回成功响应
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": true,
|
||||||
|
"message": "文件上传成功(重复文件,使用已有文件)",
|
||||||
|
"data": fileInfo,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件不存在,正常上传流程
|
||||||
// 获取分类(可选)
|
// 获取分类(可选)
|
||||||
category := c.GetString("category")
|
category := c.GetString("category")
|
||||||
if category == "" {
|
if category == "" {
|
||||||
category = "未分类"
|
category = "未分类"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取租户ID(可选,从请求参数或中间件获取)
|
|
||||||
tenantID := c.GetString("tenant_id")
|
|
||||||
if tenantID == "" {
|
|
||||||
tenantID = "default"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成日期路径(年/月/日)
|
// 生成日期路径(年/月/日)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
datePath := now.Format("2006/01/02")
|
datePath := now.Format("2006/01/02")
|
||||||
@ -513,8 +597,8 @@ func (c *FileController) Post() {
|
|||||||
// 计算相对路径(用于存储到数据库)
|
// 计算相对路径(用于存储到数据库)
|
||||||
relativePath := path.Join("uploads", datePath, uniqueFileName)
|
relativePath := path.Join("uploads", datePath, uniqueFileName)
|
||||||
|
|
||||||
// 保存文件
|
// 保存文件(将已读取的数据写入文件)
|
||||||
if err := c.SaveToFile("file", savePath); err != nil {
|
if err := os.WriteFile(savePath, fileData, 0644); err != nil {
|
||||||
c.Data["json"] = map[string]interface{}{
|
c.Data["json"] = map[string]interface{}{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "保存文件失败: " + err.Error(),
|
"message": "保存文件失败: " + err.Error(),
|
||||||
@ -524,19 +608,7 @@ func (c *FileController) Post() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取文件类型
|
// 获取文件类型
|
||||||
fileType := "other"
|
fileType := getFileTypeByExt(fileExt)
|
||||||
switch fileExt {
|
|
||||||
case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp":
|
|
||||||
fileType = "image"
|
|
||||||
case ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt":
|
|
||||||
fileType = "document"
|
|
||||||
case ".mp4", ".avi", ".mov", ".wmv":
|
|
||||||
fileType = "video"
|
|
||||||
case ".mp3", ".wav", ".flac":
|
|
||||||
fileType = "audio"
|
|
||||||
case ".zip", ".rar", ".7z":
|
|
||||||
fileType = "archive"
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构造文件URL(相对路径)
|
// 构造文件URL(相对路径)
|
||||||
fileURL := "/" + relativePath
|
fileURL := "/" + relativePath
|
||||||
@ -552,7 +624,9 @@ func (c *FileController) Post() {
|
|||||||
FileSize: fileSize,
|
FileSize: fileSize,
|
||||||
FileType: fileType,
|
FileType: fileType,
|
||||||
FileExt: fileExt,
|
FileExt: fileExt,
|
||||||
|
MD5: fileMD5,
|
||||||
Category: category,
|
Category: category,
|
||||||
|
Status: 1, // 设置为正常状态
|
||||||
UploadBy: username,
|
UploadBy: username,
|
||||||
UploadTime: now,
|
UploadTime: now,
|
||||||
}
|
}
|
||||||
@ -580,3 +654,351 @@ func (c *FileController) Post() {
|
|||||||
}
|
}
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DownloadFile 下载文件
|
||||||
|
func (c *FileController) DownloadFile() {
|
||||||
|
idStr := c.Ctx.Input.Param(":id")
|
||||||
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "参数错误",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件信息
|
||||||
|
file, err := models.GetFileById(id)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "文件不存在",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取实际的文件记录(如果文件不存在,通过MD5查找)
|
||||||
|
actualFile, err := getActualFile(file)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "获取文件信息失败",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件是否存在(尝试多个可能的路径)
|
||||||
|
var filePath string
|
||||||
|
possiblePaths := []string{
|
||||||
|
actualFile.FilePath, // 直接使用相对路径
|
||||||
|
filepath.Join("server", actualFile.FilePath), // server目录前缀
|
||||||
|
filepath.Join(".", actualFile.FilePath), // 当前目录
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range possiblePaths {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
filePath = path
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filePath == "" {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "文件不存在于服务器",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
c.Ctx.Output.Header("Content-Description", "File Transfer")
|
||||||
|
c.Ctx.Output.Header("Content-Type", "application/octet-stream")
|
||||||
|
c.Ctx.Output.Header("Content-Disposition", "attachment; filename="+url.QueryEscape(actualFile.OriginalName))
|
||||||
|
c.Ctx.Output.Header("Content-Transfer-Encoding", "binary")
|
||||||
|
c.Ctx.Output.Header("Expires", "0")
|
||||||
|
c.Ctx.Output.Header("Cache-Control", "must-revalidate")
|
||||||
|
c.Ctx.Output.Header("Pragma", "public")
|
||||||
|
|
||||||
|
// 输出文件
|
||||||
|
http.ServeFile(c.Ctx.ResponseWriter, c.Ctx.Request, filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreviewFile 预览文件
|
||||||
|
func (c *FileController) PreviewFile() {
|
||||||
|
idStr := c.Ctx.Input.Param(":id")
|
||||||
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "参数错误",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件信息
|
||||||
|
file, err := models.GetFileById(id)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "文件不存在",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取实际的文件记录(如果文件不存在,通过MD5查找)
|
||||||
|
actualFile, err := getActualFile(file)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "获取文件信息失败",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件是否可预览
|
||||||
|
if !actualFile.CanPreview() {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "该文件类型不支持预览",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件是否存在(尝试多个可能的路径)
|
||||||
|
var filePath string
|
||||||
|
possiblePaths := []string{
|
||||||
|
actualFile.FilePath, // 直接使用相对路径
|
||||||
|
filepath.Join("server", actualFile.FilePath), // server目录前缀
|
||||||
|
filepath.Join(".", actualFile.FilePath), // 当前目录
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range possiblePaths {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
filePath = path
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filePath == "" {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "文件不存在于服务器",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置正确的 Content-Type
|
||||||
|
contentType := getContentType(actualFile.FileExt)
|
||||||
|
c.Ctx.Output.Header("Content-Type", contentType)
|
||||||
|
c.Ctx.Output.Header("Content-Disposition", "inline; filename="+url.QueryEscape(actualFile.OriginalName))
|
||||||
|
|
||||||
|
// 打开文件
|
||||||
|
fileHandle, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "打开文件失败: " + err.Error(),
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer fileHandle.Close()
|
||||||
|
|
||||||
|
// 复制文件内容到响应
|
||||||
|
io.Copy(c.Ctx.ResponseWriter, fileHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicPreviewFile 公开预览文件(用于 Office Online Viewer,无需认证但仅用于预览)
|
||||||
|
func (c *FileController) PublicPreviewFile() {
|
||||||
|
idStr := c.Ctx.Input.Param(":id")
|
||||||
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "参数错误",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件信息
|
||||||
|
file, err := models.GetFileById(id)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "文件不存在",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取实际的文件记录
|
||||||
|
actualFile, err := getActualFile(file)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "获取文件信息失败",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件是否可预览
|
||||||
|
if !actualFile.CanPreview() {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "该文件类型不支持预览",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
var filePath string
|
||||||
|
possiblePaths := []string{
|
||||||
|
actualFile.FilePath,
|
||||||
|
filepath.Join("server", actualFile.FilePath),
|
||||||
|
filepath.Join(".", actualFile.FilePath),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range possiblePaths {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
filePath = path
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filePath == "" {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "文件不存在于服务器",
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置正确的 Content-Type
|
||||||
|
contentType := getContentType(actualFile.FileExt)
|
||||||
|
c.Ctx.Output.Header("Content-Type", contentType)
|
||||||
|
c.Ctx.Output.Header("Content-Disposition", "inline; filename="+url.QueryEscape(actualFile.OriginalName))
|
||||||
|
// 允许跨域访问(Office Online Viewer 需要)
|
||||||
|
c.Ctx.Output.Header("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
|
// 打开文件
|
||||||
|
fileHandle, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"success": false,
|
||||||
|
"message": "打开文件失败: " + err.Error(),
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer fileHandle.Close()
|
||||||
|
|
||||||
|
// 复制文件内容到响应
|
||||||
|
io.Copy(c.Ctx.ResponseWriter, fileHandle)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFileTypeByExt 根据文件扩展名获取文件类型
|
||||||
|
func getFileTypeByExt(ext string) string {
|
||||||
|
ext = strings.ToLower(ext)
|
||||||
|
switch ext {
|
||||||
|
case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp":
|
||||||
|
return "image"
|
||||||
|
case ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt":
|
||||||
|
return "document"
|
||||||
|
case ".mp4", ".avi", ".mov", ".wmv":
|
||||||
|
return "video"
|
||||||
|
case ".mp3", ".wav", ".flac":
|
||||||
|
return "audio"
|
||||||
|
case ".zip", ".rar", ".7z":
|
||||||
|
return "archive"
|
||||||
|
default:
|
||||||
|
return "other"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getContentType 根据文件扩展名获取 Content-Type
|
||||||
|
func getContentType(ext string) string {
|
||||||
|
ext = strings.ToLower(ext)
|
||||||
|
contentTypes := map[string]string{
|
||||||
|
// 图片
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
".png": "image/png",
|
||||||
|
".gif": "image/gif",
|
||||||
|
".bmp": "image/bmp",
|
||||||
|
".webp": "image/webp",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
// 文档
|
||||||
|
".pdf": "application/pdf",
|
||||||
|
".txt": "text/plain; charset=utf-8",
|
||||||
|
".doc": "application/msword",
|
||||||
|
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
".xls": "application/vnd.ms-excel",
|
||||||
|
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
".ppt": "application/vnd.ms-powerpoint",
|
||||||
|
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||||
|
// 视频
|
||||||
|
".mp4": "video/mp4",
|
||||||
|
".webm": "video/webm",
|
||||||
|
// 音频
|
||||||
|
".mp3": "audio/mpeg",
|
||||||
|
".wav": "audio/wav",
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentType, ok := contentTypes[ext]; ok {
|
||||||
|
return contentType
|
||||||
|
}
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
// getActualFile 获取实际的文件记录(如果当前记录的文件不存在,通过MD5查找唯一文件)
|
||||||
|
func getActualFile(file *models.FileInfo) (*models.FileInfo, error) {
|
||||||
|
// 检查当前记录的文件是否存在(尝试多个可能的路径)
|
||||||
|
possiblePaths := []string{
|
||||||
|
file.FilePath, // 直接使用相对路径
|
||||||
|
filepath.Join("server", file.FilePath), // server目录前缀
|
||||||
|
filepath.Join(".", file.FilePath), // 当前目录
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range possiblePaths {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
// 文件存在,返回当前记录
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件不存在,通过MD5查找唯一文件
|
||||||
|
if file.MD5 == "" {
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
actualFile, err := models.GetFileByMD5(file.MD5)
|
||||||
|
if err == nil && actualFile != nil {
|
||||||
|
// 检查找到的文件是否存在
|
||||||
|
possiblePaths = []string{
|
||||||
|
actualFile.FilePath,
|
||||||
|
filepath.Join("server", actualFile.FilePath),
|
||||||
|
filepath.Join(".", actualFile.FilePath),
|
||||||
|
}
|
||||||
|
for _, path := range possiblePaths {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return actualFile, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果找不到,返回原始记录
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -25,6 +25,8 @@ func (c *UserController) GetAllUsers() {
|
|||||||
"avatar": user.Avatar,
|
"avatar": user.Avatar,
|
||||||
"nickname": user.Nickname,
|
"nickname": user.Nickname,
|
||||||
"tenant_id": user.TenantId,
|
"tenant_id": user.TenantId,
|
||||||
|
"status": user.Status,
|
||||||
|
"role": user.Role,
|
||||||
"lastLoginTime": user.LastLoginTime,
|
"lastLoginTime": user.LastLoginTime,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -37,6 +39,56 @@ func (c *UserController) GetAllUsers() {
|
|||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTenantUsers 获取指定租户下的所有用户(排除已删除的用户)
|
||||||
|
func (c *UserController) GetTenantUsers() {
|
||||||
|
// 从URL参数获取租户ID
|
||||||
|
tenantId, err := c.GetInt(":tenantId")
|
||||||
|
if err != nil || tenantId <= 0 {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 1,
|
||||||
|
"message": "租户ID无效",
|
||||||
|
"data": nil,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用模型层方法查询
|
||||||
|
users, err := models.GetTenantUsers(tenantId)
|
||||||
|
if err != nil {
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 1,
|
||||||
|
"message": err.Error(),
|
||||||
|
"data": nil,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化返回数据
|
||||||
|
userList := make([]map[string]interface{}, 0)
|
||||||
|
for _, user := range users {
|
||||||
|
userList = append(userList, map[string]interface{}{
|
||||||
|
"id": user.Id,
|
||||||
|
"username": user.Username,
|
||||||
|
"email": user.Email,
|
||||||
|
"avatar": user.Avatar,
|
||||||
|
"nickname": user.Nickname,
|
||||||
|
"tenant_id": user.TenantId,
|
||||||
|
"status": user.Status,
|
||||||
|
"role": user.Role,
|
||||||
|
"last_login_time": user.LastLoginTime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
"code": 0,
|
||||||
|
"message": "获取租户用户列表成功",
|
||||||
|
"data": userList,
|
||||||
|
}
|
||||||
|
c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
// ChangePassword 修改用户密码
|
// ChangePassword 修改用户密码
|
||||||
func (c *UserController) ChangePassword() {
|
func (c *UserController) ChangePassword() {
|
||||||
// 从URL获取用户ID
|
// 从URL获取用户ID
|
||||||
@ -144,6 +196,8 @@ func (c *UserController) GetUserInfo() {
|
|||||||
"avatar": user.Avatar,
|
"avatar": user.Avatar,
|
||||||
"nickname": user.Nickname,
|
"nickname": user.Nickname,
|
||||||
"tenant_id": user.TenantId,
|
"tenant_id": user.TenantId,
|
||||||
|
"role": user.Role,
|
||||||
|
"status": user.Status,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
c.ServeJSON()
|
c.ServeJSON()
|
||||||
@ -159,6 +213,7 @@ func (c *UserController) AddUser() {
|
|||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
TenantId int `json:"tenant_id"`
|
TenantId int `json:"tenant_id"`
|
||||||
|
Role int `json:"role"` // 角色ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析请求体JSON数据
|
// 解析请求体JSON数据
|
||||||
@ -210,6 +265,7 @@ func (c *UserController) AddUser() {
|
|||||||
userData.Nickname,
|
userData.Nickname,
|
||||||
userData.Avatar,
|
userData.Avatar,
|
||||||
userData.TenantId,
|
userData.TenantId,
|
||||||
|
userData.Role, // 添加 role 参数
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Data["json"] = map[string]interface{}{
|
c.Data["json"] = map[string]interface{}{
|
||||||
@ -243,6 +299,8 @@ func (c *UserController) EditUser() {
|
|||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Nickname string `json:"nickname"`
|
Nickname string `json:"nickname"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Role int `json:"role"` // 改为 role,存储角色ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析请求体JSON
|
// 解析请求体JSON
|
||||||
@ -275,6 +333,8 @@ func (c *UserController) EditUser() {
|
|||||||
updateData.Email,
|
updateData.Email,
|
||||||
updateData.Nickname,
|
updateData.Nickname,
|
||||||
updateData.Avatar,
|
updateData.Avatar,
|
||||||
|
updateData.Status,
|
||||||
|
updateData.Role, // 改为 Role
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Data["json"] = map[string]interface{}{
|
c.Data["json"] = map[string]interface{}{
|
||||||
|
|||||||
@ -63,5 +63,6 @@ func JWTAuthMiddleware() web.FilterFunc {
|
|||||||
// 将用户信息存储在上下文
|
// 将用户信息存储在上下文
|
||||||
ctx.Input.SetData("userId", claims.UserID)
|
ctx.Input.SetData("userId", claims.UserID)
|
||||||
ctx.Input.SetData("username", claims.Username)
|
ctx.Input.SetData("username", claims.Username)
|
||||||
|
ctx.Input.SetData("tenantId", claims.TenantId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/beego/beego/v2/client/orm"
|
"github.com/beego/beego/v2/client/orm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,6 +24,7 @@ type FileInfo struct {
|
|||||||
FileSize int64 `orm:"column(file_size);default(0)" json:"file_size"`
|
FileSize int64 `orm:"column(file_size);default(0)" json:"file_size"`
|
||||||
FileType string `orm:"column(file_type);size(50)" json:"file_type"`
|
FileType string `orm:"column(file_type);size(50)" json:"file_type"`
|
||||||
FileExt string `orm:"column(file_ext);size(20)" json:"file_ext"`
|
FileExt string `orm:"column(file_ext);size(20)" json:"file_ext"`
|
||||||
|
MD5 string `orm:"column(md5);size(32);null" json:"md5"`
|
||||||
|
|
||||||
// 分类信息
|
// 分类信息
|
||||||
Category string `orm:"column(category);size(100)" json:"category"`
|
Category string `orm:"column(category);size(100)" json:"category"`
|
||||||
@ -34,6 +37,7 @@ type FileInfo struct {
|
|||||||
// 上传信息
|
// 上传信息
|
||||||
UploadBy string `orm:"column(upload_by);size(100)" json:"upload_by"`
|
UploadBy string `orm:"column(upload_by);size(100)" json:"upload_by"`
|
||||||
UploadTime time.Time `orm:"column(upload_time);type(datetime);auto_now_add" json:"upload_time"`
|
UploadTime time.Time `orm:"column(upload_time);type(datetime);auto_now_add" json:"upload_time"`
|
||||||
|
DeleteTime *time.Time `orm:"column(delete_time);type(datetime);null" json:"delete_time,omitempty"`
|
||||||
|
|
||||||
// 关联的用户信息(非数据库字段)
|
// 关联的用户信息(非数据库字段)
|
||||||
User *User `orm:"-" json:"user,omitempty"`
|
User *User `orm:"-" json:"user,omitempty"`
|
||||||
@ -44,6 +48,31 @@ func (f *FileInfo) TableName() string {
|
|||||||
return "yz_files"
|
return "yz_files"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CanPreview 判断文件是否可以在线预览
|
||||||
|
func (f *FileInfo) CanPreview() bool {
|
||||||
|
previewableExts := map[string]bool{
|
||||||
|
// 图片格式
|
||||||
|
".jpg": true,
|
||||||
|
".jpeg": true,
|
||||||
|
".png": true,
|
||||||
|
".gif": true,
|
||||||
|
".bmp": true,
|
||||||
|
".webp": true,
|
||||||
|
".svg": true,
|
||||||
|
// 文档格式(仅支持 .docx,不支持旧的 .doc、Excel、PPT)
|
||||||
|
".pdf": true,
|
||||||
|
".txt": true,
|
||||||
|
".docx": true,
|
||||||
|
// 视频格式
|
||||||
|
".mp4": true,
|
||||||
|
".webm": true,
|
||||||
|
// 音频格式
|
||||||
|
".mp3": true,
|
||||||
|
".wav": true,
|
||||||
|
}
|
||||||
|
return previewableExts[strings.ToLower(f.FileExt)]
|
||||||
|
}
|
||||||
|
|
||||||
// GetAllFiles 获取所有文件信息
|
// GetAllFiles 获取所有文件信息
|
||||||
func GetAllFiles() ([]*FileInfo, error) {
|
func GetAllFiles() ([]*FileInfo, error) {
|
||||||
o := orm.NewOrm()
|
o := orm.NewOrm()
|
||||||
@ -109,7 +138,7 @@ func UpdateFile(file *FileInfo) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteFile 删除文件信息(软删除,设置状态为0)
|
// DeleteFile 删除文件信息(软删除,设置状态为0并记录删除时间)
|
||||||
func DeleteFile(id int64) error {
|
func DeleteFile(id int64) error {
|
||||||
o := orm.NewOrm()
|
o := orm.NewOrm()
|
||||||
file := &FileInfo{ID: id}
|
file := &FileInfo{ID: id}
|
||||||
@ -117,7 +146,9 @@ func DeleteFile(id int64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
file.Status = 0
|
file.Status = 0
|
||||||
_, err := o.Update(file, "Status")
|
now := time.Now()
|
||||||
|
file.DeleteTime = &now
|
||||||
|
_, err := o.Update(file, "Status", "DeleteTime")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,22 +190,45 @@ func GetFileStatistics(tenantID string) (map[string]interface{}, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchFiles 搜索文件
|
// SearchFiles 搜索文件(通过原始文件名搜索)
|
||||||
func SearchFiles(keyword string, tenantID string) ([]*FileInfo, error) {
|
func SearchFiles(keyword string, tenantID string) ([]*FileInfo, error) {
|
||||||
o := orm.NewOrm()
|
o := orm.NewOrm()
|
||||||
var files []*FileInfo
|
var files []*FileInfo
|
||||||
|
|
||||||
// 构建查询条件
|
// 构建查询条件 - 只通过原始文件名搜索
|
||||||
qs := o.QueryTable("yz_files").Filter("tenant_id", tenantID).Filter("status", 1)
|
qs := o.QueryTable("yz_files").Filter("tenant_id", tenantID).Filter("status", 1)
|
||||||
|
|
||||||
// 搜索文件名、原始文件名、分类(使用or条件)
|
// 搜索原始文件名
|
||||||
cond := orm.NewCondition()
|
qs = qs.Filter("original_name__icontains", keyword)
|
||||||
cond = cond.Or("file_name__icontains", keyword).
|
|
||||||
Or("original_name__icontains", keyword).
|
|
||||||
Or("category__icontains", keyword)
|
|
||||||
|
|
||||||
qs = qs.SetCond(cond)
|
|
||||||
|
|
||||||
_, err := qs.OrderBy("-upload_time").All(&files)
|
_, err := qs.OrderBy("-upload_time").All(&files)
|
||||||
return files, err
|
return files, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFileByMD5 根据MD5获取文件信息(获取唯一的文件记录)
|
||||||
|
func GetFileByMD5(md5 string) (*FileInfo, error) {
|
||||||
|
o := orm.NewOrm()
|
||||||
|
var file FileInfo
|
||||||
|
err := o.QueryTable("yz_files").Filter("md5", md5).Filter("status", 1).OrderBy("upload_time").One(&file)
|
||||||
|
if err == orm.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFileByMD5AndTenant 根据MD5和租户ID获取文件信息
|
||||||
|
func GetFileByMD5AndTenant(md5 string, tenantID string) (*FileInfo, error) {
|
||||||
|
o := orm.NewOrm()
|
||||||
|
var file FileInfo
|
||||||
|
err := o.QueryTable("yz_files").Filter("md5", md5).Filter("tenant_id", tenantID).Filter("status", 1).OrderBy("upload_time").One(&file)
|
||||||
|
if err == orm.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &file, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -24,6 +24,8 @@ type User struct {
|
|||||||
Email string
|
Email string
|
||||||
Avatar string
|
Avatar string
|
||||||
Nickname string
|
Nickname string
|
||||||
|
Status int `orm:"column(status);default(1)" json:"status"`
|
||||||
|
Role int `orm:"column(role);default(0)" json:"role"`
|
||||||
DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"`
|
DeleteTime *time.Time `orm:"column(delete_time);null;type(datetime)" json:"delete_time"`
|
||||||
LastLoginTime *time.Time `orm:"column(last_login_time);null;type(datetime)" json:"last_login_time"`
|
LastLoginTime *time.Time `orm:"column(last_login_time);null;type(datetime)" json:"last_login_time"`
|
||||||
}
|
}
|
||||||
@ -147,6 +149,20 @@ func GetAllUsers(tenantId int) []*User {
|
|||||||
return users
|
return users
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTenantUsers 获取指定租户下的所有用户(排除已删除的用户)
|
||||||
|
func GetTenantUsers(tenantId int) ([]*User, error) {
|
||||||
|
o := orm.NewOrm()
|
||||||
|
var users []*User
|
||||||
|
|
||||||
|
// 查询指定租户下未删除的用户
|
||||||
|
_, err := o.Raw("SELECT * FROM yz_users WHERE tenant_id = ? AND delete_time IS NULL ORDER BY id DESC", tenantId).QueryRows(&users)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("查询租户用户失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetUserInfo 根据用户ID或用户名获取用户
|
// GetUserInfo 根据用户ID或用户名获取用户
|
||||||
func GetUserInfo(userId int, username string, tenantId int) (*User, error) {
|
func GetUserInfo(userId int, username string, tenantId int) (*User, error) {
|
||||||
o := orm.NewOrm()
|
o := orm.NewOrm()
|
||||||
@ -221,7 +237,7 @@ func ValidateUser(username, password string, tenantName string) (*User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AddUser 向数据库添加新用户
|
// AddUser 向数据库添加新用户
|
||||||
func AddUser(username, password, email, nickname, avatar string, tenantId int) (*User, error) {
|
func AddUser(username, password, email, nickname, avatar string, tenantId, role int) (*User, error) {
|
||||||
// 1. 验证租户是否存在且有效
|
// 1. 验证租户是否存在且有效
|
||||||
o := orm.NewOrm()
|
o := orm.NewOrm()
|
||||||
var tenantExists bool
|
var tenantExists bool
|
||||||
@ -258,11 +274,12 @@ func AddUser(username, password, email, nickname, avatar string, tenantId int) (
|
|||||||
user := &User{
|
user := &User{
|
||||||
TenantId: tenantId,
|
TenantId: tenantId,
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: hashedPassword, // 存储加密后的密码
|
Password: hashedPassword,
|
||||||
Salt: salt, // 存储盐值(用于后续验证)
|
Salt: salt,
|
||||||
Email: email,
|
Email: email,
|
||||||
Nickname: nickname,
|
Nickname: nickname,
|
||||||
Avatar: avatar,
|
Avatar: avatar,
|
||||||
|
Role: role, // 设置角色ID
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 插入数据库(使用之前定义的 o)
|
// 5. 插入数据库(使用之前定义的 o)
|
||||||
@ -276,7 +293,7 @@ func AddUser(username, password, email, nickname, avatar string, tenantId int) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EditUser 更新用户信息
|
// EditUser 更新用户信息
|
||||||
func EditUser(id int, username, email, nickname, avatar string) (*User, error) {
|
func EditUser(id int, username, email, nickname, avatar, status string, roleId int) (*User, error) {
|
||||||
// 根据ID查询用户
|
// 根据ID查询用户
|
||||||
o := orm.NewOrm()
|
o := orm.NewOrm()
|
||||||
user := &User{}
|
user := &User{}
|
||||||
@ -307,6 +324,20 @@ func EditUser(id int, username, email, nickname, avatar string) (*User, error) {
|
|||||||
user.Avatar = avatar
|
user.Avatar = avatar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新状态(将字符串转换为数字)
|
||||||
|
if status != "" {
|
||||||
|
if status == "active" {
|
||||||
|
user.Status = 1
|
||||||
|
} else if status == "inactive" {
|
||||||
|
user.Status = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新角色ID
|
||||||
|
if roleId > 0 {
|
||||||
|
user.Role = roleId
|
||||||
|
}
|
||||||
|
|
||||||
// 执行数据库更新
|
// 执行数据库更新
|
||||||
_, err = o.Update(user)
|
_, err = o.Update(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
1640
server/package-lock.json
generated
Normal file
1640
server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
server/package.json
Normal file
5
server/package.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"vue-office": "^0.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ package routers
|
|||||||
import (
|
import (
|
||||||
"server/controllers"
|
"server/controllers"
|
||||||
"server/middleware"
|
"server/middleware"
|
||||||
|
"strings"
|
||||||
|
|
||||||
beego "github.com/beego/beego/v2/server/web"
|
beego "github.com/beego/beego/v2/server/web"
|
||||||
"github.com/beego/beego/v2/server/web/context"
|
"github.com/beego/beego/v2/server/web/context"
|
||||||
@ -44,6 +45,11 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否为公开预览接口(/api/files/public-preview/:id)
|
||||||
|
if strings.HasPrefix(path, "/api/files/public-preview/") {
|
||||||
|
skipAuth = true
|
||||||
|
}
|
||||||
|
|
||||||
if !skipAuth {
|
if !skipAuth {
|
||||||
middleware.JWTAuthMiddleware()(ctx)
|
middleware.JWTAuthMiddleware()(ctx)
|
||||||
}
|
}
|
||||||
@ -63,6 +69,7 @@ func init() {
|
|||||||
beego.Router("/api/deleteUser/:id", &controllers.UserController{}, "delete:DeleteUser")
|
beego.Router("/api/deleteUser/:id", &controllers.UserController{}, "delete:DeleteUser")
|
||||||
beego.Router("/api/changePassword/:id", &controllers.UserController{}, "post:ChangePassword")
|
beego.Router("/api/changePassword/:id", &controllers.UserController{}, "post:ChangePassword")
|
||||||
beego.Router("/api/reset-password", &controllers.UserController{}, "post:ResetPassword")
|
beego.Router("/api/reset-password", &controllers.UserController{}, "post:ResetPassword")
|
||||||
|
beego.Router("/api/tenantUsers/:tenantId", &controllers.UserController{}, "get:GetTenantUsers")
|
||||||
|
|
||||||
// 认证路由
|
// 认证路由
|
||||||
beego.Router("/api/login", &controllers.AuthController{}, "post:Login")
|
beego.Router("/api/login", &controllers.AuthController{}, "post:Login")
|
||||||
@ -84,6 +91,9 @@ func init() {
|
|||||||
beego.Router("/api/files", &controllers.FileController{}, "get:GetAllFiles")
|
beego.Router("/api/files", &controllers.FileController{}, "get:GetAllFiles")
|
||||||
beego.Router("/api/files", &controllers.FileController{}, "post:Post")
|
beego.Router("/api/files", &controllers.FileController{}, "post:Post")
|
||||||
beego.Router("/api/files/my", &controllers.FileController{}, "get:GetMyFiles")
|
beego.Router("/api/files/my", &controllers.FileController{}, "get:GetMyFiles")
|
||||||
|
beego.Router("/api/files/download/:id", &controllers.FileController{}, "get:DownloadFile")
|
||||||
|
beego.Router("/api/files/preview/:id", &controllers.FileController{}, "get:PreviewFile")
|
||||||
|
beego.Router("/api/files/public-preview/:id", &controllers.FileController{}, "get:PublicPreviewFile")
|
||||||
beego.Router("/api/files/:id", &controllers.FileController{}, "get:GetFileById")
|
beego.Router("/api/files/:id", &controllers.FileController{}, "get:GetFileById")
|
||||||
beego.Router("/api/files/tenant", &controllers.FileController{}, "get:GetFilesByTenant")
|
beego.Router("/api/files/tenant", &controllers.FileController{}, "get:GetFilesByTenant")
|
||||||
beego.Router("/api/files/:id", &controllers.FileController{}, "put:UpdateFile")
|
beego.Router("/api/files/:id", &controllers.FileController{}, "put:UpdateFile")
|
||||||
|
|||||||
@ -1,42 +0,0 @@
|
|||||||
package test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"runtime"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/beego/beego/v2/core/logs"
|
|
||||||
|
|
||||||
_ "server/routers"
|
|
||||||
|
|
||||||
beego "github.com/beego/beego/v2/server/web"
|
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
_, file, _, _ := runtime.Caller(0)
|
|
||||||
apppath, _ := filepath.Abs(filepath.Dir(filepath.Join(file, ".." + string(filepath.Separator))))
|
|
||||||
beego.TestBeegoInit(apppath)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// TestBeego is a sample to run an endpoint test
|
|
||||||
func TestBeego(t *testing.T) {
|
|
||||||
r, _ := http.NewRequest("GET", "/", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
beego.BeeApp.Handlers.ServeHTTP(w, r)
|
|
||||||
|
|
||||||
logs.Trace("testing", "TestBeego", "Code[%d]\n%s", w.Code, w.Body.String())
|
|
||||||
|
|
||||||
Convey("Subject: Test Station Endpoint\n", t, func() {
|
|
||||||
Convey("Status Code Should Be 200", func() {
|
|
||||||
So(w.Code, ShouldEqual, 200)
|
|
||||||
})
|
|
||||||
Convey("The Result Should Not Be Empty", func() {
|
|
||||||
So(w.Body.Len(), ShouldBeGreaterThan, 0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
package tests
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
"server/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFileModel(t *testing.T) {
|
|
||||||
// 测试创建文件信息
|
|
||||||
file := &models.FileInfo{
|
|
||||||
TenantID: "test-tenant-001",
|
|
||||||
FileName: "test-file.txt",
|
|
||||||
OriginalName: "original-test-file.txt",
|
|
||||||
FilePath: "/uploads/test/test-file.txt",
|
|
||||||
FileURL: "http://localhost:8080/uploads/test/test-file.txt",
|
|
||||||
FileSize: 1024,
|
|
||||||
FileType: "text/plain",
|
|
||||||
FileExt: "txt",
|
|
||||||
Category: "test",
|
|
||||||
SubCategory: "unit-test",
|
|
||||||
Status: 1,
|
|
||||||
IsPublic: 0,
|
|
||||||
UploadBy: "test-user",
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试添加文件
|
|
||||||
id, err := models.AddFile(file)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("添加文件失败: %v", err)
|
|
||||||
}
|
|
||||||
t.Logf("文件添加成功,ID: %d", id)
|
|
||||||
|
|
||||||
// 测试根据ID获取文件
|
|
||||||
retrievedFile, err := models.GetFileById(id)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("获取文件失败: %v", err)
|
|
||||||
}
|
|
||||||
if retrievedFile.FileName != file.FileName {
|
|
||||||
t.Errorf("文件名不匹配,期望: %s, 实际: %s", file.FileName, retrievedFile.FileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试更新文件
|
|
||||||
retrievedFile.FileName = "updated-test-file.txt"
|
|
||||||
err = models.UpdateFile(retrievedFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("更新文件失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试根据租户获取文件
|
|
||||||
files, err := models.GetFilesByTenant("test-tenant-001")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("根据租户获取文件失败: %v", err)
|
|
||||||
}
|
|
||||||
if len(files) == 0 {
|
|
||||||
t.Error("根据租户获取文件为空")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试根据分类获取文件
|
|
||||||
filesByCategory, err := models.GetFilesByCategory("test")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("根据分类获取文件失败: %v", err)
|
|
||||||
}
|
|
||||||
if len(filesByCategory) == 0 {
|
|
||||||
t.Error("根据分类获取文件为空")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试搜索文件
|
|
||||||
searchFiles, err := models.SearchFiles("test", "test-tenant-001")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("搜索文件失败: %v", err)
|
|
||||||
}
|
|
||||||
if len(searchFiles) == 0 {
|
|
||||||
t.Error("搜索文件结果为空")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试文件统计
|
|
||||||
stats, err := models.GetFileStatistics("test-tenant-001")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("获取文件统计失败: %v", err)
|
|
||||||
}
|
|
||||||
t.Logf("文件统计: %+v", stats)
|
|
||||||
|
|
||||||
// 测试软删除文件
|
|
||||||
err = models.DeleteFile(id)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("软删除文件失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证软删除后的状态
|
|
||||||
deletedFile, err := models.GetFileById(id)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("获取软删除后的文件失败: %v", err)
|
|
||||||
}
|
|
||||||
if deletedFile.Status != 0 {
|
|
||||||
t.Errorf("软删除后文件状态应为0,实际为: %d", deletedFile.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 测试硬删除文件
|
|
||||||
err = models.HardDeleteFile(id)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("硬删除文件失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Log("文件模型测试完成")
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user