提交 e535338e authored 作者: 王鹏飞's avatar 王鹏飞

feat: 合并 school 本地化适配,统一文件/视频上传并接入大模型配置

上级 b0691436
VITE_LOGIN_URL=https://login.ezijing.com/auth/login/index VITE_LOGIN_URL=https://login.ezijing.com/auth/login/index
VITE_QA_CENTER_URL=https://qa-center.ezijing.com VITE_QA_CENTER_URL=https://qa-center.ezijing.com
VITE_BI_URL=https://bi.ezijing.com VITE_BI_URL=https://bi.ezijing.com
VITE_STATIC_URL=https://saas-lab-api
VITE_LOGIN_URL=http://172.16.3.203:1001/auth/login/index
VITE_QA_CENTER_URL=http://172.16.3.203:1004
VITE_BI_URL=http://172.16.3.203:1012
VITE_FILE_PREVIEW_URL=http://172.16.3.203:8012
VITE_STATIC_URL=https://saas-lab-api
\ No newline at end of file
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="https://webapp-pub.ezijing.com/website/base/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>统一资源管理平台</title> <title>统一资源管理平台</title>
<script src="/center_resource/lib/tinymce@6/tinymce.min.js"></script> <script src="/center_resource/lib/tinymce@6/tinymce.min.js"></script>
......
...@@ -43,27 +43,11 @@ ...@@ -43,27 +43,11 @@
"sass": "^1.70.0", "sass": "^1.70.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"unplugin-auto-import": "^21.0.0", "unplugin-auto-import": "^21.0.0",
"vite": "^8.0.1", "vite": "^8.0.2",
"vite-plugin-checker": "^0.12.0",
"vite-plugin-mkcert": "^1.17.10", "vite-plugin-mkcert": "^1.17.10",
"vue-tsc": "^3.2.6" "vue-tsc": "^3.2.6"
} }
}, },
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
...@@ -348,9 +332,9 @@ ...@@ -348,9 +332,9 @@
} }
}, },
"node_modules/@oxc-project/types": { "node_modules/@oxc-project/types": {
"version": "0.120.0", "version": "0.122.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
"integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
...@@ -368,9 +352,9 @@ ...@@ -368,9 +352,9 @@
} }
}, },
"node_modules/@rolldown/binding-android-arm64": { "node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.11.tgz",
"integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", "integrity": "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
...@@ -385,9 +369,9 @@ ...@@ -385,9 +369,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-arm64": { "node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.11.tgz",
"integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", "integrity": "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
...@@ -402,9 +386,9 @@ ...@@ -402,9 +386,9 @@
} }
}, },
"node_modules/@rolldown/binding-darwin-x64": { "node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.11.tgz",
"integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", "integrity": "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
...@@ -419,9 +403,9 @@ ...@@ -419,9 +403,9 @@
} }
}, },
"node_modules/@rolldown/binding-freebsd-x64": { "node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.11.tgz",
"integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", "integrity": "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
...@@ -436,9 +420,9 @@ ...@@ -436,9 +420,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm-gnueabihf": { "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.11.tgz",
"integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", "integrity": "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
...@@ -453,9 +437,9 @@ ...@@ -453,9 +437,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-gnu": { "node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.11.tgz",
"integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", "integrity": "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
...@@ -473,9 +457,9 @@ ...@@ -473,9 +457,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-arm64-musl": { "node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.11.tgz",
"integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", "integrity": "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
...@@ -493,9 +477,9 @@ ...@@ -493,9 +477,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-ppc64-gnu": { "node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.11.tgz",
"integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", "integrity": "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
...@@ -513,9 +497,9 @@ ...@@ -513,9 +497,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-s390x-gnu": { "node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.11.tgz",
"integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", "integrity": "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
...@@ -533,9 +517,9 @@ ...@@ -533,9 +517,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-gnu": { "node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.11.tgz",
"integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", "integrity": "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
...@@ -553,9 +537,9 @@ ...@@ -553,9 +537,9 @@
} }
}, },
"node_modules/@rolldown/binding-linux-x64-musl": { "node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.11.tgz",
"integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", "integrity": "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
...@@ -573,9 +557,9 @@ ...@@ -573,9 +557,9 @@
} }
}, },
"node_modules/@rolldown/binding-openharmony-arm64": { "node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.11.tgz",
"integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", "integrity": "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
...@@ -590,9 +574,9 @@ ...@@ -590,9 +574,9 @@
} }
}, },
"node_modules/@rolldown/binding-wasm32-wasi": { "node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.11.tgz",
"integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", "integrity": "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==",
"cpu": [ "cpu": [
"wasm32" "wasm32"
], ],
...@@ -607,9 +591,9 @@ ...@@ -607,9 +591,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-arm64-msvc": { "node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.11.tgz",
"integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", "integrity": "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
...@@ -624,9 +608,9 @@ ...@@ -624,9 +608,9 @@
} }
}, },
"node_modules/@rolldown/binding-win32-x64-msvc": { "node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.11.tgz",
"integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", "integrity": "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
...@@ -1668,22 +1652,6 @@ ...@@ -1668,22 +1652,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/codepage": { "node_modules/codepage": {
"version": "1.15.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
...@@ -3165,13 +3133,6 @@ ...@@ -3165,13 +3133,6 @@
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==",
"dev": true "dev": true
}, },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
...@@ -3860,36 +3821,6 @@ ...@@ -3860,36 +3821,6 @@
"resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.1.2.tgz", "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.1.2.tgz",
"integrity": "sha512-scX83plWJXYH1J4+BhAuIHadROzxX0UBF3+HuZNY2Ks8BciE7tSTQ+5JhTsvzjaO0/EJdm4JBGrfObKxFf3Png==" "integrity": "sha512-scX83plWJXYH1J4+BhAuIHadROzxX0UBF3+HuZNY2Ks8BciE7tSTQ+5JhTsvzjaO0/EJdm4JBGrfObKxFf3Png=="
}, },
"node_modules/npm-run-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
"integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^4.0.0",
"unicorn-magic": "^0.3.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/npm-run-path/node_modules/path-key": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/nth-check": { "node_modules/nth-check": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
...@@ -4349,20 +4280,6 @@ ...@@ -4349,20 +4280,6 @@
"util-deprecate": "~1.0.1" "util-deprecate": "~1.0.1"
} }
}, },
"node_modules/readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 14.18.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/regexpp": { "node_modules/regexpp": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
...@@ -4410,14 +4327,14 @@ ...@@ -4410,14 +4327,14 @@
} }
}, },
"node_modules/rolldown": { "node_modules/rolldown": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.11.tgz",
"integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", "integrity": "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@oxc-project/types": "=0.120.0", "@oxc-project/types": "=0.122.0",
"@rolldown/pluginutils": "1.0.0-rc.10" "@rolldown/pluginutils": "1.0.0-rc.11"
}, },
"bin": { "bin": {
"rolldown": "bin/cli.mjs" "rolldown": "bin/cli.mjs"
...@@ -4426,27 +4343,27 @@ ...@@ -4426,27 +4343,27 @@
"node": "^20.19.0 || >=22.12.0" "node": "^20.19.0 || >=22.12.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-rc.10", "@rolldown/binding-android-arm64": "1.0.0-rc.11",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-arm64": "1.0.0-rc.11",
"@rolldown/binding-darwin-x64": "1.0.0-rc.10", "@rolldown/binding-darwin-x64": "1.0.0-rc.11",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.10", "@rolldown/binding-freebsd-x64": "1.0.0-rc.11",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11"
} }
}, },
"node_modules/rolldown/node_modules/@rolldown/pluginutils": { "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz",
"integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", "integrity": "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
...@@ -4859,13 +4776,6 @@ ...@@ -4859,13 +4776,6 @@
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
"dev": true "dev": true
}, },
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"dev": true,
"license": "MIT"
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
...@@ -5031,19 +4941,6 @@ ...@@ -5031,19 +4941,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/unicorn-magic": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/unimport": { "node_modules/unimport": {
"version": "5.7.0", "version": "5.7.0",
"resolved": "https://registry.npmjs.org/unimport/-/unimport-5.7.0.tgz", "resolved": "https://registry.npmjs.org/unimport/-/unimport-5.7.0.tgz",
...@@ -5336,16 +5233,16 @@ ...@@ -5336,16 +5233,16 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "8.0.1", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.2.tgz",
"integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", "integrity": "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"picomatch": "^4.0.3", "picomatch": "^4.0.3",
"postcss": "^8.5.8", "postcss": "^8.5.8",
"rolldown": "1.0.0-rc.10", "rolldown": "1.0.0-rc.11",
"tinyglobby": "^0.2.15" "tinyglobby": "^0.2.15"
}, },
"bin": { "bin": {
...@@ -5413,84 +5310,6 @@ ...@@ -5413,84 +5310,6 @@
} }
} }
}, },
"node_modules/vite-plugin-checker": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.12.0.tgz",
"integrity": "sha512-CmdZdDOGss7kdQwv73UyVgLPv0FVYe5czAgnmRX2oKljgEvSrODGuClaV3PDR2+3ou7N/OKGauDDBjy2MB07Rg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"chokidar": "^4.0.3",
"npm-run-path": "^6.0.0",
"picocolors": "^1.1.1",
"picomatch": "^4.0.3",
"tiny-invariant": "^1.3.3",
"tinyglobby": "^0.2.15",
"vscode-uri": "^3.1.0"
},
"engines": {
"node": ">=16.11"
},
"peerDependencies": {
"@biomejs/biome": ">=1.7",
"eslint": ">=9.39.1",
"meow": "^13.2.0",
"optionator": "^0.9.4",
"oxlint": ">=1",
"stylelint": ">=16",
"typescript": "*",
"vite": ">=5.4.21",
"vls": "*",
"vti": "*",
"vue-tsc": "~2.2.10 || ^3.0.0"
},
"peerDependenciesMeta": {
"@biomejs/biome": {
"optional": true
},
"eslint": {
"optional": true
},
"meow": {
"optional": true
},
"optionator": {
"optional": true
},
"oxlint": {
"optional": true
},
"stylelint": {
"optional": true
},
"typescript": {
"optional": true
},
"vls": {
"optional": true
},
"vti": {
"optional": true
},
"vue-tsc": {
"optional": true
}
}
},
"node_modules/vite-plugin-checker/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/vite-plugin-mkcert": { "node_modules/vite-plugin-mkcert": {
"version": "1.17.10", "version": "1.17.10",
"resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.17.10.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.17.10.tgz",
...@@ -5861,17 +5680,6 @@ ...@@ -5861,17 +5680,6 @@
} }
}, },
"dependencies": { "dependencies": {
"@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"dev": true,
"requires": {
"@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
}
},
"@babel/helper-string-parser": { "@babel/helper-string-parser": {
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
...@@ -6100,9 +5908,9 @@ ...@@ -6100,9 +5908,9 @@
} }
}, },
"@oxc-project/types": { "@oxc-project/types": {
"version": "0.120.0", "version": "0.122.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
"integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
"dev": true "dev": true
}, },
"@popperjs/core": { "@popperjs/core": {
...@@ -6111,93 +5919,93 @@ ...@@ -6111,93 +5919,93 @@
"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==" "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ=="
}, },
"@rolldown/binding-android-arm64": { "@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.11.tgz",
"integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", "integrity": "sha512-SJ+/g+xNnOh6NqYxD0V3uVN4W3VfnrGsC9/hoglicgTNfABFG9JjISvkkU0dNY84MNHLWyOgxP9v9Y9pX4S7+A==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"@rolldown/binding-darwin-arm64": { "@rolldown/binding-darwin-arm64": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.11.tgz",
"integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", "integrity": "sha512-7WQgR8SfOPwmDZGFkThUvsmd/nwAWv91oCO4I5LS7RKrssPZmOt7jONN0cW17ydGC1n/+puol1IpoieKqQidmg==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"@rolldown/binding-darwin-x64": { "@rolldown/binding-darwin-x64": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.11.tgz",
"integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", "integrity": "sha512-39Ks6UvIHq4rEogIfQBoBRusj0Q0nPVWIvqmwBLaT6aqQGIakHdESBVOPRRLacy4WwUPIx4ZKzfZ9PMW+IeyUQ==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"@rolldown/binding-freebsd-x64": { "@rolldown/binding-freebsd-x64": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.11.tgz",
"integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", "integrity": "sha512-jfsm0ZHfhiqrvWjJAmzsqiIFPz5e7mAoCOPBNTcNgkiid/LaFKiq92+0ojH+nmJmKYkre4t71BWXUZDNp7vsag==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"@rolldown/binding-linux-arm-gnueabihf": { "@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.11.tgz",
"integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", "integrity": "sha512-zjQaUtSyq1nVe3nxmlSCuR96T1LPlpvmJ0SZy0WJFEsV4kFbXcq2u68L4E6O0XeFj4aex9bEauqjW8UQBeAvfQ==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"@rolldown/binding-linux-arm64-gnu": { "@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.11.tgz",
"integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", "integrity": "sha512-WMW1yE6IOnehTcFE9eipFkm3XN63zypWlrJQ2iF7NrQ9b2LDRjumFoOGJE8RJJTJCTBAdmLMnJ8uVitACUUo1Q==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"@rolldown/binding-linux-arm64-musl": { "@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.11.tgz",
"integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", "integrity": "sha512-jfndI9tsfm4APzjNt6QdBkYwre5lRPUgHeDHoI7ydKUuJvz3lZeCfMsI56BZj+7BYqiKsJm7cfd/6KYV7ubrBg==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"@rolldown/binding-linux-ppc64-gnu": { "@rolldown/binding-linux-ppc64-gnu": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.11.tgz",
"integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", "integrity": "sha512-ZlFgw46NOAGMgcdvdYwAGu2Q+SLFA9LzbJLW+iyMOJyhj5wk6P3KEE9Gct4xWwSzFoPI7JCdYmYMzVtlgQ+zfw==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"@rolldown/binding-linux-s390x-gnu": { "@rolldown/binding-linux-s390x-gnu": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.11.tgz",
"integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", "integrity": "sha512-hIOYmuT6ofM4K04XAZd3OzMySEO4K0/nc9+jmNcxNAxRi6c5UWpqfw3KMFV4MVFWL+jQsSh+bGw2VqmaPMTLyw==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"@rolldown/binding-linux-x64-gnu": { "@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.11.tgz",
"integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", "integrity": "sha512-qXBQQO9OvkjjQPLdUVr7Nr2t3QTZI7s4KZtfw7HzBgjbmAPSFwSv4rmET9lLSgq3rH/ndA3ngv3Qb8l2njoPNA==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"@rolldown/binding-linux-x64-musl": { "@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.11.tgz",
"integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", "integrity": "sha512-/tpFfoSTzUkH9LPY+cYbqZBDyyX62w5fICq9qzsHLL8uTI6BHip3Q9Uzft0wylk/i8OOwKik8OxW+QAhDmzwmg==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"@rolldown/binding-openharmony-arm64": { "@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.11.tgz",
"integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", "integrity": "sha512-mcp3Rio2w72IvdZG0oQ4bM2c2oumtwHfUfKncUM6zGgz0KgPz4YmDPQfnXEiY5t3+KD/i8HG2rOB/LxdmieK2g==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"@rolldown/binding-wasm32-wasi": { "@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.11.tgz",
"integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", "integrity": "sha512-LXk5Hii1Ph9asuGRjBuz8TUxdc1lWzB7nyfdoRgI0WGPZKmCxvlKk8KfYysqtr4MfGElu/f/pEQRh8fcEgkrWw==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"requires": { "requires": {
...@@ -6205,16 +6013,16 @@ ...@@ -6205,16 +6013,16 @@
} }
}, },
"@rolldown/binding-win32-arm64-msvc": { "@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.11.tgz",
"integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", "integrity": "sha512-dDwf5otnx0XgRY1yqxOC4ITizcdzS/8cQ3goOWv3jFAo4F+xQYni+hnMuO6+LssHHdJW7+OCVL3CoU4ycnh35Q==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
"@rolldown/binding-win32-x64-msvc": { "@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.11.tgz",
"integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", "integrity": "sha512-LN4/skhSggybX71ews7dAj6r2geaMJfm3kMbK2KhFMg9B10AZXnKoLCVVgzhMHL0S+aKtr4p8QbAW8k+w95bAA==",
"dev": true, "dev": true,
"optional": true "optional": true
}, },
...@@ -6974,15 +6782,6 @@ ...@@ -6974,15 +6782,6 @@
"integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==",
"dev": true "dev": true
}, },
"chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"requires": {
"readdirp": "^4.0.1"
}
},
"codepage": { "codepage": {
"version": "1.15.0", "version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
...@@ -8097,12 +7896,6 @@ ...@@ -8097,12 +7896,6 @@
"integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==",
"dev": true "dev": true
}, },
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"js-yaml": { "js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
...@@ -8529,24 +8322,6 @@ ...@@ -8529,24 +8322,6 @@
"resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.1.2.tgz", "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.1.2.tgz",
"integrity": "sha512-scX83plWJXYH1J4+BhAuIHadROzxX0UBF3+HuZNY2Ks8BciE7tSTQ+5JhTsvzjaO0/EJdm4JBGrfObKxFf3Png==" "integrity": "sha512-scX83plWJXYH1J4+BhAuIHadROzxX0UBF3+HuZNY2Ks8BciE7tSTQ+5JhTsvzjaO0/EJdm4JBGrfObKxFf3Png=="
}, },
"npm-run-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
"integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
"dev": true,
"requires": {
"path-key": "^4.0.0",
"unicorn-magic": "^0.3.0"
},
"dependencies": {
"path-key": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
"integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
"dev": true
}
}
},
"nth-check": { "nth-check": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
...@@ -8869,12 +8644,6 @@ ...@@ -8869,12 +8644,6 @@
"util-deprecate": "~1.0.1" "util-deprecate": "~1.0.1"
} }
}, },
"readdirp": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
"dev": true
},
"regexpp": { "regexpp": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz",
...@@ -8903,34 +8672,34 @@ ...@@ -8903,34 +8672,34 @@
} }
}, },
"rolldown": { "rolldown": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.11.tgz",
"integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", "integrity": "sha512-NRjoKMusSjfRbSYiH3VSumlkgFe7kYAa3pzVOsVYVFY3zb5d7nS+a3KGQ7hJKXuYWbzJKPVQ9Wxq2UvyK+ENpw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@oxc-project/types": "=0.120.0", "@oxc-project/types": "=0.122.0",
"@rolldown/binding-android-arm64": "1.0.0-rc.10", "@rolldown/binding-android-arm64": "1.0.0-rc.11",
"@rolldown/binding-darwin-arm64": "1.0.0-rc.10", "@rolldown/binding-darwin-arm64": "1.0.0-rc.11",
"@rolldown/binding-darwin-x64": "1.0.0-rc.10", "@rolldown/binding-darwin-x64": "1.0.0-rc.11",
"@rolldown/binding-freebsd-x64": "1.0.0-rc.10", "@rolldown/binding-freebsd-x64": "1.0.0-rc.11",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.11",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.11",
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.11",
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.11",
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.11",
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.11",
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.11",
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.11",
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.11",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.11",
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.11",
"@rolldown/pluginutils": "1.0.0-rc.10" "@rolldown/pluginutils": "1.0.0-rc.11"
}, },
"dependencies": { "dependencies": {
"@rolldown/pluginutils": { "@rolldown/pluginutils": {
"version": "1.0.0-rc.10", "version": "1.0.0-rc.11",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.11.tgz",
"integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", "integrity": "sha512-xQO9vbwBecJRv9EUcQ/y0dzSTJgA7Q6UVN7xp6B81+tBGSLVAK03yJ9NkJaUA7JFD91kbjxRSC/mDnmvXzbHoQ==",
"dev": true "dev": true
} }
} }
...@@ -9243,12 +9012,6 @@ ...@@ -9243,12 +9012,6 @@
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
"dev": true "dev": true
}, },
"tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"dev": true
},
"tinyglobby": { "tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
...@@ -9357,12 +9120,6 @@ ...@@ -9357,12 +9120,6 @@
"extend-shallow": "^2.0.1" "extend-shallow": "^2.0.1"
} }
}, },
"unicorn-magic": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
"integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
"dev": true
},
"unimport": { "unimport": {
"version": "5.7.0", "version": "5.7.0",
"resolved": "https://registry.npmjs.org/unimport/-/unimport-5.7.0.tgz", "resolved": "https://registry.npmjs.org/unimport/-/unimport-5.7.0.tgz",
...@@ -9584,16 +9341,16 @@ ...@@ -9584,16 +9341,16 @@
} }
}, },
"vite": { "vite": {
"version": "8.0.1", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.2.tgz",
"integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", "integrity": "sha512-1gFhNi+bHhRE/qKZOJXACm6tX4bA3Isy9KuKF15AgSRuRazNBOJfdDemPBU16/mpMxApDPrWvZ08DcLPEoRnuA==",
"dev": true, "dev": true,
"requires": { "requires": {
"fsevents": "~2.3.3", "fsevents": "~2.3.3",
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"picomatch": "^4.0.3", "picomatch": "^4.0.3",
"postcss": "^8.5.8", "postcss": "^8.5.8",
"rolldown": "1.0.0-rc.10", "rolldown": "1.0.0-rc.11",
"tinyglobby": "^0.2.15" "tinyglobby": "^0.2.15"
}, },
"dependencies": { "dependencies": {
...@@ -9605,30 +9362,6 @@ ...@@ -9605,30 +9362,6 @@
} }
} }
}, },
"vite-plugin-checker": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.12.0.tgz",
"integrity": "sha512-CmdZdDOGss7kdQwv73UyVgLPv0FVYe5czAgnmRX2oKljgEvSrODGuClaV3PDR2+3ou7N/OKGauDDBjy2MB07Rg==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.27.1",
"chokidar": "^4.0.3",
"npm-run-path": "^6.0.0",
"picocolors": "^1.1.1",
"picomatch": "^4.0.3",
"tiny-invariant": "^1.3.3",
"tinyglobby": "^0.2.15",
"vscode-uri": "^3.1.0"
},
"dependencies": {
"picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true
}
}
},
"vite-plugin-mkcert": { "vite-plugin-mkcert": {
"version": "1.17.10", "version": "1.17.10",
"resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.17.10.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.17.10.tgz",
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
"scripts": { "scripts": {
"dev": "vite --mode dev", "dev": "vite --mode dev",
"build": "vue-tsc --noEmit && vite build --mode prod && npm run deploy", "build": "vue-tsc --noEmit && vite build --mode prod && npm run deploy",
"build:test": "vue-tsc --noEmit && vite build --mode test", "build:school": "vue-tsc --noEmit && vite build --mode school",
"build:pre": "vue-tsc --noEmit && vite build --mode pre", "build:pre": "vue-tsc --noEmit && vite build --mode pre",
"preview": "vite preview --port 4173", "preview": "vite preview --port 4173",
"typecheck": "vue-tsc --noEmit", "typecheck": "vue-tsc --noEmit",
...@@ -49,8 +49,7 @@ ...@@ -49,8 +49,7 @@
"sass": "^1.70.0", "sass": "^1.70.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"unplugin-auto-import": "^21.0.0", "unplugin-auto-import": "^21.0.0",
"vite": "^8.0.1", "vite": "^8.0.2",
"vite-plugin-checker": "^0.12.0",
"vite-plugin-mkcert": "^1.17.10", "vite-plugin-mkcert": "^1.17.10",
"vue-tsc": "^3.2.6" "vue-tsc": "^3.2.6"
} }
......
...@@ -25,7 +25,7 @@ export function uploadFile(data: Record<string, any>) { ...@@ -25,7 +25,7 @@ export function uploadFile(data: Record<string, any>) {
return httpRequest return httpRequest
.post(data.host || 'https://webapp-pub.ezijing.com', data, { .post(data.host || 'https://webapp-pub.ezijing.com', data, {
withCredentials: false, withCredentials: false,
headers: { 'Content-Type': 'multipart/form-data' } headers: { 'Content-Type': 'multipart/form-data' },
}) })
.then(() => data) .then(() => data)
} }
...@@ -62,3 +62,38 @@ export function getProjectList(params: { organization_id?: string; project_id?: ...@@ -62,3 +62,38 @@ export function getProjectList(params: { organization_id?: string; project_id?:
export function getQuestionCategory(params: { project_tag: string }) { export function getQuestionCategory(params: { project_tag: string }) {
return httpRequest.get(`/api/qbs/admin/v2/question-category/tree/${params.project_tag}`, { params }) return httpRequest.get(`/api/qbs/admin/v2/question-category/tree/${params.project_tag}`, { params })
} }
// 获取分片大小和唯一文件名
export function getLocalFileChunk(params: { file_size: number; file_name: string }) {
return httpRequest.get('/api/lab/v1/common/file/chunk', { params })
}
// 上传每个分片前请求接口来获取当前文件是否超时,之前的分片是否被清理,如果被请求则拒绝处理。返回客户端错误码,让客户端户端不再续传剩余分片
// 1文件被清理 0文件未被清理
export function checkLocalFile(params: { file_name: string }) {
return httpRequest.get('/api/lab/v1/common/file/check', { params })
}
// 上传文件
export function uploadLocalFile(
data: {
file: File | Blob
file_name: string
is_continuingly?: number
now_package_num: number
total_package_num: number
},
options = {},
) {
return httpRequest.post(
'/api/lab/v1/common/file/upload',
data,
Object.assign(
{
withCredentials: false,
headers: { 'Content-Type': 'multipart/form-data' },
},
options,
),
)
}
...@@ -20,7 +20,8 @@ import { ...@@ -20,7 +20,8 @@ import {
QuestionFilled, QuestionFilled,
EditPen, EditPen,
DataAnalysis, DataAnalysis,
ChatDotRound ChatDotRound,
Setting,
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
export const menus: IMenuItem[] = [ export const menus: IMenuItem[] = [
{ {
...@@ -32,37 +33,37 @@ export const menus: IMenuItem[] = [ ...@@ -32,37 +33,37 @@ export const menus: IMenuItem[] = [
tag: 'v1-resource-video-list', tag: 'v1-resource-video-list',
icon: VideoCamera, icon: VideoCamera,
name: '视频', name: '视频',
path: '/resource/video' path: '/resource/video',
}, },
{ {
tag: 'v1-resource-courseware-list', tag: 'v1-resource-courseware-list',
icon: Suitcase, icon: Suitcase,
name: '课件', name: '课件',
path: '/resource/courseware' path: '/resource/courseware',
}, },
{ {
tag: 'v1-resource-lesson-plan-list', tag: 'v1-resource-lesson-plan-list',
icon: FolderOpened, icon: FolderOpened,
name: '教案', name: '教案',
path: '/resource/lessonplan' path: '/resource/lessonplan',
}, },
{ {
tag: 'v1-resource-other-information-list', tag: 'v1-resource-other-information-list',
icon: Files, icon: Files,
name: '其他资料', name: '其他资料',
path: '/resource/other' path: '/resource/other',
}, },
{ {
icon: Collection, icon: Collection,
name: '题库管理', name: '题库管理',
path: import.meta.env.VITE_QA_CENTER_URL + '/question/list?project_tag=resourse_ci' path: import.meta.env.VITE_QA_CENTER_URL + '/question/list?project_tag=resourse_ci',
}, },
{ {
icon: ToiletPaper, icon: ToiletPaper,
name: '试卷管理', name: '试卷管理',
path: import.meta.env.VITE_QA_CENTER_URL + '/paper/list?project_tag=resourse_ci' path: import.meta.env.VITE_QA_CENTER_URL + '/paper/list?project_tag=resourse_ci',
} },
] ],
}, },
{ {
tag: 'v1-course', tag: 'v1-course',
...@@ -73,15 +74,15 @@ export const menus: IMenuItem[] = [ ...@@ -73,15 +74,15 @@ export const menus: IMenuItem[] = [
tag: 'v1-course-list', tag: 'v1-course-list',
icon: Monitor, icon: Monitor,
name: '我的课程', name: '我的课程',
path: '/course/my' path: '/course/my',
}, },
{ {
tag: 'v1-course-create', tag: 'v1-course-create',
icon: Edit, icon: Edit,
name: '新建课程', name: '新建课程',
path: '/course/update-course' path: '/course/update-course',
} },
] ],
}, },
{ {
tag: 'v1-learning', tag: 'v1-learning',
...@@ -92,51 +93,51 @@ export const menus: IMenuItem[] = [ ...@@ -92,51 +93,51 @@ export const menus: IMenuItem[] = [
tag: 'v1-backend-lecturer-list', tag: 'v1-backend-lecturer-list',
icon: UserFilled, icon: UserFilled,
name: '讲师管理', name: '讲师管理',
path: '/admin/teacher' path: '/admin/teacher',
}, },
{ {
tag: 'v1-learning-teacher-list', tag: 'v1-learning-teacher-list',
icon: School, icon: School,
name: '教工用户管理', name: '教工用户管理',
path: '/admin/staff' path: '/admin/staff',
}, },
{ {
tag: 'v1-learning-student-list', tag: 'v1-learning-student-list',
icon: User, icon: User,
name: '学生管理', name: '学生管理',
path: '/admin/student' path: '/admin/student',
}, },
{ {
tag: 'v1-backend-specialty-list', tag: 'v1-backend-specialty-list',
icon: Promotion, icon: Promotion,
name: '专业管理', name: '专业管理',
path: '/admin/pro' path: '/admin/pro',
}, },
{ {
tag: 'v1-learning-class-list', tag: 'v1-learning-class-list',
icon: School, icon: School,
name: '班级管理', name: '班级管理',
path: '/admin/class' path: '/admin/class',
}, },
{ {
tag: 'v1-learning-semester-list', tag: 'v1-learning-semester-list',
icon: Guide, icon: Guide,
name: '学期管理', name: '学期管理',
path: '/admin/semester' path: '/admin/semester',
}, },
{ {
tag: 'v1-backend-category-list', tag: 'v1-backend-category-list',
icon: Filter, icon: Filter,
name: '类别管理', name: '类别管理',
path: '/admin/category' path: '/admin/category',
}, },
{ {
icon: Coordinate, icon: Coordinate,
name: '资源审核管理', name: '资源审核管理',
path: '/admin/audit' path: '/admin/audit',
} },
] ],
}, },
{ {
tag: 'v1-backend', tag: 'v1-backend',
...@@ -147,21 +148,30 @@ export const menus: IMenuItem[] = [ ...@@ -147,21 +148,30 @@ export const menus: IMenuItem[] = [
tag: 'v1-backend-data-dictionary-list', tag: 'v1-backend-data-dictionary-list',
icon: Notebook, icon: Notebook,
name: '数据字典', name: '数据字典',
path: '/system/dictionary' path: '/system/dictionary',
}, },
{ {
tag: 'v1-backend-cover-list', tag: 'v1-backend-cover-list',
icon: Picture, icon: Picture,
name: '封面管理', name: '封面管理',
path: '/system/cover' path: '/system/cover',
}, },
{ {
tag: 'v1-backend-suggestion-list', tag: 'v1-backend-suggestion-list',
icon: ChatDotRound, icon: ChatDotRound,
name: ' 投诉建议管理', name: ' 投诉建议管理',
path: '/system/suggestion' path: '/system/suggestion',
} },
] ...(import.meta.env.MODE === 'school'
? [
{
icon: Setting,
name: '大模型配置',
path: '/system/llm',
},
]
: []),
],
}, },
{ {
tag: 'v1-teaching', tag: 'v1-teaching',
...@@ -172,44 +182,53 @@ export const menus: IMenuItem[] = [ ...@@ -172,44 +182,53 @@ export const menus: IMenuItem[] = [
tag: 'v1-teaching-discussion', tag: 'v1-teaching-discussion',
icon: QuestionFilled, icon: QuestionFilled,
name: '帖子管理', name: '帖子管理',
path: '/teach/posts' path: '/teach/posts',
}, },
{ {
tag: 'v1-teaching-paper-paper-list', tag: 'v1-teaching-paper-paper-list',
icon: EditPen, icon: EditPen,
name: '批改试卷', name: '批改试卷',
path: '/teach/exam' path: '/teach/exam',
}, },
{ {
tag: 'v1-teaching-job-list', tag: 'v1-teaching-job-list',
icon: Edit, icon: Edit,
name: '批改大作业', name: '批改大作业',
path: '/teach/work' path: '/teach/work',
}, },
{ ...(import.meta.env.MODE === 'school'
tag: '', ? [
icon: DataAnalysis, {
name: '课程资源数据画像', icon: DataAnalysis,
path: name: '课程资源数据画像',
import.meta.env.VITE_BI_URL + // path:
'/bi/?proc=1&action=viewer&hback=true&isInPreview=true&db=!7d2b!!8346!!6559!!80b2!e-SaaS!2f!!8d44!!6e90!!6570!!636e!!753b!!50cf!.db&platform=PC&browserType=chrome' // import.meta.env.VITE_BI_URL +
}, // '/bi/?proc=1&action=viewer&hback=true&isInPreview=true&db=!7d2b!!8346!!6559!!80b2!e-SaaS!2f!!8d44!!6e90!!6570!!636e!!753b!!50cf!.db&platform=PC&browserType=chrome',
{ path: '/teach/chart/resource',
tag: '', },
icon: DataAnalysis, {
name: '在线学习数据画像', icon: DataAnalysis,
path: name: '在线学习数据画像',
import.meta.env.VITE_BI_URL + // path:
'/bi/?proc=1&action=viewer&hback=true&isInPreview=true&db=!7d2b!!8346!!6559!!80b2!e-SaaS!2f!!5b66!!4e60!!884c!!4e3a!!753b!!50cf!.db&platform=PC&browserType=chrome' // import.meta.env.VITE_BI_URL +
}, // '/bi/?proc=1&action=viewer&hback=true&isInPreview=true&db=!7d2b!!8346!!6559!!80b2!e-SaaS!2f!!5b66!!4e60!!884c!!4e3a!!753b!!50cf!.db&platform=PC&browserType=chrome',
{ path: '/teach/chart/learning',
tag: '', },
icon: DataAnalysis, {
name: '生源地分布', icon: DataAnalysis,
path: name: '数据可视化',
import.meta.env.VITE_BI_URL + path: '/teach/chart/visualization',
'/bi/?proc=1&action=viewer&hback=true&isInPreview=true&db=!7d2b!!8346!!6559!!80b2!e-SaaS!2f!!751f!!6e90!!5730!!5206!!5e03!.db&platform=PC&browserType=chrome' },
} {
] icon: DataAnalysis,
} name: '生源地分布',
// path:
// import.meta.env.VITE_BI_URL +
// '/bi/?proc=1&action=viewer&hback=true&isInPreview=true&db=!7d2b!!8346!!6559!!80b2!e-SaaS!2f!!751f!!6e90!!5730!!5206!!5e03!.db&platform=PC&browserType=chrome',
path: '/teach/chart/origin',
},
]
: []),
],
},
] ]
<script lang="ts" setup> <script lang="ts" setup>
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue' import { Plus } from '@element-plus/icons-vue'
import type { UploadProps, UploadUserFile } from 'element-plus' import type { UploadProps, UploadRequestOptions, UploadUserFile } from 'element-plus'
import md5 from 'blueimp-md5' import { upload } from '@/utils/upload'
import { getSignature } from '@/api/base'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
...@@ -20,8 +19,6 @@ const props = withDefaults( ...@@ -20,8 +19,6 @@ const props = withDefaults(
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const uploadData = ref()
const fileList = ref<UploadUserFile[]>([]) const fileList = ref<UploadUserFile[]>([])
watch( watch(
...@@ -35,23 +32,23 @@ const showFileList = computed(() => { ...@@ -35,23 +32,23 @@ const showFileList = computed(() => {
return Array.isArray(props.modelValue) return Array.isArray(props.modelValue)
}) })
// 上传之前
const handleBeforeUpload = async (file: any) => { const handleBeforeUpload = async (file: any) => {
// Keep a pseudo-url for legacy validators that inspect file.url/file name before upload.
file.url = file.name
const fileName = file.name const fileName = file.name
const key = props.prefix + md5(fileName + new Date().getTime()) + '/' + fileName
const response: Record<string, any> = await getSignature()
uploadData.value = {
key,
host: response.host,
OSSAccessKeyId: response.accessid,
policy: response.policy,
signature: response.signature,
success_action_status: '200',
url: `${response.host}/${key}`
}
file.url = `${response.host}/${key}`
if (props.beforeUploadFiles) { if (props.beforeUploadFiles) {
return props.beforeUploadFiles(file) return props.beforeUploadFiles({ ...file, url: fileName })
}
}
const handleUploadRequest = async (option: UploadRequestOptions) => {
try {
const url = await upload(option.file)
;(option.file as any).url = url
;(option.file as any).raw = Object.assign((option.file as any).raw || {}, { url })
option.onSuccess({ url })
} catch (error: any) {
option.onError(error)
} }
} }
...@@ -73,6 +70,7 @@ const handleSuccess: UploadProps['onSuccess'] = (response, file: any, files: any ...@@ -73,6 +70,7 @@ const handleSuccess: UploadProps['onSuccess'] = (response, file: any, files: any
} else { } else {
emit('update:modelValue', file.raw.url) emit('update:modelValue', file.raw.url)
} }
props.onChange?.(file, files)
} }
// 上传限制 // 上传限制
...@@ -102,8 +100,8 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => { ...@@ -102,8 +100,8 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => {
<template> <template>
<el-upload <el-upload
:action="uploadData?.host" action="#"
:data="uploadData" :http-request="handleUploadRequest"
:show-file-list="showFileList" :show-file-list="showFileList"
:before-upload="handleBeforeUpload" :before-upload="handleBeforeUpload"
:on-exceed="handleExceed" :on-exceed="handleExceed"
......
<script setup lang="ts"> <script setup lang="ts">
const props = defineProps({ import { computed } from 'vue'
item: { import { getOnlinePreviewUrl } from '@/utils/util'
type: Object,
require: true interface PreviewFileItem {
url?: string
name?: string
}
const props = defineProps<{
item?: PreviewFileItem
url?: string
name?: string
}>()
const previewItem = computed<PreviewFileItem>(() => {
return {
url: props.item?.url || props.url || '',
name: props.item?.name || props.name || '',
} }
}) })
let isShowType = ref(1) const fileUrl = computed(() => previewItem.value.url || '')
// 判断用什么标签展示内容 const fileName = computed(() => previewItem.value.name || '')
if (props.item?.url?.indexOf('.pdf') !== -1 || props.item?.url?.indexOf('.txt') !== -1) {
isShowType.value = 2 const fileExtension = computed(() => {
} else if (props.item?.url?.indexOf('.mp4') !== -1) { const cleanUrl = fileUrl.value.split('?')[0].split('#')[0].toLowerCase()
isShowType.value = 3 const matches = cleanUrl.match(/\.([a-z0-9]+)$/)
} else if (props.item?.url?.indexOf('.mp3') !== -1) { return matches?.[1] || ''
isShowType.value = 4 })
} else if (
props.item?.url?.indexOf('.png') !== -1 || const previewType = computed(() => {
props.item?.url?.indexOf('.jpg') !== -1 || if (['pdf', 'txt'].includes(fileExtension.value)) return 'embed'
props.item?.url?.indexOf('.jpeg') !== -1 if (fileExtension.value === 'mp4') return 'video'
) { if (fileExtension.value === 'mp3') return 'audio'
isShowType.value = 5 if (['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(fileExtension.value)) return 'image'
} else if (props.item?.url?.indexOf('.rar') !== -1 || props.item?.url?.indexOf('.zip') !== -1) { if (['rar', 'zip'].includes(fileExtension.value)) return 'archive'
isShowType.value = 6 return 'iframe'
} })
const archiveIcon = computed(() => {
if (fileExtension.value === 'rar') return '/center_resource/rar.png'
if (fileExtension.value === 'zip') return '/center_resource/zip.png'
return ''
})
</script> </script>
<template> <template>
<el-card> <el-card>
<div class="max-w-h"> <div class="preview-container">
<iframe <iframe v-if="previewType === 'iframe'" :src="getOnlinePreviewUrl(fileUrl)" allowfullscreen>
id="iframe" {{ fileName }}
v-if="isShowType === 1" </iframe>
:src="`https://view.officeapps.live.com/op/view.aspx?src=${props.item?.url}`"
>{{ item?.name }}</iframe <embed v-else-if="previewType === 'embed'" :src="fileUrl" />
>
<embed :src="props.item?.url" v-else-if="isShowType === 2" /> <video v-else-if="previewType === 'video'" controls>
<video v-else-if="isShowType === 3" controls id="video"> <source :src="fileUrl" />
<source :src="props.item?.url" />
</video> </video>
<audio v-else-if="isShowType === 4" :src="props.item?.url" controls></audio>
<img v-else-if="isShowType === 5" :src="props.item?.url" />
<div v-else-if="isShowType === 6" class="zip_con">
<a :href="props.item?.url" style="color: #aa1941">
<img v-if="props.item?.url?.indexOf('.rar') !== -1" src="/center_resource/rar.png" class="img_zip" />
<img v-else-if="props.item?.url?.indexOf('.zip') !== -1" src="/center_resource/zip.png" class="img_zip" />
{{ props.item?.name }}
</a>
<div class="zip_tips">* 该文件格式暂不支持预览,可点击上方文件名下载</div> <audio v-else-if="previewType === 'audio'" :src="fileUrl" controls />
<img v-else-if="previewType === 'image'" :src="fileUrl" :alt="fileName" />
<div v-else class="archive-container">
<a :href="fileUrl" class="archive-link">
<img v-if="archiveIcon" :src="archiveIcon" class="archive-icon" />
{{ fileName || fileUrl }}
</a>
<div class="archive-tip">* 该文件格式暂不支持预览,可点击上方文件名下载</div>
</div> </div>
</div> </div>
</el-card> </el-card>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.max-w-h { .preview-container {
// max-width: 1200px;
width: 100%; width: 100%;
height: 600px; height: 600px;
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: stretch;
min-height: 0;
iframe, iframe,
embed, embed,
video { video,
img {
flex: 1;
width: 100%; width: 100%;
height: 100%; height: 100%;
}
.zip_con {
border: 1px solid #ccc;
width: 100%;
display: block; display: block;
display: flex; border: 0;
justify-content: center;
align-items: center;
flex-direction: column;
}
.img_zip {
width: 100px;
height: 100px;
margin: auto;
}
.zip_tips {
font-size: 14px;
margin-top: 10px;
} }
img { img {
width: 100%;
display: block; display: block;
object-fit: contain;
} }
} }
.archive-container {
width: 100%;
border: 1px solid #ccc;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.archive-link {
color: #aa1941;
display: flex;
flex-direction: column;
align-items: center;
}
.archive-icon {
width: 100px;
height: 100px;
margin: auto;
}
.archive-tip {
font-size: 14px;
margin-top: 10px;
}
</style> </style>
<script lang="ts"> <script lang="ts">
export default { export default {
name: 'AppAside' name: 'AppAside',
} }
</script> </script>
...@@ -10,10 +10,20 @@ import type { IMenuItem } from '@/types' ...@@ -10,10 +10,20 @@ import type { IMenuItem } from '@/types'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
function isUrl(path: string) {
return /^https?:\/\//.test(path)
}
function isRouteMatch(currentPath: string, targetPath: string) {
if (!targetPath || isUrl(targetPath)) return false
return currentPath === targetPath || currentPath.startsWith(`${targetPath}/`)
}
const menuList = computed<IMenuItem[]>(() => { const menuList = computed<IMenuItem[]>(() => {
const found = menus.find(item => route.fullPath.includes(item.path)) const found = menus.find((item) => isRouteMatch(route.path, item.path))
return found?.children || [] return found?.children || []
}) })
const defaultActive = computed(() => { const defaultActive = computed(() => {
// 扁平菜单 // 扁平菜单
const flatMenuList: IMenuItem[] = menuList.value.reduce((result: IMenuItem[], item) => { const flatMenuList: IMenuItem[] = menuList.value.reduce((result: IMenuItem[], item) => {
...@@ -23,16 +33,12 @@ const defaultActive = computed(() => { ...@@ -23,16 +33,12 @@ const defaultActive = computed(() => {
} }
return result return result
}, []) }, [])
const found = flatMenuList.reverse().find(item => { const found = flatMenuList.reverse().find((item) => {
return route.path.includes(item.path) return isRouteMatch(route.path, item.path)
}) })
return found ? found.path : '/' return found ? found.path : '/'
}) })
function isUrl(path: string) {
return /^https?:\/\//.test(path)
}
function handleClick(path: string) { function handleClick(path: string) {
if (isUrl(path)) { if (isUrl(path)) {
window.open(path) window.open(path)
...@@ -56,8 +62,7 @@ function handleClick(path: string) { ...@@ -56,8 +62,7 @@ function handleClick(path: string) {
v-for="subitem in item.children" v-for="subitem in item.children"
:key="subitem.path" :key="subitem.path"
v-permission="subitem.tag" v-permission="subitem.tag"
@click="handleClick(subitem.path)" @click="handleClick(subitem.path)">
>
{{ subitem.name }} {{ subitem.name }}
</el-menu-item> </el-menu-item>
</el-sub-menu> </el-sub-menu>
......
...@@ -7,7 +7,7 @@ import { menus } from '@/assets/menus' ...@@ -7,7 +7,7 @@ import { menus } from '@/assets/menus'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import type { IMenuItem } from '@/types' import type { IMenuItem } from '@/types'
withDefaults(defineProps<{ hasTitle?: boolean }>(), { withDefaults(defineProps<{ hasTitle?: boolean }>(), {
hasTitle: true hasTitle: true,
}) })
const route = useRoute() const route = useRoute()
...@@ -31,7 +31,7 @@ function genNavClassName(data: IMenuItem) { ...@@ -31,7 +31,7 @@ function genNavClassName(data: IMenuItem) {
<header class="app-header"> <header class="app-header">
<div class="app-header-left"> <div class="app-header-left">
<div class="logo"> <div class="logo">
<router-link to="/"><img src="https://webapp-pub.ezijing.com/website/base/logo_white.svg" /></router-link> <router-link to="/"><img src="/logo_white.svg" /></router-link>
</div> </div>
<h1 class="app-name">统一资源管理平台</h1> <h1 class="app-name">统一资源管理平台</h1>
</div> </div>
...@@ -48,7 +48,7 @@ function genNavClassName(data: IMenuItem) { ...@@ -48,7 +48,7 @@ function genNavClassName(data: IMenuItem) {
<div class="app-header-right"> <div class="app-header-right">
<el-dropdown v-if="userInfo"> <el-dropdown v-if="userInfo">
<div class="avatar"> <div class="avatar">
<img :src="userInfo.avatar || 'https://webapp-pub.ezijing.com/website/base/avatar.png'" /> <img :src="userInfo.avatar || '/avatar.png'" />
</div> </div>
<template #dropdown> <template #dropdown>
<el-dropdown-menu style="width: 280px"> <el-dropdown-menu style="width: 280px">
...@@ -161,7 +161,10 @@ function genNavClassName(data: IMenuItem) { ...@@ -161,7 +161,10 @@ function genNavClassName(data: IMenuItem) {
.app-header-user-main { .app-header-user-main {
h3 { h3 {
color: #202124; color: #202124;
font: 500 16px/22px Helvetica, Arial, sans-serif; font:
500 16px/22px Helvetica,
Arial,
sans-serif;
letter-spacing: 0.29px; letter-spacing: 0.29px;
margin: 0; margin: 0;
text-align: center; text-align: center;
...@@ -170,7 +173,10 @@ function genNavClassName(data: IMenuItem) { ...@@ -170,7 +173,10 @@ function genNavClassName(data: IMenuItem) {
} }
p { p {
color: #5f6368; color: #5f6368;
font: 400 14px/19px Helvetica, Arial, sans-serif; font:
400 14px/19px Helvetica,
Arial,
sans-serif;
letter-spacing: normal; letter-spacing: normal;
text-align: center; text-align: center;
text-overflow: ellipsis; text-overflow: ellipsis;
......
<script setup lang="ts"> <script setup lang="ts">
import Editor from '@tinymce/tinymce-vue' import Editor from '@tinymce/tinymce-vue'
import md5 from 'blueimp-md5' import { upload } from '@/utils/upload'
import { getSignature, uploadFile } from '@/api/base'
const props = defineProps({ const props = defineProps({
height: { height: {
...@@ -12,32 +11,12 @@ const props = defineProps({ ...@@ -12,32 +11,12 @@ const props = defineProps({
const ImageUploadHandler = (blobInfo: any) => const ImageUploadHandler = (blobInfo: any) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const file = blobInfo.blob() upload(blobInfo.blob())
getSignature() .then((url) => {
.then((response: any) => { resolve(url)
const prefix = 'upload/admin/'
const key = prefix + md5(file.name + new Date().getTime()) + file.name.substr(file.name.lastIndexOf('.'))
const { accessid, policy, signature, host } = response
const params = {
key,
host,
OSSAccessKeyId: accessid,
policy,
signature,
success_action_status: '200',
file,
url: `${host}/${key}`,
}
uploadFile(params)
.then((res: any) => {
resolve(res.url)
})
.catch(() => {
reject('上传失败')
})
}) })
.catch(() => { .catch(() => {
reject('获取Signature失败') reject('上传失败')
}) })
}) })
......
import { getLocalFileChunk, uploadLocalFile } from '@/api/base'
interface FileItem {
file: File
url: string
name: string
progress: number
abortController: AbortController
}
interface UploadOptions {
multiple?: boolean
autoUpload?: boolean
onUploadStarted?: (file: FileItem) => void
onUploadSucceed?: (file: FileItem) => void
onUploadFailed?: (file: FileItem, error: any) => void
onUploadProgress?: (file: FileItem, progress: number) => void
onUploadEnd?: () => void
}
export function useUpload(options: UploadOptions = {}) {
options = Object.assign({ autoUpload: true, multiple: false }, options)
const files = ref<FileItem[]>([])
const uploading = ref(false)
function addFile(file: File) {
// 检查文件是否已经在队列中
const existingFileIndex = files.value.findIndex((f) => f.name === file.name)
// 如果文件已经在队列中,并且上传已经开始,则直接返回
if (existingFileIndex !== -1 && files.value[existingFileIndex].progress > 0) return
if (!options.multiple) stopUpload()
const abortController = new AbortController()
files.value.push({ url: '', name: file.name, file, progress: 0, abortController })
if (options.autoUpload && !uploading.value) startUpload()
}
async function startUpload() {
uploading.value = true
for (const file of files.value) {
if (file.progress === 0) {
// 只处理尚未开始上传的文件
try {
options?.onUploadStarted?.(file)
await uploadFile(file)
options?.onUploadSucceed?.(file)
} catch (error) {
options?.onUploadFailed?.(file, error)
}
}
}
uploading.value = false
options?.onUploadEnd?.()
}
async function uploadFile(file: FileItem) {
const {
data: { detail },
} = await getLocalFileChunk({ file_name: file.file.name, file_size: file.file.size })
const fileName = detail.file_name
const chunkSize = detail.chunk_size
const totalChunks = Math.ceil(file.file.size / chunkSize)
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const chunk = file.file.slice(chunkIndex * chunkSize, (chunkIndex + 1) * chunkSize)
await uploadChunk({ file, chunk, chunkIndex, fileName, totalChunks })
}
}
async function uploadChunk({
file,
chunk,
chunkIndex,
fileName,
totalChunks,
}: {
file: FileItem
chunk: Blob
chunkIndex: number
fileName: string
totalChunks: number
}) {
const {
data: { detail },
} = await uploadLocalFile(
{ file_name: fileName, file: chunk, now_package_num: chunkIndex + 1, total_package_num: totalChunks },
{
signal: file.abortController.signal,
onUploadProgress(event: ProgressEvent) {
updateProgress({ file, event, chunkIndex, totalChunks })
},
},
)
file.url = detail.uri
}
function updateProgress({
event,
chunkIndex,
totalChunks,
file,
}: {
file: FileItem
event: ProgressEvent
chunkIndex: number
totalChunks: number
}) {
const totalSize = event.total * totalChunks
const loadedSize = event.loaded + chunkIndex * event.total
const progressPercent = (loadedSize / totalSize) * 100
file.progress = parseFloat(progressPercent.toFixed(2))
options?.onUploadProgress?.(file, file.progress)
}
function cancelUpload(file: FileItem) {
file.abortController.abort()
files.value = files.value.filter((item) => item.name !== file.name)
}
function stopUpload() {
files.value.forEach(cancelUpload) // 停止所有文件的上传
files.value.length = 0
uploading.value = false // 更新上传状态
}
return { files, uploading, addFile, startUpload, cancelUpload, stopUpload }
}
...@@ -185,11 +185,14 @@ const handleSelectionChange = (val: any) => { ...@@ -185,11 +185,14 @@ const handleSelectionChange = (val: any) => {
} }
const handleAnalysis = () => { const handleAnalysis = () => {
// isShowAnalysisDialog.value = true // isShowAnalysisDialog.value = true
window.open( // window.open(
import.meta.env.VITE_BI_URL + // import.meta.env.VITE_BI_URL +
'/bi/?proc=1&action=viewer&hback=true&isInPreview=true&db=!7d2b!!8346!!6559!!80b2!e-SaaS!2f!!751f!!6e90!!5730!!5206!!5e03!.db&platform=PC&browserType=chrome', // '/bi/?proc=1&action=viewer&hback=true&isInPreview=true&db=!7d2b!!8346!!6559!!80b2!e-SaaS!2f!!751f!!6e90!!5730!!5206!!5e03!.db&platform=PC&browserType=chrome',
) // )
window.open('/teach/chart/origin')
} }
const isSchool = import.meta.env.MODE === 'school'
</script> </script>
<template> <template>
...@@ -213,7 +216,7 @@ const handleAnalysis = () => { ...@@ -213,7 +216,7 @@ const handleAnalysis = () => {
<el-button type="primary" round @click="handleUpdate" v-permission="'v1-learning-student-import'" <el-button type="primary" round @click="handleUpdate" v-permission="'v1-learning-student-import'"
>批量修改</el-button >批量修改</el-button
> >
<el-button type="primary" round @click="handleAnalysis">生源地分析</el-button> <el-button type="primary" round @click="handleAnalysis" v-if="isSchool">生源地分析</el-button>
<template #status="{ row }"> <template #status="{ row }">
<el-switch <el-switch
size="large" size="large"
......
...@@ -2,6 +2,7 @@ import axios from 'axios' ...@@ -2,6 +2,7 @@ import axios from 'axios'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { useGetCategoryList } from '@/composables/useGetCategoryList' import { useGetCategoryList } from '@/composables/useGetCategoryList'
import { upload } from '@/utils/upload' import { upload } from '@/utils/upload'
import { getUploadedVideoSourceId, uploadVideo as uploadVideoFile } from '@/utils/videoUpload'
import { import {
createBook, createBook,
createCase, createCase,
...@@ -10,8 +11,6 @@ import { ...@@ -10,8 +11,6 @@ import {
createResourceOther, createResourceOther,
createResourceVideo, createResourceVideo,
createVideo, createVideo,
getResourceUploadVideoAuth,
updateResourceUploadVideoAuth,
} from '../api' } from '../api'
import type { AuthorizePlatform, CaseFileSelectionType, CaseLibraryFile, CaseLibraryItem } from '../types' import type { AuthorizePlatform, CaseFileSelectionType, CaseLibraryFile, CaseLibraryItem } from '../types'
import { useCaseLibrary } from './useCaseLibrary' import { useCaseLibrary } from './useCaseLibrary'
...@@ -265,55 +264,21 @@ export function useCaseAuthorization() { ...@@ -265,55 +264,21 @@ export function useCaseAuthorization() {
total: number, total: number,
) { ) {
const file = await downloadRemoteFile(url, fileName, index, total) const file = await downloadRemoteFile(url, fileName, index, total)
const uploadedVideo = await uploadVideoFile(file, {
return new Promise<string>((resolve, reject) => { onUploadStarted(uploadItem) {
const uploader = new (window as any).AliyunUpload.Vod({ setStepProgress(index, total, 0.55, `正在上传视频:${uploadItem.file.name}`)
userId: '1303984639806000', },
region: 'cn-shanghai', onUploadSucceed(uploadItem) {
partSize: 1048576, setStepProgress(index, total, 0.95, `视频上传完成:${uploadItem.file.name}`)
parallel: 5, },
retryCount: 3, onUploadProgress(_uploadItem, progress) {
retryDuration: 2, setStepProgress(index, total, 0.55 + (progress / 100) * 0.4, `正在上传视频 ${Math.round(progress)}%:${fileName}`)
onUploadstarted(uploadInfo: any) { },
setStepProgress(index, total, 0.55, `正在上传视频:${uploadInfo.file.name}`)
getResourceUploadVideoAuth({ title: uploadInfo.file.name, file_name: uploadInfo.file.name })
.then((res: any) => {
uploader.setUploadAuthAndAddress(
uploadInfo,
res.data.upload_auth,
res.data.upload_address,
res.data.source_id,
)
})
.catch(reject)
},
onUploadSucceed(uploadInfo: any) {
setStepProgress(index, total, 0.95, `视频上传完成:${uploadInfo.file.name}`)
resolve(uploadInfo.videoId)
},
onUploadFailed(_uploadInfo: any, code: number, message: string) {
reject(new Error(message || `视频上传失败: ${code}`))
},
onUploadProgress(_uploadInfo: any, _totalSize: number, loadedPercent: number) {
setStepProgress(
index,
total,
0.55 + loadedPercent * 0.4,
`正在上传视频 ${Math.round(loadedPercent * 100)}%:${fileName}`,
)
},
onUploadTokenExpired(uploadInfo: any) {
updateResourceUploadVideoAuth({ source_id: uploadInfo.videoId })
.then((res: any) => {
uploader.resumeUploadWithAuth(res.data.UploadAuth || res.data.upload_auth || res.UploadAuth)
})
.catch(reject)
},
})
uploader.addFile(file, null, null, null, '{"Vod":{}}')
uploader.startUpload()
}) })
return {
sourceId: getUploadedVideoSourceId(uploadedVideo),
size: file.size,
}
} }
async function submitAuthorize() { async function submitAuthorize() {
...@@ -357,14 +322,14 @@ export function useCaseAuthorization() { ...@@ -357,14 +322,14 @@ export function useCaseAuthorization() {
setStepProgress(index, actionableItems.length, 0.05, `准备处理 ${index + 1}/${actionableItems.length}${file.name}`) setStepProgress(index, actionableItems.length, 0.05, `准备处理 ${index + 1}/${actionableItems.length}${file.name}`)
if (type === 'video') { if (type === 'video') {
const sourceId = await uploadVideoFromCase(file.url, file.name, index, actionableItems.length) const uploadedVideo = await uploadVideoFromCase(file.url, file.name, index, actionableItems.length)
if (authorizeToExperiment) { if (authorizeToExperiment) {
setStepProgress(index, actionableItems.length, 0.98, `正在创建实验操作视频:${file.name}`) setStepProgress(index, actionableItems.length, 0.98, `正在创建实验操作视频:${file.name}`)
await createVideo({ await createVideo({
experiment_id: selectedExperiment.value, experiment_id: selectedExperiment.value,
name: authorizedName, name: authorizedName,
source_id: sourceId, source_id: uploadedVideo.sourceId,
status: '1', status: '1',
}) })
successStats.experiment.video += 1 successStats.experiment.video += 1
...@@ -378,7 +343,8 @@ export function useCaseAuthorization() { ...@@ -378,7 +343,8 @@ export function useCaseAuthorization() {
classification: resourceClassification.value, classification: resourceClassification.value,
knowledge_points: '', knowledge_points: '',
cover: '', cover: '',
source_id: sourceId, source_id: uploadedVideo.sourceId,
size: uploadedVideo.size,
}) })
successStats.resource.video += 1 successStats.resource.video += 1
} }
......
...@@ -74,4 +74,5 @@ export interface ResourceVideoCreateItem { ...@@ -74,4 +74,5 @@ export interface ResourceVideoCreateItem {
knowledge_points: string knowledge_points: string
cover: string cover: string
source_id: string source_id: string
size?: number
} }
<script lang="ts" setup> <script lang="ts" setup>
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
// import { Plus } from '@element-plus/icons-vue' // import { Plus } from '@element-plus/icons-vue'
import type { UploadProps, UploadUserFile } from 'element-plus' import type { UploadProps, UploadRequestOptions, UploadUserFile } from 'element-plus'
import md5 from 'blueimp-md5' import { upload } from '@/utils/upload'
import { getSignature } from '@/api/base'
type UploadFileItem = { name: string; url: string } type UploadFileItem = { name: string; url: string }
...@@ -12,8 +11,6 @@ const props = withDefaults(defineProps<{ modelValue: string | UploadFileItem[]; ...@@ -12,8 +11,6 @@ const props = withDefaults(defineProps<{ modelValue: string | UploadFileItem[];
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const uploadData = ref()
const fileList = ref<UploadUserFile[]>([]) const fileList = ref<UploadUserFile[]>([])
watch( watch(
...@@ -27,21 +24,15 @@ const showFileList = computed(() => { ...@@ -27,21 +24,15 @@ const showFileList = computed(() => {
return Array.isArray(props.modelValue) return Array.isArray(props.modelValue)
}) })
// 上传之前 const handleUploadRequest = async (option: UploadRequestOptions) => {
const handleBeforeUpload = async (file: any) => { try {
const fileName = file.name const url = await upload(option.file)
const key = props.prefix + md5(fileName + new Date().getTime()) + fileName.substr(fileName.lastIndexOf('.')) ;(option.file as any).url = url
const response: Record<string, any> = await getSignature() ;(option.file as any).raw = Object.assign((option.file as any).raw || {}, { url })
uploadData.value = { option.onSuccess({ url })
key, } catch (error: any) {
host: response.host, option.onError(error)
OSSAccessKeyId: response.accessid,
policy: response.policy,
signature: response.signature,
success_action_status: '200',
url: `${response.host}/${key}`
} }
file.url = `${response.host}/${key}`
} }
// 上传成功 // 上传成功
...@@ -82,10 +73,9 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => { ...@@ -82,10 +73,9 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => {
<template> <template>
<el-upload <el-upload
:action="uploadData?.host" action="#"
:data="uploadData" :http-request="handleUploadRequest"
:show-file-list="false" :show-file-list="false"
:before-upload="handleBeforeUpload"
:on-exceed="handleExceed" :on-exceed="handleExceed"
:on-remove="handleRemove" :on-remove="handleRemove"
:on-preview="handlePreview" :on-preview="handlePreview"
......
<script lang="ts" setup> <script lang="ts" setup>
import { checkPermission } from '@/utils/permission' import { checkPermission } from '@/utils/permission'
import { getVideoPlayUrl } from '@/utils/video'
import { Plus } from '@element-plus/icons-vue' import { Plus } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
...@@ -315,7 +316,7 @@ const handleConsult = (node: any) => { ...@@ -315,7 +316,7 @@ const handleConsult = (node: any) => {
// 视频 // 视频
if (node.data.resource_type === '2') { if (node.data.resource_type === '2') {
getVideoDetails({ id: node.data.resource_id }).then((res) => { getVideoDetails({ id: node.data.resource_id }).then((res) => {
videoUrl.value = res.data.play_auth.play_info_list.filter((item: any) => item.Definition === 'SD')[0].PlayURL videoUrl.value = getVideoPlayUrl(res.data.play_auth)
isShowVideoPlayDialog.value = true isShowVideoPlayDialog.value = true
}) })
} }
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { getVideoDetails } from '../api' import { getVideoDetails } from '../api'
import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue' import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue'
import ViewCourseChapter from './ViewCourseChapter.vue' import ViewCourseChapter from './ViewCourseChapter.vue'
import { getVideoPlayUrl } from '@/utils/video'
const route = useRoute() const route = useRoute()
...@@ -32,11 +33,9 @@ const videoOptions = computed(() => { ...@@ -32,11 +33,9 @@ const videoOptions = computed(() => {
return { return {
sources: [ sources: [
{ {
src: resourceData.play_auth?.play_info_list.find((item: any) => { src: getVideoPlayUrl(resourceData.play_auth),
return item.Definition === 'SD' },
}).PlayURL ],
}
]
} }
}) })
const video = computed<{ id: string }>(() => { const video = computed<{ id: string }>(() => {
......
...@@ -6,7 +6,15 @@ export function getVideoList(params: { tab: string; status?: string; authorized? ...@@ -6,7 +6,15 @@ export function getVideoList(params: { tab: string; status?: string; authorized?
} }
// 创建视频 // 创建视频
export function createVideo(data: { name: string; source: string; classification: string; knowledge_points: string; cover: string; source_id: string }) { export function createVideo(data: {
name: string
source: string
classification: string
knowledge_points: string
cover: string
source_id: string
size?: number
}) {
return httpRequest.post('/api/resource/v1/resource/video/create', data) return httpRequest.post('/api/resource/v1/resource/video/create', data)
} }
......
...@@ -3,6 +3,7 @@ import { ElMessage } from 'element-plus' ...@@ -3,6 +3,7 @@ import { ElMessage } from 'element-plus'
import { useGetCategoryList } from '@/composables/useGetCategoryList' import { useGetCategoryList } from '@/composables/useGetCategoryList'
import { createVideo } from '../api' import { createVideo } from '../api'
import UploadMultipleVideo from './UploadMultipleVideo.vue' import UploadMultipleVideo from './UploadMultipleVideo.vue'
import { getUploadedVideoSourceId } from '@/utils/videoUpload'
const emit = defineEmits<Emits>() const emit = defineEmits<Emits>()
let { list: selectTree }: any = useGetCategoryList() let { list: selectTree }: any = useGetCategoryList()
const defaultProps = { children: 'children', label: 'category_name', value: 'id' } const defaultProps = { children: 'children', label: 'category_name', value: 'id' }
...@@ -23,26 +24,22 @@ const handleConfirm = () => { ...@@ -23,26 +24,22 @@ const handleConfirm = () => {
emit('update') emit('update')
} }
const handleCancel = () => { const handleCancel = () => {
const fileList = uploadMultipleVideoRef.value?.uploader?._uploadList || [] if (uploadMultipleVideoRef.value?.uploading) {
if (fileList.length) { return ElMessage.error('请先完成上传')
for (const item of fileList) {
if (item.state === 'Uploading') {
return ElMessage.error('请先完成上传')
}
}
} }
emit('update:modelValue', false) emit('update:modelValue', false)
} }
// 上传视频成功 // 上传视频成功
const uploadVideo = (data: any) => { const uploadVideo = (data: any) => {
const { file, videoId } = data const { file } = data
const params = { const params = {
name: file.name.slice(0, file.name.lastIndexOf('.')), name: file.name.slice(0, file.name.lastIndexOf('.')),
source: '2', source: '2',
classification: form.classification, classification: form.classification,
knowledge_points: '', knowledge_points: '',
source_id: videoId, source_id: getUploadedVideoSourceId(data),
cover: '' size: file.size,
cover: '',
} }
createVideo(params).then(() => { createVideo(params).then(() => {
ElMessage.success('视频上传成功') ElMessage.success('视频上传成功')
......
<script lang="ts" setup> <script lang="ts" setup>
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
// import { Plus } from '@element-plus/icons-vue' // import { Plus } from '@element-plus/icons-vue'
import type { UploadProps, UploadUserFile } from 'element-plus' import type { UploadProps, UploadRequestOptions, UploadUserFile } from 'element-plus'
import md5 from 'blueimp-md5' import { upload } from '@/utils/upload'
import { getSignature } from '@/api/base'
type UploadFileItem = { name: string; url: string } type UploadFileItem = { name: string; url: string }
...@@ -12,8 +11,6 @@ const props = withDefaults(defineProps<{ modelValue: string | UploadFileItem[]; ...@@ -12,8 +11,6 @@ const props = withDefaults(defineProps<{ modelValue: string | UploadFileItem[];
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const uploadData = ref()
const fileList = ref<UploadUserFile[]>([]) const fileList = ref<UploadUserFile[]>([])
watch( watch(
...@@ -27,21 +24,15 @@ const showFileList = computed(() => { ...@@ -27,21 +24,15 @@ const showFileList = computed(() => {
return Array.isArray(props.modelValue) return Array.isArray(props.modelValue)
}) })
// 上传之前 const handleUploadRequest = async (option: UploadRequestOptions) => {
const handleBeforeUpload = async (file: any) => { try {
const fileName = file.name const url = await upload(option.file)
const key = props.prefix + md5(fileName + new Date().getTime()) + fileName.substr(fileName.lastIndexOf('.')) ;(option.file as any).url = url
const response: Record<string, any> = await getSignature() ;(option.file as any).raw = Object.assign((option.file as any).raw || {}, { url })
uploadData.value = { option.onSuccess({ url })
key, } catch (error: any) {
host: response.host, option.onError(error)
OSSAccessKeyId: response.accessid,
policy: response.policy,
signature: response.signature,
success_action_status: '200',
url: `${response.host}/${key}`
} }
file.url = `${response.host}/${key}`
} }
// 上传成功 // 上传成功
...@@ -82,10 +73,9 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => { ...@@ -82,10 +73,9 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => {
<template> <template>
<el-upload <el-upload
:action="uploadData?.host" action="#"
:data="uploadData" :http-request="handleUploadRequest"
:show-file-list="false" :show-file-list="false"
:before-upload="handleBeforeUpload"
:on-exceed="handleExceed" :on-exceed="handleExceed"
:on-remove="handleRemove" :on-remove="handleRemove"
:on-preview="handlePreview" :on-preview="handlePreview"
......
<script setup lang="ts"> <script setup lang="ts">
import VideoDetail from './VideoDetail.vue' import VideoDetail from './VideoDetail.vue'
import { getCreateAuth, updateAuth } from '@/api/base'
import { CircleClose } from '@element-plus/icons-vue' import { CircleClose } from '@element-plus/icons-vue'
const idShowMore = ref(false) import type { VideoUploadItem } from '@/utils/videoUpload'
import { createVideoUploader } from '@/utils/videoUpload'
// uploadInfo 包含要上传的文件信息
interface UploadInfo {
bucket: string
checkpoint: { file: File; name: string; fileSize: number; partSize: number; uploadId: string }
endpoint: string
file: File
fileHash: string
isImage: boolean
loaded: number
object: string
region: string
retry: boolean
ri: string
state: string
userData: string
videoId: string
videoInfo: any
progress: number
}
const idShowMore = ref(false)
const emit = defineEmits(['upload', 'canClose']) const emit = defineEmits(['upload', 'canClose'])
let uploader = createUploader() const { files: fileList, uploading, addFile, cancelUpload } = createVideoUploader({
const fileList = ref<UploadInfo[]>([]) multiple: true,
onUploadStarted() {
emit('canClose', { closeStatus: false })
},
onUploadSucceed(file) {
emit('upload', file)
},
onUploadEnd() {
emit('canClose', { closeStatus: true })
},
})
const fileChange = (event: Event) => { const fileChange = (event: Event) => {
const element = event.currentTarget as HTMLInputElement const element = event.currentTarget as HTMLInputElement
let files: FileList | null = element.files const files = element.files
if (!files) return if (!files) return
for (const file of files) { for (const file of files) {
// 是否重复上传 addFile(file)
const hasRepeat = !!fileList.value.find(
item =>
item.file.name === file.name && item.file.size === file.size && item.file.lastModified === file.lastModified
)
!hasRepeat && uploader.addFile(file, null, null, null, '{"Vod":{}}')
} }
uploader.startUpload()
fileList.value = uploader._uploadList
}
function updateFileList(uploadInfo: UploadInfo) {
if (!uploadInfo) return
fileList.value = fileList.value.map(item => {
if (item.ri === uploadInfo.ri) {
return { ...item, ...uploadInfo }
}
return item
})
}
function createUploader() {
return new window['AliyunUpload'].Vod({
//userID,必填,您可以使用阿里云账号访问账号中心(https://account.console.aliyun.com/),即可查看账号ID
userId: '1303984639806000',
//上传到视频点播的地域,默认值为'cn-shanghai',
//eu-central-1,ap-southeast-1
region: 'cn-shanghai',
//分片大小默认1 MB,不能小于100 KB(100*1024)
partSize: 1048576,
//并行上传分片个数,默认5
parallel: 5,
//网络原因失败时,重新上传次数,默认为3
retryCount: 3,
//网络原因失败时,重新上传间隔时间,默认为2秒
retryDuration: 2,
//开始上传
onUploadstarted: onUploadStarted,
//文件上传成功
onUploadSucceed: onUploadSucceed,
//文件上传失败
onUploadFailed: onUploadFailed,
//文件上传进度,单位:字节
onUploadProgress: onUploadProgress,
//上传凭证或STS token超时
onUploadTokenExpired: onUploadTokenExpired,
//全部文件上传结束
onUploadEnd: onUploadEnd
})
}
// 开始上传
function onUploadStarted(uploadInfo: UploadInfo) {
console.log('onUploadStarted', uploadInfo)
getCreateAuth({ title: uploadInfo.file.name, file_name: uploadInfo.file.name }).then(res => {
uploader.setUploadAuthAndAddress(uploadInfo, res.data.upload_auth, res.data.upload_address, res.data.source_id)
})
updateFileList(uploadInfo)
emit('canClose', { closeStatus: false })
}
// 文件上传成功
function onUploadSucceed(uploadInfo: UploadInfo) {
console.log('onUploadSucceed', uploadInfo)
updateFileList(uploadInfo)
emit('upload', uploadInfo)
}
//文件上传失败
function onUploadFailed(uploadInfo: UploadInfo, code: number, message: string) {
console.log(uploadInfo, '111111')
console.log('onUploadFailed', uploadInfo, code, message)
updateFileList(uploadInfo)
}
//文件上传进度,单位:字节
function onUploadProgress(uploadInfo: UploadInfo, totalSize: number, loadedPercent: number) {
console.log('onUploadProgress', uploadInfo.file.name, uploadInfo, totalSize, loadedPercent)
updateFileList(uploadInfo)
}
//上传凭证或STS token超时
function onUploadTokenExpired(uploadInfo: UploadInfo) {
console.log('onUploadTokenExpired', uploadInfo)
updateAuth({ source_id: uploadInfo.videoId }).then(res => {
uploader.resumeUploadWithAuth(res.data.UploadAuth)
})
updateFileList(uploadInfo)
}
// 全部文件上传结束
function onUploadEnd(uploadInfo: UploadInfo) {
console.log('onUploadEnd', uploadInfo)
updateFileList(uploadInfo)
emit('canClose', { closeStatus: true })
} }
const handleView = () => { const handleView = () => {
idShowMore.value = true idShowMore.value = true
} }
// 进度条
function percentage(value: number) {
return parseFloat((value ? value * 100 : 0).toFixed(2))
}
// 删除上传文件 const deleteFile = (item: VideoUploadItem) => {
const deleteFile = function (index: number) { cancelUpload(item)
console.log(index, 'deleteFile', fileList)
fileList.value.splice(index, 1)
uploader.deleteFile(index)
} }
defineExpose({ uploader, fileList })
defineExpose({ files: fileList, uploading, cancelUpload })
</script> </script>
<template> <template>
<div class="upload-video" style="display: flex; flex-direction: column; align-items: flex-start"> <div class="upload-video" style="display: flex; flex-direction: column; align-items: flex-start">
<div class="upload-btn"> <div class="upload-btn">
...@@ -149,9 +50,9 @@ defineExpose({ uploader, fileList }) ...@@ -149,9 +50,9 @@ defineExpose({ uploader, fileList })
<div v-for="(item, index) in fileList.slice(0, 3)" :key="index"> <div v-for="(item, index) in fileList.slice(0, 3)" :key="index">
<div class="video-info"> <div class="video-info">
<span class="name">{{ item.file?.name }}</span> <span class="name">{{ item.file?.name }}</span>
<el-progress style="width: 200px" :percentage="percentage(item.loaded)" class="view" /> <el-progress style="width: 200px" :percentage="item.progress" class="view" />
<div v-if="percentage(item.loaded) == 100">上传成功</div> <div v-if="item.progress === 100">上传成功</div>
<el-icon v-else @click="deleteFile(index)"><CircleClose /></el-icon> <el-icon v-else @click="deleteFile(item)"><CircleClose /></el-icon>
</div> </div>
</div> </div>
<el-link style="padding-top: 3px" :underline="false" type="primary" v-if="fileList.length > 3" @click="handleView" <el-link style="padding-top: 3px" :underline="false" type="primary" v-if="fileList.length > 3" @click="handleView"
...@@ -160,6 +61,7 @@ defineExpose({ uploader, fileList }) ...@@ -160,6 +61,7 @@ defineExpose({ uploader, fileList })
</div> </div>
<VideoDetail :videoList="fileList" v-model:modelValue="idShowMore" v-if="idShowMore === true" /> <VideoDetail :videoList="fileList" v-model:modelValue="idShowMore" v-if="idShowMore === true" />
</template> </template>
<style lang="scss"> <style lang="scss">
.demo-progress { .demo-progress {
min-width: 350px; min-width: 350px;
......
<script setup lang="ts"> <script setup lang="ts">
import { getCreateAuth, updateAuth } from '@/api/base'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
/** import { createVideoUploader } from '@/utils/videoUpload'
* upload 上传状态 {code: -1(待上传)0(成功) 1(开始上传) 2(上传失败), msg: '上传信息'}
* progress 上传进度
**/
interface IUpload {
code: number
name?: string
msg?: string
progress?: number
videoId?: string
}
const uploadData = ref<IUpload>({ code: -1 })
const emit = defineEmits(['upload']) const emit = defineEmits(['upload'])
const form: any = reactive({
timeout: '', const uploadState = reactive({
partSize: '', code: -1,
parallel: '', name: '',
retryCount: '', progress: 0,
retryDuration: '', })
region: 'cn-shanghai',
userId: '1303984639806000', const { files, uploading, addFile, stopUpload } = createVideoUploader({
file: null, onUploadStarted(file) {
authProgress: 0, uploadState.code = 1
uploader: null, uploadState.name = file.name
statusText: '' uploadState.progress = 0
},
onUploadProgress(_file, progress) {
uploadState.code = 1
uploadState.progress = progress
},
onUploadSucceed(file) {
uploadState.code = 0
uploadState.name = file.name
uploadState.progress = 100
emit('upload', file)
},
onUploadFailed(file) {
uploadState.code = 2
uploadState.name = file.name
},
}) })
const fileChange = (e: any) => { const fileChange = (e: any) => {
form.file = e.target.files[0] const file = e.target.files[0]
if (form.file.name.indexOf('.mp4') === -1) { if (!file) return
if (file.name.indexOf('.mp4') === -1) {
ElMessage('请上传mp4格式视频') ElMessage('请上传mp4格式视频')
return return
} }
var userData = '{"Vod":{}}' if (files.value.length) {
if (form.uploader) { stopUpload()
form.uploader.stopUpload()
form.authProgress = 0
form.statusText = ''
} }
form.uploader = createUploader() addFile(file)
form.uploader.addFile(form.file, null, null, null, userData)
form.uploader.startUpload()
}
const createUploader: any = () => {
const w = window as any
const uploader = new w.AliyunUpload.Vod({
timeout: form.timeout || 60000,
partSize: form.partSize || 1048576,
parallel: form.parallel || 5,
retryCount: form.retryCount || 3,
retryDuration: form.retryDuration || 2,
region: form.region,
userId: form.userId,
// 开始上传
onUploadstarted: function (uploadInfo: any) {
const fileData = JSON.parse(window.localStorage.fileData || '{}')
// 判断有没有上传过
const isFile = !!fileData.sourceId
if (!isFile) {
// 没上传过请求凭证上传
getCreateAuth({ title: uploadInfo.file.name, file_name: uploadInfo.file.name }).then((data: any) => {
window.localStorage.fileData = JSON.stringify({
uploadAuth: data.data.upload_auth,
uploadAddress: data.data.upload_address,
videoId: data.data.source_id,
fileName: uploadInfo.file.name,
fileSize: uploadInfo.file.size
})
uploader.setUploadAuthAndAddress(
uploadInfo,
data.data.upload_auth,
data.data.upload_address,
data.data.source_id
)
})
} else {
// 上传过判断一下上次上传的文件和本次上传的文件一不一样,一样的话继续上传
if (fileData.fileName === uploadInfo.file.name && fileData.fileSize === uploadInfo.file.size) {
uploader.setUploadAuthAndAddress(uploadInfo, fileData.uploadAuth, fileData.uploadAddress, fileData.videoId)
} else {
getCreateAuth({ title: uploadInfo.file.name, file_name: uploadInfo.file.name }).then((data: any) => {
uploader.setUploadAuthAndAddress(
uploadInfo,
data.data.upload_auth,
data.data.upload_address,
data.data.source_id
)
})
}
}
uploadData.value = {
code: 1,
name: uploadInfo.file.name,
msg: '开始上传'
}
},
// 文件上传成功
onUploadSucceed: function (uploadInfo: any) {
const fileData = window.localStorage.fileData ? JSON.parse(window.localStorage.fileData) : {}
uploadData.value = {
code: 0,
name: uploadInfo.file.name,
videoId: fileData.videoId,
msg: '上传成功'
}
emit('upload', { videoId: fileData.videoId, name: uploadInfo.file.name })
},
// 文件上传失败
// code:any, message:any
onUploadFailed: function (uploadInfo: any) {
uploadData.value = {
code: 2,
name: uploadInfo.file.name,
msg: '文件上传失败'
}
},
// 文件上传进度,单位:字节, 可以在这个函数中拿到上传进度并显示在页面上
onUploadProgress: function (uploadInfo: any, totalSize: any, progress: any) {
let progressPercent = Math.ceil(progress * 100)
form.authProgress = progressPercent
uploadData.value.progress = progressPercent
},
// 上传凭证超时
onUploadTokenExpired: function (uploadInfo: any) {
const fileData = JSON.parse(window.localStorage.fileData || '{}')
updateAuth({ source_id: fileData.videoId }).then(({ data }) => {
let uploadAuth = data.UploadAuth
window.localStorage.fileData = JSON.stringify({
uploadAuth: data.data.upload_auth,
uploadAddress: data.data.upload_address,
videoId: data.data.source_id,
fileName: uploadInfo.file.name,
fileSize: uploadInfo.file.size
})
uploader.resumeUploadWithAuth(uploadAuth)
})
}
})
return uploader
} }
</script> </script>
<template> <template>
...@@ -151,15 +53,15 @@ const createUploader: any = () => { ...@@ -151,15 +53,15 @@ const createUploader: any = () => {
<!-- accept=".mp4" --> <!-- accept=".mp4" -->
<input accept=".mp4" type="file" id="fileUpload" @change="fileChange($event)" /> <input accept=".mp4" type="file" id="fileUpload" @change="fileChange($event)" />
</div> </div>
<div class="demo-progress" v-if="uploadData.code === 1"> <div class="demo-progress" v-if="uploading || uploadState.code === 1">
<el-progress style="width: 340px" :percentage="uploadData.progress" /> <el-progress style="width: 340px" :percentage="uploadState.progress" />
<!-- <span> {{ uploadData.progress }}% </span> --> <!-- <span> {{ uploadData.progress }}% </span> -->
</div> </div>
<div class="error video-info" v-if="uploadData.code === 2"> <div class="error video-info" v-if="uploadState.code === 2">
<div class="name">上传失败(请重新选择文件进行上传)</div> <div class="name">上传失败(请重新选择文件进行上传)</div>
</div> </div>
<div class="video-info" v-if="uploadData.code === 0"> <div class="video-info" v-if="uploadState.code === 0">
<div class="name">{{ uploadData.name }}</div> <div class="name">{{ uploadState.name }}</div>
</div> </div>
</div> </div>
<div class="tips">推荐视频格式:帧率为25fps\输出码率为4M\输出格式为mp4,建议采用格式工厂等工具处理后上传。</div> <div class="tips">推荐视频格式:帧率为25fps\输出码率为4M\输出格式为mp4,建议采用格式工厂等工具处理后上传。</div>
......
<script lang="ts" setup> <script lang="ts" setup>
interface UploadInfo { import type { VideoUploadItem } from '@/utils/videoUpload'
bucket: string
checkpoint: { file: File; name: string; fileSize: number; partSize: number; uploadId: string }
endpoint: string
file: File
fileHash: string
isImage: boolean
loaded: number
object: string
region: string
retry: boolean
ri: string
state: string
userData: string
videoId: string
videoInfo: any
progress: number
}
interface Props { interface Props {
videoList: UploadInfo[] videoList: VideoUploadItem[]
modelValue: boolean modelValue: boolean
} }
interface Emits { interface Emits {
...@@ -40,9 +24,6 @@ const handleSizeChange = (val: any) => { ...@@ -40,9 +24,6 @@ const handleSizeChange = (val: any) => {
const handleCurrentChange = (val: any) => { const handleCurrentChange = (val: any) => {
page.currentPage = val page.currentPage = val
} }
function percentage(value: number) {
return parseFloat((value ? value * 100 : 0).toFixed(2))
}
</script> </script>
<template> <template>
<el-dialog :model-value="props.modelValue" title="更多视频文件" :before-close="handleCancel" width="30vw"> <el-dialog :model-value="props.modelValue" title="更多视频文件" :before-close="handleCancel" width="30vw">
...@@ -52,7 +33,7 @@ function percentage(value: number) { ...@@ -52,7 +33,7 @@ function percentage(value: number) {
> >
<div class="video-info"> <div class="video-info">
<span class="name">{{ item.file?.name }}</span> <span class="name">{{ item.file?.name }}</span>
<el-progress style="width: 200px" :percentage="percentage(item.loaded)" class="view" /> <el-progress style="width: 200px" :percentage="item.progress" class="view" />
</div> </div>
</div> </div>
<el-pagination <el-pagination
......
<script setup lang="ts"> <script setup lang="ts">
import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue' import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue'
import { getVideoPlayUrl } from '@/utils/video'
const props = defineProps(['data']) const props = defineProps(['data'])
const videoOptions = computed(() => { const videoOptions = computed(() => {
return { return {
sources: [ sources: [
{ {
src: props.data.play_auth.play_info_list.find((item: any) => { src: getVideoPlayUrl(props.data.play_auth),
return item.Definition === 'SD' },
}).PlayURL ],
}
]
} }
}) })
</script> </script>
......
...@@ -3,6 +3,7 @@ import { ElMessage } from 'element-plus' ...@@ -3,6 +3,7 @@ import { ElMessage } from 'element-plus'
import { useGetCategoryList } from '@/composables/useGetCategoryList' import { useGetCategoryList } from '@/composables/useGetCategoryList'
import { createVideo } from '../api' import { createVideo } from '../api'
import UploadMultipleVideo from '../components/UploadMultipleVideo.vue' import UploadMultipleVideo from '../components/UploadMultipleVideo.vue'
import { getUploadedVideoSourceId } from '@/utils/videoUpload'
// const emit = defineEmits<Emits>() // const emit = defineEmits<Emits>()
...@@ -39,14 +40,15 @@ const uploadMultipleVideoRef = ref<InstanceType<typeof UploadMultipleVideo> | nu ...@@ -39,14 +40,15 @@ const uploadMultipleVideoRef = ref<InstanceType<typeof UploadMultipleVideo> | nu
// } // }
// 上传视频成功 // 上传视频成功
const uploadVideo = (data: any) => { const uploadVideo = (data: any) => {
const { file, videoId } = data const { file } = data
const params = { const params = {
name: file.name.slice(0, file.name.lastIndexOf('.')), name: file.name.slice(0, file.name.lastIndexOf('.')),
source: '2', source: '2',
classification: form.classification, classification: form.classification,
knowledge_points: '', knowledge_points: '',
source_id: videoId, source_id: getUploadedVideoSourceId(data),
cover: '' size: file.size,
cover: '',
} }
createVideo(params).then(() => { createVideo(params).then(() => {
ElMessage.success('视频上传成功') ElMessage.success('视频上传成功')
......
...@@ -7,6 +7,7 @@ import UploadVideo from '../components/UploadVideo.vue' ...@@ -7,6 +7,7 @@ import UploadVideo from '../components/UploadVideo.vue'
import { getCoverList, createVideo, getVideoDetails, updateVideo } from '../api' import { getCoverList, createVideo, getVideoDetails, updateVideo } from '../api'
import { useGetCategoryList } from '@/composables/useGetCategoryList' import { useGetCategoryList } from '@/composables/useGetCategoryList'
import Protocol from '@/components/base/Protocol.vue' import Protocol from '@/components/base/Protocol.vue'
import { getUploadedVideoSourceId } from '@/utils/videoUpload'
// 路由 // 路由
const router = useRouter() const router = useRouter()
...@@ -60,7 +61,7 @@ const swiperItemHandle = (url: string) => { ...@@ -60,7 +61,7 @@ const swiperItemHandle = (url: string) => {
// form表单 // form表单
let form = reactive({ let form = reactive({
data: { name: '', source: '2', classification: '', knowledge_points: '', cover: '', source_id: '' } data: { name: '', source: '2', classification: '', knowledge_points: '', cover: '', source_id: '', size: 0 }
}) })
// 表单验证 // 表单验证
const rules = { const rules = {
...@@ -139,7 +140,8 @@ const protocol = ref(false) ...@@ -139,7 +140,8 @@ const protocol = ref(false)
// 上传视频成功 // 上传视频成功
const uploadVideo = (data: any) => { const uploadVideo = (data: any) => {
form.data.source_id = data.videoId form.data.source_id = getUploadedVideoSourceId(data)
form.data.size = data.file?.size || 0
const name = data.name const name = data.name
form.data.name = name.slice(0, name.lastIndexOf('.')) form.data.name = name.slice(0, name.lastIndexOf('.'))
} }
......
<template>
<el-dialog
:model-value="props.visible"
@update:model-value="emit('update:visible', $event)"
:title="isEdit ? '编辑大模型配置' : '新增大模型配置'"
width="800px"
:before-close="handleClose">
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" class="llm-config-form">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="配置名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入配置名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="提供商" prop="provider">
<el-select v-model="formData.provider" placeholder="请选择提供商" @change="handleProviderChange">
<el-option
v-for="provider in providerOptions"
:key="provider.value"
:label="provider.label"
:value="provider.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="模型名称" prop="model_name">
<el-select v-model="formData.model_name" placeholder="请选择模型" filterable>
<el-option v-for="model in modelOptions" :key="model.value" :label="model.label" :value="model.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="API地址" prop="api_url">
<el-input v-model="formData.api_url" placeholder="请输入API地址" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="API密钥" prop="api_key">
<el-input v-model="formData.api_key" type="password" placeholder="请输入API密钥" show-password />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="formData.description" type="textarea" :rows="3" placeholder="请输入配置描述" />
</el-form-item>
<el-divider content-position="left">高级参数配置</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="最大令牌数" prop="max_tokens">
<el-input-number
v-model="formData.max_tokens"
:min="1"
:max="100000"
controls-position="right"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="温度" prop="temperature">
<el-input-number
v-model="formData.temperature"
:min="0"
:max="2"
:step="0.1"
controls-position="right"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="Top P" prop="top_p">
<el-input-number
v-model="formData.top_p"
:min="0"
:max="1"
:step="0.1"
controls-position="right"
style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="频率惩罚" prop="frequency_penalty">
<el-input-number
v-model="formData.frequency_penalty"
:min="-2"
:max="2"
:step="0.1"
controls-position="right"
style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="存在惩罚" prop="presence_penalty">
<el-input-number
v-model="formData.presence_penalty"
:min="-2"
:max="2"
:step="0.1"
controls-position="right"
style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="启用状态" prop="is_enabled">
<el-switch v-model="formData.is_enabled" />
<span class="form-tip">启用后该配置将可用于文本处理</span>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
{{ isEdit ? '更新' : '创建' }}
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { llmConfigStorage } from '@/utils/llmStorage'
interface Props {
visible: boolean
editData?: any
}
interface Emits {
(e: 'update:visible', value: boolean): void
(e: 'success'): void
}
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const formRef = ref<FormInstance>()
const submitting = ref(false)
// 提供商选项
const providerOptions = [
{ label: 'DeepSeek', value: 'deepseek' },
{ label: '通义千问 (Qwen)', value: 'qwen' },
{ label: '自定义', value: 'custom' },
]
// 模型选项(根据提供商动态加载)
const modelOptions = ref<Array<{ label: string; value: string }>>([])
// 表单数据
const formData = reactive({
name: '',
provider: '',
model_name: '',
api_key: '',
api_url: '',
max_tokens: 4000,
temperature: 0.7,
top_p: 0.9,
frequency_penalty: 0,
presence_penalty: 0,
is_enabled: true,
description: '',
})
// 表单验证规则
const rules: FormRules = {
name: [
{ required: true, message: '请输入配置名称', trigger: 'blur' },
{ min: 2, max: 50, message: '配置名称长度在 2 到 50 个字符', trigger: 'blur' },
],
provider: [{ required: true, message: '请选择提供商', trigger: 'change' }],
model_name: [{ required: true, message: '请选择模型名称', trigger: 'change' }],
api_key: [{ required: true, message: '请输入API密钥', trigger: 'blur' }],
api_url: [
{ required: true, message: '请输入API地址', trigger: 'blur' },
{ type: 'url', message: '请输入正确的URL格式', trigger: 'blur' },
],
max_tokens: [{ required: true, message: '请输入最大令牌数', trigger: 'blur' }],
}
// 计算属性
const isEdit = computed(() => !!props.editData)
// 监听对话框显示状态
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.editData) {
Object.assign(formData, props.editData)
} else {
resetForm()
}
loadModelOptions()
}
}
)
// 监听提供商变化
watch(
() => formData.provider,
() => {
loadModelOptions()
formData.model_name = ''
}
)
// 重置表单
const resetForm = () => {
Object.assign(formData, {
name: '',
provider: '',
model_name: '',
api_key: '',
api_url: '',
max_tokens: 4000,
temperature: 0.7,
top_p: 0.9,
frequency_penalty: 0,
presence_penalty: 0,
is_enabled: true,
description: '',
})
formRef.value?.clearValidate()
}
// 加载模型选项
const loadModelOptions = () => {
modelOptions.value = getDefaultModels(formData.provider)
}
// 获取默认模型列表
const getDefaultModels = (provider: string) => {
const modelMap: Record<string, Array<{ label: string; value: string }>> = {
deepseek: [
{ label: 'DeepSeek-V2', value: 'deepseek-chat' },
{ label: 'DeepSeek-Coder', value: 'deepseek-coder' },
],
qwen: [
{ label: 'Qwen2.5-72B-Instruct', value: 'qwen2.5-72b-instruct' },
{ label: 'Qwen2.5-32B-Instruct', value: 'qwen2.5-32b-instruct' },
{ label: 'Qwen2.5-14B-Instruct', value: 'qwen2.5-14b-instruct' },
{ label: 'Qwen2.5-7B-Instruct', value: 'qwen2.5-7b-instruct' },
],
custom: [{ label: '自定义模型', value: 'custom' }],
}
return modelMap[provider] || []
}
// 处理提供商变化
const handleProviderChange = () => {
// 根据提供商设置默认API地址
const defaultUrls: Record<string, string> = {
deepseek: 'https://api.deepseek.com/v1/chat/completions',
qwen: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
custom: '',
}
formData.api_url = defaultUrls[formData.provider] || ''
}
// 移除测试连接功能
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
submitting.value = true
try {
if (isEdit.value && props.editData?.id) {
llmConfigStorage.update(props.editData.id, formData)
ElMessage.success('更新成功')
} else {
llmConfigStorage.create(formData)
ElMessage.success('创建成功')
}
emit('success')
handleClose()
} catch (error: any) {
ElMessage.error(error.message || '操作失败')
} finally {
submitting.value = false
}
}
// 关闭对话框
const handleClose = () => {
emit('update:visible', false)
resetForm()
}
</script>
<style lang="scss" scoped>
.llm-config-form {
.form-tip {
margin-left: 10px;
color: #909399;
font-size: 12px;
}
}
.dialog-footer {
text-align: right;
}
</style>
<template>
<div class="llm-config-list">
<!-- 搜索栏 -->
<div class="search-bar">
<el-row :gutter="20">
<el-col :span="6">
<el-input
v-model="searchForm.name"
placeholder="请输入配置名称"
clearable
@clear="handleSearch"
@keyup.enter="handleSearch">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-col>
<el-col :span="4">
<el-select v-model="searchForm.provider" placeholder="选择提供商" clearable @change="handleSearch">
<el-option
v-for="provider in providerOptions"
:key="provider.value"
:label="provider.label"
:value="provider.value" />
</el-select>
</el-col>
<el-col :span="4">
<el-select v-model="searchForm.is_enabled" placeholder="启用状态" clearable @change="handleSearch">
<el-option label="已启用" value="true" />
<el-option label="已禁用" value="false" />
</el-select>
</el-col>
<el-col :span="6">
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-col>
<el-col :span="4" class="text-right">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增配置
</el-button>
</el-col>
</el-row>
</div>
<!-- 表格 -->
<el-table
v-loading="loading"
:data="tableData"
stripe
style="width: 100%"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="配置名称" min-width="150" />
<el-table-column prop="provider" label="提供商" width="120">
<template #default="{ row }">
<el-tag :type="getProviderTagType(row.provider) as any">
{{ getProviderLabel(row.provider) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="model_name" label="模型名称" min-width="180" />
<el-table-column prop="api_url" label="API地址" min-width="200" show-overflow-tooltip />
<el-table-column prop="max_tokens" label="最大令牌" width="100" />
<el-table-column prop="temperature" label="温度" width="80" />
<el-table-column prop="is_enabled" label="状态" width="80">
<template #default="{ row }">
<el-switch v-model="row.is_enabled" @change="handleToggleStatus(row)" :loading="row.statusLoading" />
</template>
</el-table-column>
<el-table-column prop="created_time" label="创建时间" width="160">
<template #default="{ row }">
{{ formatDate(row.created_time) }}
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)"> 编辑 </el-button>
<el-button type="danger" size="small" @click="handleDelete(row)"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
<!-- 移除分页组件 -->
<!-- 批量操作 -->
<div v-if="selectedRows.length > 0" class="batch-actions">
<el-alert :title="`已选择 ${selectedRows.length} 项`" type="info" show-icon :closable="false">
<template #default>
<el-button type="danger" size="small" @click="handleBatchDelete"> 批量删除 </el-button>
<el-button type="warning" size="small" @click="handleBatchDisable"> 批量禁用 </el-button>
<el-button type="success" size="small" @click="handleBatchEnable"> 批量启用 </el-button>
</template>
</el-alert>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
import { llmConfigStorage } from '@/utils/llmStorage'
interface Props {
onEdit: (data: any) => void
}
const props = defineProps<Props>()
// 搜索表单
const searchForm = reactive({
name: '',
provider: '',
is_enabled: '',
})
// 提供商选项
const providerOptions = [
{ label: 'DeepSeek', value: 'deepseek' },
{ label: '通义千问', value: 'qwen' },
{ label: '自定义', value: 'custom' },
]
// 表格数据
const tableData = ref<any[]>([])
const loading = ref(false)
const selectedRows = ref<any[]>([])
// 移除分页相关变量
// 获取提供商标签类型
const getProviderTagType = (provider: string) => {
const typeMap: Record<string, string> = {
deepseek: 'primary',
qwen: 'success',
custom: 'danger',
}
return typeMap[provider] || 'info'
}
// 获取提供商标签
const getProviderLabel = (provider: string) => {
const option = providerOptions.find((item) => item.value === provider)
return option?.label || provider
}
// 格式化日期
const formatDate = (dateStr?: string) => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString('zh-CN')
}
// 加载数据
const loadData = () => {
loading.value = true
try {
// 获取所有数据
let allData = llmConfigStorage.getAll()
// 应用搜索过滤
if (searchForm.name) {
allData = allData.filter((item: any) => item.name.toLowerCase().includes(searchForm.name.toLowerCase()))
}
if (searchForm.provider) {
allData = allData.filter((item: any) => item.provider === searchForm.provider)
}
if (searchForm.is_enabled) {
const enabled = searchForm.is_enabled === 'true'
allData = allData.filter((item: any) => item.is_enabled === enabled)
}
// 直接显示所有数据,不分页
tableData.value = allData
} catch (error: any) {
ElMessage.error(error.message || '加载数据失败')
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
loadData()
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, {
name: '',
provider: '',
is_enabled: '',
})
handleSearch()
}
// 新增
const handleAdd = () => {
props.onEdit({})
}
// 编辑
const handleEdit = (row: any) => {
props.onEdit(row)
}
// 移除测试连接功能
// 删除
const handleDelete = async (row: any) => {
try {
await ElMessageBox.confirm(`确定要删除配置 "${row.name}" 吗?`, '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
if (row.id) {
llmConfigStorage.delete(row.id)
ElMessage.success('删除成功')
loadData()
}
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message || '删除失败')
}
}
}
// 切换状态
const handleToggleStatus = async (row: any) => {
if (!row.id) return
row.statusLoading = true
try {
llmConfigStorage.update(row.id, { is_enabled: row.is_enabled })
ElMessage.success(row.is_enabled ? '启用成功' : '禁用成功')
} catch (error: any) {
row.is_enabled = !row.is_enabled // 回滚状态
ElMessage.error(error.message || '操作失败')
} finally {
row.statusLoading = false
}
}
// 选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 批量删除
const handleBatchDelete = async () => {
try {
await ElMessageBox.confirm(`确定要删除选中的 ${selectedRows.value.length} 个配置吗?`, '确认批量删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
selectedRows.value.filter((item) => item.id).forEach((item) => llmConfigStorage.delete(item.id))
ElMessage.success('批量删除成功')
loadData()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message || '批量删除失败')
}
}
}
// 批量禁用
const handleBatchDisable = async () => {
try {
selectedRows.value
.filter((item) => item.id)
.forEach((item) => llmConfigStorage.update(item.id, { is_enabled: false }))
ElMessage.success('批量禁用成功')
loadData()
} catch (error: any) {
ElMessage.error(error.message || '批量禁用失败')
}
}
// 批量启用
const handleBatchEnable = async () => {
try {
selectedRows.value
.filter((item) => item.id)
.forEach((item) => llmConfigStorage.update(item.id, { is_enabled: true }))
ElMessage.success('批量启用成功')
loadData()
} catch (error: any) {
ElMessage.error(error.message || '批量启用失败')
}
}
// 移除分页变化方法
// 暴露刷新方法
defineExpose({
refresh: loadData,
})
onMounted(() => {
loadData()
})
</script>
<style lang="scss" scoped>
.llm-config-list {
.search-bar {
margin-bottom: 20px;
padding: 20px;
background: #f5f7fa;
border-radius: 6px;
}
.pagination-wrapper {
margin-top: 20px;
text-align: right;
}
.batch-actions {
margin-top: 20px;
}
.text-right {
text-align: right;
}
}
</style>
<template>
<div class="sensitive-person-manager">
<!-- 搜索栏 -->
<div class="search-bar">
<el-row :gutter="20">
<el-col :span="6">
<el-input
v-model="searchForm.name"
placeholder="请输入敏感人物姓名"
clearable
@clear="handleSearch"
@keyup.enter="handleSearch">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-col>
<el-col :span="4">
<el-select v-model="searchForm.level" placeholder="敏感级别" clearable @change="handleSearch">
<el-option label="低" value="low" />
<el-option label="中" value="medium" />
<el-option label="高" value="high" />
</el-select>
</el-col>
<el-col :span="6">
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-col>
<el-col :span="8" class="text-right">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增敏感人物
</el-button>
</el-col>
</el-row>
</div>
<!-- 表格 -->
<el-table
v-loading="loading"
:data="tableData"
stripe
style="width: 100%"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="name" label="姓名" min-width="120">
<template #default="{ row }">
<span :class="{ 'sensitive-person': true, [`level-${row.level}`]: true }">
{{ row.name }}
</span>
</template>
</el-table-column>
<el-table-column prop="aliases" label="别名" min-width="200">
<template #default="{ row }">
<el-tag v-for="alias in row.aliases" :key="alias" size="small" class="alias-tag">
{{ alias }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="level" label="敏感级别" width="100">
<template #default="{ row }">
<el-tag :type="getLevelTagType(row.level) as any">
{{ getLevelLabel(row.level) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="is_enabled" label="状态" width="80">
<template #default="{ row }">
<el-switch v-model="row.is_enabled" @change="handleToggleStatus(row)" :loading="row.statusLoading" />
</template>
</el-table-column>
<el-table-column prop="created_time" label="创建时间" width="160">
<template #default="{ row }">
{{ formatDate(row.created_time) }}
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)"> 编辑 </el-button>
<el-button type="danger" size="small" @click="handleDelete(row)"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
<!-- 移除分页组件 -->
<!-- 批量操作 -->
<div v-if="selectedRows.length > 0" class="batch-actions">
<el-alert :title="`已选择 ${selectedRows.length} 项`" type="info" show-icon :closable="false">
<template #default>
<el-button type="danger" size="small" @click="handleBatchDelete"> 批量删除 </el-button>
<el-button type="warning" size="small" @click="handleBatchDisable"> 批量禁用 </el-button>
<el-button type="success" size="small" @click="handleBatchEnable"> 批量启用 </el-button>
</template>
</el-alert>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog v-model="formVisible" :title="isEdit ? '编辑敏感人物' : '新增敏感人物'" width="600px">
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="姓名" prop="name">
<el-input v-model="formData.name" placeholder="请输入敏感人物姓名" />
</el-form-item>
<el-form-item label="别名" prop="aliases">
<el-input
v-model="aliasesText"
type="textarea"
:rows="3"
placeholder="请输入别名,每行一个"
@blur="handleAliasesChange" />
<div class="form-tip">每行输入一个别名,系统会自动识别</div>
</el-form-item>
<el-form-item label="敏感级别" prop="level">
<el-select v-model="formData.level" placeholder="请选择敏感级别">
<el-option label="低" value="low" />
<el-option label="中" value="medium" />
<el-option label="高" value="high" />
</el-select>
</el-form-item>
<el-form-item label="启用状态" prop="is_enabled">
<el-switch v-model="formData.is_enabled" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="formVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
{{ isEdit ? '更新' : '创建' }}
</el-button>
</template>
</el-dialog>
<!-- 移除测试检测对话框 -->
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import { sensitivePersonStorage } from '@/utils/llmStorage'
// 搜索表单
const searchForm = reactive({
name: '',
level: '',
})
// 表格数据
const tableData = ref<any[]>([])
const loading = ref(false)
const selectedRows = ref<any[]>([])
// 移除分页相关变量
// 表单相关
const formVisible = ref(false)
const formRef = ref<FormInstance>()
const submitting = ref(false)
const isEdit = ref(false)
const formData = reactive({
name: '',
aliases: [],
level: 'medium',
is_enabled: true,
})
const aliasesText = ref('')
// 移除测试相关变量
// 表单验证规则
const rules: FormRules = {
name: [
{ required: true, message: '请输入敏感人物姓名', trigger: 'blur' },
{ min: 1, max: 50, message: '姓名长度在 1 到 50 个字符', trigger: 'blur' },
],
level: [{ required: true, message: '请选择敏感级别', trigger: 'change' }],
}
// 获取级别标签类型
const getLevelTagType = (level: string) => {
const typeMap: Record<string, string> = {
low: 'info',
medium: 'warning',
high: 'danger',
}
return typeMap[level] || 'info'
}
// 获取级别标签
const getLevelLabel = (level: string) => {
const labelMap: Record<string, string> = {
low: '低',
medium: '中',
high: '高',
}
return labelMap[level] || level
}
// 格式化日期
const formatDate = (dateStr?: string) => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString('zh-CN')
}
// 处理别名变化
const handleAliasesChange = () => {
const aliases = aliasesText.value
.split('\n')
.map((alias) => alias.trim())
.filter((alias) => alias)
formData.aliases = aliases as any
}
// 加载数据
const loadData = () => {
loading.value = true
try {
// 获取所有数据
let allData = sensitivePersonStorage.getAll()
// 应用搜索过滤
if (searchForm.name) {
allData = allData.filter(
(item: any) =>
item.name.toLowerCase().includes(searchForm.name.toLowerCase()) ||
item.aliases.some((alias: string) => alias.toLowerCase().includes(searchForm.name.toLowerCase()))
)
}
if (searchForm.level) {
allData = allData.filter((item: any) => item.level === searchForm.level)
}
// 直接显示所有数据,不分页
tableData.value = allData
} catch (error: any) {
ElMessage.error(error.message || '加载数据失败')
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
loadData()
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, {
name: '',
level: '',
})
handleSearch()
}
// 新增
const handleAdd = () => {
isEdit.value = false
Object.assign(formData, {
name: '',
aliases: [],
level: 'medium',
is_enabled: true,
})
aliasesText.value = ''
formVisible.value = true
}
// 编辑
const handleEdit = (row: any) => {
isEdit.value = true
Object.assign(formData, row)
aliasesText.value = row.aliases.join('\n')
formVisible.value = true
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
// 更新别名
handleAliasesChange()
submitting.value = true
try {
if (isEdit.value && (formData as any).id) {
sensitivePersonStorage.update((formData as any).id, formData)
ElMessage.success('更新成功')
} else {
sensitivePersonStorage.create(formData)
ElMessage.success('创建成功')
}
formVisible.value = false
loadData()
} catch (error: any) {
ElMessage.error(error.message || '操作失败')
} finally {
submitting.value = false
}
}
// 删除
const handleDelete = async (row: any) => {
try {
await ElMessageBox.confirm(`确定要删除敏感人物 "${row.name}" 吗?`, '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
if (row.id) {
sensitivePersonStorage.delete(row.id)
ElMessage.success('删除成功')
loadData()
}
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message || '删除失败')
}
}
}
// 切换状态
const handleToggleStatus = async (row: any) => {
if (!row.id) return
row.statusLoading = true
try {
sensitivePersonStorage.update(row.id, { is_enabled: row.is_enabled })
ElMessage.success(row.is_enabled ? '启用成功' : '禁用成功')
} catch (error: any) {
row.is_enabled = !row.is_enabled // 回滚状态
ElMessage.error(error.message || '操作失败')
} finally {
row.statusLoading = false
}
}
// 移除测试检测功能
// 选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 批量删除
const handleBatchDelete = async () => {
try {
await ElMessageBox.confirm(`确定要删除选中的 ${selectedRows.value.length} 个敏感人物吗?`, '确认批量删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
selectedRows.value.filter((item) => item.id).forEach((item) => sensitivePersonStorage.delete(item.id))
ElMessage.success('批量删除成功')
loadData()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message || '批量删除失败')
}
}
}
// 批量禁用
const handleBatchDisable = async () => {
try {
selectedRows.value
.filter((item) => item.id)
.forEach((item) => sensitivePersonStorage.update(item.id, { is_enabled: false }))
ElMessage.success('批量禁用成功')
loadData()
} catch (error: any) {
ElMessage.error(error.message || '批量禁用失败')
}
}
// 批量启用
const handleBatchEnable = async () => {
try {
selectedRows.value
.filter((item) => item.id)
.forEach((item) => sensitivePersonStorage.update(item.id, { is_enabled: true }))
ElMessage.success('批量启用成功')
loadData()
} catch (error: any) {
ElMessage.error(error.message || '批量启用失败')
}
}
// 移除分页变化方法
onMounted(() => {
loadData()
})
</script>
<style lang="scss" scoped>
.sensitive-person-manager {
.search-bar {
margin-bottom: 20px;
padding: 20px;
background: #f5f7fa;
border-radius: 6px;
}
.pagination-wrapper {
margin-top: 20px;
text-align: right;
}
.batch-actions {
margin-top: 20px;
}
.text-right {
text-align: right;
}
.sensitive-person {
font-weight: bold;
&.level-low {
color: #409eff;
}
&.level-medium {
color: #e6a23c;
}
&.level-high {
color: #f56c6c;
}
}
.alias-tag {
margin-right: 5px;
margin-bottom: 5px;
}
.form-tip {
color: #909399;
font-size: 12px;
margin-top: 5px;
}
.test-result {
margin-top: 20px;
padding: 15px;
background: #f5f7fa;
border-radius: 6px;
.result-item {
display: flex;
align-items: center;
margin-bottom: 10px;
gap: 10px;
.result-name {
font-weight: bold;
color: #f56c6c;
}
.result-alias {
color: #909399;
font-size: 12px;
}
.result-position {
color: #909399;
font-size: 12px;
}
}
}
}
</style>
<template>
<div class="sensitive-word-manager">
<!-- 搜索栏 -->
<div class="search-bar">
<el-row :gutter="20">
<el-col :span="6">
<el-input
v-model="searchForm.word"
placeholder="请输入敏感词"
clearable
@clear="handleSearch"
@keyup.enter="handleSearch">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-col>
<el-col :span="4">
<el-select v-model="searchForm.level" placeholder="敏感级别" clearable @change="handleSearch">
<el-option label="低" value="low" />
<el-option label="中" value="medium" />
<el-option label="高" value="high" />
</el-select>
</el-col>
<el-col :span="6">
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-col>
<el-col :span="8" class="text-right">
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增敏感词
</el-button>
</el-col>
</el-row>
</div>
<!-- 表格 -->
<el-table
v-loading="loading"
:data="tableData"
stripe
style="width: 100%"
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" />
<el-table-column prop="word" label="敏感词" min-width="150">
<template #default="{ row }">
<span :class="{ 'sensitive-word': true, [`level-${row.level}`]: true }">
{{ row.word }}
</span>
</template>
</el-table-column>
<el-table-column prop="level" label="敏感级别" width="100">
<template #default="{ row }">
<el-tag :type="getLevelTagType(row.level) as any">
{{ getLevelLabel(row.level) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="replacement" label="替换词" min-width="150">
<template #default="{ row }">
{{ row.replacement || '***' }}
</template>
</el-table-column>
<el-table-column prop="is_enabled" label="状态" width="80">
<template #default="{ row }">
<el-switch v-model="row.is_enabled" @change="handleToggleStatus(row)" :loading="row.statusLoading" />
</template>
</el-table-column>
<el-table-column prop="created_time" label="创建时间" width="160">
<template #default="{ row }">
{{ formatDate(row.created_time) }}
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click="handleEdit(row)"> 编辑 </el-button>
<el-button type="danger" size="small" @click="handleDelete(row)"> 删除 </el-button>
</template>
</el-table-column>
</el-table>
<!-- 移除分页组件 -->
<!-- 批量操作 -->
<div v-if="selectedRows.length > 0" class="batch-actions">
<el-alert :title="`已选择 ${selectedRows.length} 项`" type="info" show-icon :closable="false">
<template #default>
<el-button type="danger" size="small" @click="handleBatchDelete"> 批量删除 </el-button>
<el-button type="warning" size="small" @click="handleBatchDisable"> 批量禁用 </el-button>
<el-button type="success" size="small" @click="handleBatchEnable"> 批量启用 </el-button>
</template>
</el-alert>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog v-model="formVisible" :title="isEdit ? '编辑敏感词' : '新增敏感词'" width="500px">
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="敏感词" prop="word">
<el-input v-model="formData.word" placeholder="请输入敏感词" />
</el-form-item>
<el-form-item label="敏感级别" prop="level">
<el-select v-model="formData.level" placeholder="请选择敏感级别">
<el-option label="低" value="low" />
<el-option label="中" value="medium" />
<el-option label="高" value="high" />
</el-select>
</el-form-item>
<el-form-item label="替换词" prop="replacement">
<el-input v-model="formData.replacement" placeholder="请输入替换词(可选)" />
</el-form-item>
<el-form-item label="启用状态" prop="is_enabled">
<el-switch v-model="formData.is_enabled" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="formVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
{{ isEdit ? '更新' : '创建' }}
</el-button>
</template>
</el-dialog>
<!-- 移除批量导入和测试检测对话框 -->
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
import type { FormInstance, FormRules } from 'element-plus'
import { sensitiveWordStorage } from '@/utils/llmStorage'
// 搜索表单
const searchForm = reactive({
word: '',
level: '',
})
// 表格数据
const tableData = ref<any[]>([])
const loading = ref(false)
const selectedRows = ref<any[]>([])
// 移除分页相关变量
// 表单相关
const formVisible = ref(false)
const formRef = ref<FormInstance>()
const submitting = ref(false)
const isEdit = ref(false)
const formData = reactive({
word: '',
level: 'medium',
replacement: '',
is_enabled: true,
})
// 移除导入和测试相关变量
// 表单验证规则
const rules: FormRules = {
word: [
{ required: true, message: '请输入敏感词', trigger: 'blur' },
{ min: 1, max: 50, message: '敏感词长度在 1 到 50 个字符', trigger: 'blur' },
],
level: [{ required: true, message: '请选择敏感级别', trigger: 'change' }],
}
// 获取级别标签类型
const getLevelTagType = (level: string) => {
const typeMap: Record<string, string> = {
low: 'info',
medium: 'warning',
high: 'danger',
}
return typeMap[level] || 'info'
}
// 获取级别标签
const getLevelLabel = (level: string) => {
const labelMap: Record<string, string> = {
low: '低',
medium: '中',
high: '高',
}
return labelMap[level] || level
}
// 格式化日期
const formatDate = (dateStr?: string) => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString('zh-CN')
}
// 加载数据
const loadData = () => {
loading.value = true
try {
// 获取所有数据
let allData = sensitiveWordStorage.getAll()
// 应用搜索过滤
if (searchForm.word) {
allData = allData.filter((item: any) => item.word.toLowerCase().includes(searchForm.word.toLowerCase()))
}
if (searchForm.level) {
allData = allData.filter((item: any) => item.level === searchForm.level)
}
// 直接显示所有数据,不分页
tableData.value = allData
} catch (error: any) {
ElMessage.error(error.message || '加载数据失败')
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
loadData()
}
// 重置搜索
const handleReset = () => {
Object.assign(searchForm, {
word: '',
level: '',
})
handleSearch()
}
// 新增
const handleAdd = () => {
isEdit.value = false
Object.assign(formData, {
word: '',
level: 'medium',
replacement: '',
is_enabled: true,
})
formVisible.value = true
}
// 编辑
const handleEdit = (row: any) => {
isEdit.value = true
Object.assign(formData, row)
formVisible.value = true
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
submitting.value = true
try {
if (isEdit.value && (formData as any).id) {
sensitiveWordStorage.update((formData as any).id, formData)
ElMessage.success('更新成功')
} else {
sensitiveWordStorage.create(formData)
ElMessage.success('创建成功')
}
formVisible.value = false
loadData()
} catch (error: any) {
ElMessage.error(error.message || '操作失败')
} finally {
submitting.value = false
}
}
// 删除
const handleDelete = async (row: any) => {
try {
await ElMessageBox.confirm(`确定要删除敏感词 "${row.word}" 吗?`, '确认删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
if (row.id) {
sensitiveWordStorage.delete(row.id)
ElMessage.success('删除成功')
loadData()
}
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message || '删除失败')
}
}
}
// 切换状态
const handleToggleStatus = async (row: any) => {
if (!row.id) return
row.statusLoading = true
try {
sensitiveWordStorage.update(row.id, { is_enabled: row.is_enabled })
ElMessage.success(row.is_enabled ? '启用成功' : '禁用成功')
} catch (error: any) {
row.is_enabled = !row.is_enabled // 回滚状态
ElMessage.error(error.message || '操作失败')
} finally {
row.statusLoading = false
}
}
// 移除批量导入和测试检测功能
// 选择变化
const handleSelectionChange = (selection: any[]) => {
selectedRows.value = selection
}
// 批量删除
const handleBatchDelete = async () => {
try {
await ElMessageBox.confirm(`确定要删除选中的 ${selectedRows.value.length} 个敏感词吗?`, '确认批量删除', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
selectedRows.value.filter((item) => item.id).forEach((item) => sensitiveWordStorage.delete(item.id))
ElMessage.success('批量删除成功')
loadData()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error.message || '批量删除失败')
}
}
}
// 批量禁用
const handleBatchDisable = async () => {
try {
selectedRows.value
.filter((item) => item.id)
.forEach((item) => sensitiveWordStorage.update(item.id, { is_enabled: false }))
ElMessage.success('批量禁用成功')
loadData()
} catch (error: any) {
ElMessage.error(error.message || '批量禁用失败')
}
}
// 批量启用
const handleBatchEnable = async () => {
try {
selectedRows.value
.filter((item) => item.id)
.forEach((item) => sensitiveWordStorage.update(item.id, { is_enabled: true }))
ElMessage.success('批量启用成功')
loadData()
} catch (error: any) {
ElMessage.error(error.message || '批量启用失败')
}
}
// 移除分页变化方法
onMounted(() => {
loadData()
})
</script>
<style lang="scss" scoped>
.sensitive-word-manager {
.search-bar {
margin-bottom: 20px;
padding: 20px;
background: #f5f7fa;
border-radius: 6px;
}
.pagination-wrapper {
margin-top: 20px;
text-align: right;
}
.batch-actions {
margin-top: 20px;
}
.text-right {
text-align: right;
}
.sensitive-word {
font-weight: bold;
&.level-low {
color: #409eff;
}
&.level-medium {
color: #e6a23c;
}
&.level-high {
color: #f56c6c;
}
}
.test-result {
margin-top: 20px;
padding: 15px;
background: #f5f7fa;
border-radius: 6px;
.result-item {
display: flex;
align-items: center;
margin-bottom: 10px;
gap: 10px;
.result-word {
font-weight: bold;
color: #f56c6c;
}
.result-position {
color: #909399;
font-size: 12px;
}
}
}
}
</style>
<template>
<div class="text-process-config">
<el-card class="config-card">
<template #header>
<div class="card-header">
<span>文本处理配置</span>
<el-button type="primary" @click="handleSave" :loading="saving"> 保存配置 </el-button>
</div>
</template>
<el-form ref="formRef" :model="configData" :rules="rules" label-width="200px" class="config-form">
<!-- 意识形态检测配置 -->
<el-divider content-position="left">意识形态检测配置</el-divider>
<el-form-item label="启用意识形态检测" prop="enable_ideology_check">
<el-switch v-model="configData.enable_ideology_check" />
<div class="form-tip">启用后将对文本进行意识形态相关内容的检测和过滤</div>
</el-form-item>
<!-- 敏感词过滤配置 -->
<el-divider content-position="left">敏感词过滤配置</el-divider>
<el-form-item label="启用敏感词过滤" prop="enable_sensitive_word_filter">
<el-switch v-model="configData.enable_sensitive_word_filter" />
<div class="form-tip">启用后将检测并处理文本中的敏感词</div>
</el-form-item>
<el-form-item
v-if="configData.enable_sensitive_word_filter"
label="自动替换敏感词"
prop="auto_replace_sensitive_words">
<el-switch v-model="configData.auto_replace_sensitive_words" />
<div class="form-tip">启用后自动将敏感词替换为指定内容</div>
</el-form-item>
<el-form-item
v-if="configData.enable_sensitive_word_filter && configData.auto_replace_sensitive_words"
label="自定义替换内容"
prop="custom_replacement">
<el-input
v-model="configData.custom_replacement"
placeholder="请输入替换内容,默认为***"
maxlength="50"
show-word-limit />
<div class="form-tip">当检测到敏感词时,将替换为此内容</div>
</el-form-item>
<!-- 敏感人物过滤配置 -->
<el-divider content-position="left">敏感人物过滤配置</el-divider>
<el-form-item label="启用敏感人物过滤" prop="enable_sensitive_person_filter">
<el-switch v-model="configData.enable_sensitive_person_filter" />
<div class="form-tip">启用后将检测并处理文本中涉及的敏感人物</div>
</el-form-item>
</el-form>
</el-card>
<!-- 移除测试区域 -->
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { textProcessStorage } from '@/utils/llmStorage'
// 表单引用
const formRef = ref<FormInstance>()
const saving = ref(false)
// 配置数据
const configData = reactive({
enable_ideology_check: true,
enable_sensitive_word_filter: true,
enable_sensitive_person_filter: true,
auto_replace_sensitive_words: true,
custom_replacement: '***',
})
// 移除测试相关变量
// 表单验证规则
const rules: FormRules = {
custom_replacement: [{ max: 50, message: '替换内容不能超过50个字符', trigger: 'blur' }],
}
// 获取级别标签类型
const getLevelTagType = (level: string) => {
const typeMap: Record<string, string> = {
low: 'info',
medium: 'warning',
high: 'danger',
}
return typeMap[level] || 'info'
}
// 获取级别标签
const getLevelLabel = (level: string) => {
const labelMap: Record<string, string> = {
low: '低',
medium: '中',
high: '高',
}
return labelMap[level] || level
}
// 移除测试相关方法
// 加载配置
const loadConfig = () => {
try {
const config = textProcessStorage.get()
Object.assign(configData, config)
} catch (error: any) {
ElMessage.error(error.message || '加载配置失败')
}
}
// 保存配置
const handleSave = async () => {
if (!formRef.value) return
const valid = await formRef.value.validate()
if (!valid) return
saving.value = true
try {
textProcessStorage.update(configData)
ElMessage.success('配置保存成功')
} catch (error: any) {
ElMessage.error(error.message || '保存失败')
} finally {
saving.value = false
}
}
// 移除测试文本处理功能
onMounted(() => {
loadConfig()
})
</script>
<style lang="scss" scoped>
.text-process-config {
.config-card,
.test-card {
margin-bottom: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.config-form {
.form-tip {
color: #909399;
font-size: 12px;
margin-top: 5px;
}
}
.test-result {
margin-top: 20px;
.result-section {
margin-bottom: 20px;
h4 {
margin-bottom: 10px;
color: #303133;
}
}
.sensitive-words,
.sensitive-persons {
margin-top: 10px;
.word-tag,
.person-tag {
margin-right: 8px;
margin-bottom: 8px;
}
}
.processed-text {
padding: 15px;
background: #f5f7fa;
border-radius: 6px;
border: 1px solid #e4e7ed;
line-height: 1.6;
color: #606266;
}
.score-tip {
margin-top: 10px;
color: #909399;
font-size: 12px;
}
.score-text {
color: #fff;
font-weight: bold;
}
}
}
</style>
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/system/llm',
component: AppLayout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
}
]
<script setup lang="ts">
import { ref } from 'vue'
import { ElTabs, ElTabPane } from 'element-plus'
// 移除类型导入
import LLMConfigList from '../components/LLMConfigList.vue'
import LLMConfigForm from '../components/LLMConfigForm.vue'
import SensitiveWordManager from '../components/SensitiveWordManager.vue'
import SensitivePersonManager from '../components/SensitivePersonManager.vue'
import TextProcessConfig from '../components/TextProcessConfig.vue'
// 当前激活的标签页
const activeTab = ref('llm-config')
// 表单相关
const formVisible = ref(false)
const editData = ref<any>()
// 处理编辑
const handleEdit = (data: any) => {
editData.value = data
formVisible.value = true
}
// 处理表单成功
const handleFormSuccess = () => {
formVisible.value = false
editData.value = undefined
// 刷新列表
if (listRef.value) {
listRef.value.refresh()
}
}
// 列表引用
const listRef = ref()
</script>
<template>
<div class="llm-management">
<el-tabs v-model="activeTab" type="border-card" class="management-tabs">
<!-- 大模型配置 -->
<el-tab-pane label="大模型配置" name="llm-config">
<LLMConfigList :on-edit="handleEdit" ref="listRef" />
</el-tab-pane>
<!-- 敏感词管理 -->
<el-tab-pane label="敏感词管理" name="sensitive-word">
<SensitiveWordManager />
</el-tab-pane>
<!-- 敏感人物管理 -->
<el-tab-pane label="敏感人物管理" name="sensitive-person">
<SensitivePersonManager />
</el-tab-pane>
<!-- 文本处理配置 -->
<el-tab-pane label="文本处理配置" name="text-process">
<TextProcessConfig />
</el-tab-pane>
</el-tabs>
<!-- 大模型配置表单 -->
<LLMConfigForm v-model:visible="formVisible" :edit-data="editData" @success="handleFormSuccess" />
</div>
</template>
<style lang="scss" scoped>
.llm-management {
.management-tabs {
min-height: 600px;
:deep(.el-tabs__content) {
padding: 20px;
}
}
}
</style>
...@@ -7,8 +7,8 @@ const httpRequest = axios.create({ ...@@ -7,8 +7,8 @@ const httpRequest = axios.create({
timeout: 60000, timeout: 60000,
withCredentials: true, withCredentials: true,
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded',
} },
}) })
// 请求拦截 // 请求拦截
httpRequest.interceptors.request.use( httpRequest.interceptors.request.use(
...@@ -27,7 +27,7 @@ httpRequest.interceptors.request.use( ...@@ -27,7 +27,7 @@ httpRequest.interceptors.request.use(
}, },
function (error) { function (error) {
return Promise.reject(error) return Promise.reject(error)
} },
) )
// 响应拦截 // 响应拦截
...@@ -43,6 +43,14 @@ httpRequest.interceptors.response.use( ...@@ -43,6 +43,14 @@ httpRequest.interceptors.response.use(
ElMessage.error(data.message || data.msg) ElMessage.error(data.message || data.msg)
return Promise.reject(data) return Promise.reject(data)
} }
if (import.meta.env.VITE_STATIC_URL && import.meta.env.MODE === 'school') {
try {
const regex = /(http|https):\/\/(.*?)saas-lab-api/gi
return JSON.parse(JSON.stringify(data).replaceAll(regex, import.meta.env.VITE_STATIC_URL))
} catch (error) {
console.log(error)
}
}
return data return data
}, },
function (error) { function (error) {
...@@ -62,7 +70,7 @@ httpRequest.interceptors.response.use( ...@@ -62,7 +70,7 @@ httpRequest.interceptors.response.use(
console.log(error) console.log(error)
} }
return Promise.reject(error.response || error) return Promise.reject(error.response || error)
} },
) )
export default httpRequest export default httpRequest
// 本地存储工具函数
// 存储键名常量
const STORAGE_KEYS = {
LLM_CONFIGS: 'llm_configs',
SENSITIVE_WORDS: 'sensitive_words',
SENSITIVE_PERSONS: 'sensitive_persons',
TEXT_PROCESS_CONFIG: 'text_process_config',
LLM_FEATURES: 'llm_features',
}
// 生成唯一ID
const generateId = () => {
return Date.now().toString(36) + Math.random().toString(36).substr(2)
}
// 获取当前时间戳
const getCurrentTime = () => {
return new Date().toISOString()
}
// 通用存储操作
const storage = {
// 获取数据
get<T>(key: string, defaultValue: T): T {
try {
const item = localStorage.getItem(key)
return item ? JSON.parse(item) : defaultValue
} catch (error) {
console.error(`Error getting data from localStorage for key ${key}:`, error)
return defaultValue
}
},
// 设置数据
set<T>(key: string, value: T): void {
try {
localStorage.setItem(key, JSON.stringify(value))
} catch (error) {
console.error(`Error setting data to localStorage for key ${key}:`, error)
}
},
// 删除数据
remove(key: string): void {
try {
localStorage.removeItem(key)
} catch (error) {
console.error(`Error removing data from localStorage for key ${key}:`, error)
}
},
}
// 大模型配置相关操作
export const llmConfigStorage = {
// 获取所有配置
getAll() {
return storage.get(STORAGE_KEYS.LLM_CONFIGS, [] as any[])
},
// 根据ID获取配置
getById(id: string) {
const configs = this.getAll()
return configs.find((config: any) => config.id === id)
},
// 创建配置
create(config: any) {
const configs = this.getAll()
const newConfig = {
...config,
id: generateId(),
created_time: getCurrentTime(),
updated_time: getCurrentTime(),
}
configs.push(newConfig)
storage.set(STORAGE_KEYS.LLM_CONFIGS, configs)
return newConfig
},
// 更新配置
update(id: string, updates: any) {
const configs = this.getAll()
const index = configs.findIndex((config: any) => config.id === id)
if (index !== -1) {
configs[index] = {
...configs[index],
...updates,
updated_time: getCurrentTime(),
}
storage.set(STORAGE_KEYS.LLM_CONFIGS, configs)
return configs[index]
}
return null
},
// 删除配置
delete(id: string) {
const configs = this.getAll()
const filteredConfigs = configs.filter((config: any) => config.id !== id)
storage.set(STORAGE_KEYS.LLM_CONFIGS, filteredConfigs)
return true
},
// 搜索配置
search(params: { name?: string; provider?: string; is_enabled?: string }) {
let configs = this.getAll()
if (params.name) {
configs = configs.filter((config: any) => config.name.toLowerCase().includes(params.name!.toLowerCase()))
}
if (params.provider) {
configs = configs.filter((config: any) => config.provider === params.provider)
}
if (params.is_enabled) {
const enabled = params.is_enabled === 'true'
configs = configs.filter((config: any) => config.is_enabled === enabled)
}
return configs
},
}
// 敏感词相关操作
export const sensitiveWordStorage = {
// 获取所有敏感词
getAll() {
return storage.get(STORAGE_KEYS.SENSITIVE_WORDS, [] as any[])
},
// 根据ID获取敏感词
getById(id: string) {
const words = this.getAll()
return words.find((word: any) => word.id === id)
},
// 创建敏感词
create(word: any) {
const words = this.getAll()
const newWord = {
...word,
id: generateId(),
created_time: getCurrentTime(),
}
words.push(newWord)
storage.set(STORAGE_KEYS.SENSITIVE_WORDS, words)
return newWord
},
// 更新敏感词
update(id: string, updates: any) {
const words = this.getAll()
const index = words.findIndex((word: any) => word.id === id)
if (index !== -1) {
words[index] = { ...words[index], ...updates }
storage.set(STORAGE_KEYS.SENSITIVE_WORDS, words)
return words[index]
}
return null
},
// 删除敏感词
delete(id: string) {
const words = this.getAll()
const filteredWords = words.filter((word: any) => word.id !== id)
storage.set(STORAGE_KEYS.SENSITIVE_WORDS, filteredWords)
return true
},
// 搜索敏感词
search(params: { word?: string; level?: string }) {
let words = this.getAll()
if (params.word) {
words = words.filter((word: any) => word.word.toLowerCase().includes(params.word!.toLowerCase()))
}
if (params.level) {
words = words.filter((word: any) => word.level === params.level)
}
return words
},
// 批量导入
batchImport(words: string[], level: string) {
const existingWords = this.getAll()
const newWords = words.map((word) => ({
id: generateId(),
word: word.trim(),
level,
replacement: '***',
is_enabled: true,
created_time: getCurrentTime(),
}))
const allWords = [...existingWords, ...newWords]
storage.set(STORAGE_KEYS.SENSITIVE_WORDS, allWords)
return newWords
},
}
// 敏感人物相关操作
export const sensitivePersonStorage = {
// 获取所有敏感人物
getAll() {
return storage.get(STORAGE_KEYS.SENSITIVE_PERSONS, [] as any[])
},
// 根据ID获取敏感人物
getById(id: string) {
const persons = this.getAll()
return persons.find((person: any) => person.id === id)
},
// 创建敏感人物
create(person: any) {
const persons = this.getAll()
const newPerson = {
...person,
id: generateId(),
created_time: getCurrentTime(),
}
persons.push(newPerson)
storage.set(STORAGE_KEYS.SENSITIVE_PERSONS, persons)
return newPerson
},
// 更新敏感人物
update(id: string, updates: any) {
const persons = this.getAll()
const index = persons.findIndex((person: any) => person.id === id)
if (index !== -1) {
persons[index] = { ...persons[index], ...updates }
storage.set(STORAGE_KEYS.SENSITIVE_PERSONS, persons)
return persons[index]
}
return null
},
// 删除敏感人物
delete(id: string) {
const persons = this.getAll()
const filteredPersons = persons.filter((person: any) => person.id !== id)
storage.set(STORAGE_KEYS.SENSITIVE_PERSONS, filteredPersons)
return true
},
// 搜索敏感人物
search(params: { name?: string; level?: string }) {
let persons = this.getAll()
if (params.name) {
persons = persons.filter(
(person: any) =>
person.name.toLowerCase().includes(params.name!.toLowerCase()) ||
person.aliases.some((alias: string) => alias.toLowerCase().includes(params.name!.toLowerCase()))
)
}
if (params.level) {
persons = persons.filter((person: any) => person.level === params.level)
}
return persons
},
}
// 文本处理配置相关操作
export const textProcessStorage = {
// 获取配置
get() {
return storage.get(STORAGE_KEYS.TEXT_PROCESS_CONFIG, {
enable_ideology_check: true,
enable_sensitive_word_filter: true,
enable_sensitive_person_filter: true,
auto_replace_sensitive_words: true,
custom_replacement: '***',
})
},
// 更新配置
update(config: any) {
storage.set(STORAGE_KEYS.TEXT_PROCESS_CONFIG, config)
return config
},
}
// 大模型功能配置相关操作
export const llmFeatureStorage = {
// 获取所有功能
getAll() {
return storage.get(STORAGE_KEYS.LLM_FEATURES, [] as any[])
},
// 根据大模型ID获取功能
getByLLMId(llmId: string) {
const features = this.getAll()
return features.filter((feature: any) => feature.llm_id === llmId)
},
// 创建功能
create(feature: any) {
const features = this.getAll()
const newFeature = {
...feature,
id: generateId(),
created_time: getCurrentTime(),
updated_time: getCurrentTime(),
}
features.push(newFeature)
storage.set(STORAGE_KEYS.LLM_FEATURES, features)
return newFeature
},
// 更新功能
update(id: string, updates: any) {
const features = this.getAll()
const index = features.findIndex((feature: any) => feature.id === id)
if (index !== -1) {
features[index] = {
...features[index],
...updates,
updated_time: getCurrentTime(),
}
storage.set(STORAGE_KEYS.LLM_FEATURES, features)
return features[index]
}
return null
},
// 删除功能
delete(id: string) {
const features = this.getAll()
const filteredFeatures = features.filter((feature: any) => feature.id !== id)
storage.set(STORAGE_KEYS.LLM_FEATURES, filteredFeatures)
return true
},
// 获取可用功能模板
getAvailableFeatures() {
return [
{
code: 'text_generation',
name: '文本生成',
description: '基于输入生成文本内容',
},
{
code: 'text_summarization',
name: '文本摘要',
description: '生成文本的摘要内容',
},
{
code: 'text_translation',
name: '文本翻译',
description: '将文本翻译为其他语言',
},
{
code: 'text_analysis',
name: '文本分析',
description: '分析文本的情感、主题等',
},
{
code: 'question_answering',
name: '问答系统',
description: '回答用户提出的问题',
},
]
},
}
// 导出所有存储操作
export default {
llmConfigStorage,
sensitiveWordStorage,
sensitivePersonStorage,
textProcessStorage,
llmFeatureStorage,
}
import axios from 'axios' import axios from 'axios'
import md5 from 'blueimp-md5' import md5 from 'blueimp-md5'
import { getSignature, uploadFile } from '@/api/base' import { getSignature, uploadFile, getLocalFileChunk, uploadLocalFile } from '@/api/base'
export async function upload(blob: Blob | File) { // 生产环境线上部署
export async function ossUpload(blob: Blob | File) {
let fileType = 'png' let fileType = 'png'
if (blob instanceof File && blob.name) { if (blob instanceof File && blob.name) {
const matches = blob.name.match(/\.(\w+)$/) const matches = blob.name.match(/\.(\w+)$/)
...@@ -32,6 +33,25 @@ export async function upload(blob: Blob | File) { ...@@ -32,6 +33,25 @@ export async function upload(blob: Blob | File) {
return params.url return params.url
} }
// 本地环境上传
export async function localUpload(file: Blob | File) {
const fileName = file instanceof File && file.name ? file.name : `upload.${file.type.split('/').pop() || 'bin'}`
const response = await getLocalFileChunk({ file_name: fileName, file_size: file.size })
const params = {
file,
file_name: response.data.detail.file_name,
now_package_num: 1,
total_package_num: 1,
}
const res = await uploadLocalFile(params)
return res.data.detail.uri
}
export async function upload(blob: Blob | File) {
return import.meta.env.MODE === 'school' ? localUpload(blob) : ossUpload(blob)
}
export async function uploadFileByUrl(url: string) { export async function uploadFileByUrl(url: string) {
const res = await axios.get(url, { responseType: 'blob' }) const res = await axios.get(url, { responseType: 'blob' })
return upload(res.data) return upload(res.data)
......
/** import { Base64 } from 'js-base64'
* 文件下载
* @param {string} fileUrl 文件下载地址 export function getOnlinePreviewUrl(url: string) {
* @param {string} fileName 文件名 return import.meta.env.MODE === 'school'
* @returns {null} ? `${import.meta.env.VITE_FILE_PREVIEW_URL}/onlinePreview?url=${encodeURIComponent(Base64.encode(url))}`
*/ : `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(url)}`
export function funDownload(fileUrl: any, fileName: any) {
// console.log(fileUrl)
const elink = document.createElement('a') // 创建一个a标签
elink.download = fileName // 设置a标签的下载属性
elink.style.display = 'none' // 将a标签设置为隐藏
elink.href = fileUrl // 把之前处理好的地址赋给a标签的href
document.body.appendChild(elink) // 将a标签添加到body中
elink.click() // 执行a标签的点击方法
// URL.revokeObjectURL(elink.href) // 下载完成释放URL 对象
document.body.removeChild(elink) // 移除a标签
} }
/** /**
* 分割字符串,取得尾部 * 分割字符串,取得尾部
* @param {string} str 字符串 * @param {string} str 字符串
......
export function getVideoPlayUrl(playAuth?: { play_info_list?: any } | any) {
const playInfoList = playAuth?.play_info_list ?? playAuth
if (Array.isArray(playInfoList)) {
return playInfoList.find((item: any) => item.Definition === 'SD')?.PlayURL || playInfoList[0]?.PlayURL || ''
}
return typeof playInfoList === 'string' ? playInfoList : ''
}
import { getCreateAuth, getLocalFileChunk, updateAuth, uploadLocalFile } from '@/api/base'
export interface VideoUploadItem {
file: File
name: string
progress: number
url: string
videoId: string
state: 'pending' | 'uploading' | 'success' | 'failed'
abortController?: AbortController
uploader?: any
}
interface VideoUploadCallbacks {
onUploadStarted?: (file: VideoUploadItem) => void
onUploadSucceed?: (file: VideoUploadItem) => void
onUploadFailed?: (file: VideoUploadItem, error: unknown) => void
onUploadProgress?: (file: VideoUploadItem, progress: number) => void
}
interface VideoUploaderOptions extends VideoUploadCallbacks {
autoUpload?: boolean
multiple?: boolean
onUploadEnd?: () => void
}
const isSchool = import.meta.env.MODE === 'school'
function createVideoUploadItem(file: File): VideoUploadItem {
return {
file,
name: file.name,
progress: 0,
url: '',
videoId: '',
state: 'pending',
}
}
async function uploadLocalVideo(fileItem: VideoUploadItem, callbacks: VideoUploadCallbacks) {
fileItem.abortController = new AbortController()
const {
data: { detail },
} = await getLocalFileChunk({ file_name: fileItem.file.name, file_size: fileItem.file.size })
const fileName = detail.file_name
const chunkSize = detail.chunk_size || fileItem.file.size
const totalChunks = Math.max(1, Math.ceil(fileItem.file.size / chunkSize))
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const chunk = fileItem.file.slice(chunkIndex * chunkSize, (chunkIndex + 1) * chunkSize)
const {
data: { detail: uploadDetail },
} = await uploadLocalFile(
{
file_name: fileName,
file: chunk,
now_package_num: chunkIndex + 1,
total_package_num: totalChunks,
},
{
signal: fileItem.abortController.signal,
onUploadProgress(event: ProgressEvent) {
const totalSize = (event.total || chunk.size) * totalChunks
const loadedSize = event.loaded + chunkIndex * (event.total || chunk.size)
const progress = Math.min(100, parseFloat(((loadedSize / totalSize) * 100).toFixed(2)))
fileItem.progress = progress
callbacks.onUploadProgress?.(fileItem, progress)
},
},
)
fileItem.url = uploadDetail.uri
}
fileItem.progress = 100
}
function uploadAliyunVideo(fileItem: VideoUploadItem, callbacks: VideoUploadCallbacks) {
return new Promise<void>((resolve, reject) => {
const uploader = new (window as any).AliyunUpload.Vod({
userId: '1303984639806000',
region: 'cn-shanghai',
partSize: 4 * 1024 * 1024,
parallel: 3,
retryCount: 3,
retryDuration: 2,
onUploadstarted(uploadInfo: any) {
getCreateAuth({ title: uploadInfo.file.name, file_name: uploadInfo.file.name })
.then((res: any) => {
uploader.setUploadAuthAndAddress(
uploadInfo,
res.data.upload_auth,
res.data.upload_address,
res.data.source_id,
)
})
.catch(reject)
},
onUploadSucceed(uploadInfo: any) {
fileItem.videoId = uploadInfo.videoId
fileItem.progress = 100
resolve()
},
onUploadFailed(_uploadInfo: any, code: number, message: string) {
reject(new Error(message || `视频上传失败: ${code}`))
},
onUploadProgress(_uploadInfo: any, _totalSize: number, loadedPercent: number) {
const progress = Math.min(100, parseFloat((loadedPercent * 100).toFixed(2)))
fileItem.progress = progress
callbacks.onUploadProgress?.(fileItem, progress)
},
onUploadTokenExpired(uploadInfo: any) {
updateAuth({ source_id: uploadInfo.videoId })
.then((res: any) => {
uploader.resumeUploadWithAuth(res.data.UploadAuth || res.data.upload_auth || res.UploadAuth)
})
.catch(reject)
},
})
fileItem.uploader = uploader
uploader.addFile(fileItem.file, null, null, null, '{"Vod":{}}')
uploader.startUpload()
})
}
async function uploadVideoItem(fileItem: VideoUploadItem, callbacks: VideoUploadCallbacks) {
fileItem.state = 'uploading'
callbacks.onUploadStarted?.(fileItem)
try {
if (isSchool) {
await uploadLocalVideo(fileItem, callbacks)
} else {
await uploadAliyunVideo(fileItem, callbacks)
}
fileItem.state = 'success'
callbacks.onUploadSucceed?.(fileItem)
return fileItem
} catch (error) {
fileItem.state = 'failed'
callbacks.onUploadFailed?.(fileItem, error)
throw error
}
}
export function getUploadedVideoSourceId(fileItem: VideoUploadItem) {
return isSchool ? fileItem.url : fileItem.videoId
}
export async function uploadVideo(file: File, callbacks: VideoUploadCallbacks = {}) {
const fileItem = createVideoUploadItem(file)
await uploadVideoItem(fileItem, callbacks)
return fileItem
}
export function createVideoUploader(options: VideoUploaderOptions = {}) {
const settings = Object.assign({ autoUpload: true, multiple: false }, options)
const files = ref<VideoUploadItem[]>([])
const uploading = ref(false)
function addFile(file: File) {
const existingIndex = files.value.findIndex(
(item) =>
item.name === file.name &&
item.file.size === file.size &&
item.file.lastModified === file.lastModified &&
item.state !== 'failed',
)
if (existingIndex !== -1) return
if (!settings.multiple) {
stopUpload()
}
files.value.push(createVideoUploadItem(file))
if (settings.autoUpload && !uploading.value) {
void startUpload()
}
}
async function startUpload() {
if (uploading.value) return
uploading.value = true
try {
while (true) {
const fileItem = files.value.find((item) => item.state === 'pending')
if (!fileItem) break
try {
await uploadVideoItem(fileItem, settings)
} catch {
// Error is surfaced via callbacks and state updates.
}
}
} finally {
uploading.value = false
settings.onUploadEnd?.()
}
}
function cancelUpload(fileItem: VideoUploadItem) {
fileItem.abortController?.abort()
fileItem.uploader?.stopUpload?.()
files.value = files.value.filter((item) => item !== fileItem)
}
function stopUpload() {
const currentFiles = [...files.value]
files.value = []
currentFiles.forEach((fileItem) => {
fileItem.abortController?.abort()
fileItem.uploader?.stopUpload?.()
})
uploading.value = false
}
return {
files,
uploading,
addFile,
startUpload,
cancelUpload,
stopUpload,
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论