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

chore: update

上级 0841601b
......@@ -47,6 +47,7 @@
"unplugin-auto-import": "^0.11.2",
"vite": "^3.1.0",
"vite-plugin-checker": "^0.5.1",
"vite-plugin-mkcert": "^1.9.0",
"vue-tsc": "^0.40.5"
}
},
......@@ -310,6 +311,165 @@
"node": ">= 8"
}
},
"node_modules/@octokit/auth-token": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.1.tgz",
"integrity": "sha512-/USkK4cioY209wXRpund6HZzHo9GmjakpV9ycOkpMcMxMk7QVcVFVyCMtzvXYiHsB2crgDgrtNYSELYFBXhhaA==",
"dev": true,
"dependencies": {
"@octokit/types": "^7.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/core": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.0.5.tgz",
"integrity": "sha512-4R3HeHTYVHCfzSAi0C6pbGXV8UDI5Rk+k3G7kLVNckswN9mvpOzW9oENfjfH3nEmzg8y3AmKmzs8Sg6pLCeOCA==",
"dev": true,
"dependencies": {
"@octokit/auth-token": "^3.0.0",
"@octokit/graphql": "^5.0.0",
"@octokit/request": "^6.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^7.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/endpoint": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.2.tgz",
"integrity": "sha512-8/AUACfE9vpRpehE6ZLfEtzkibe5nfsSwFZVMsG8qabqRt1M81qZYUFRZa1B8w8lP6cdfDJfRq9HWS+MbmR7tw==",
"dev": true,
"dependencies": {
"@octokit/types": "^7.0.0",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/graphql": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.1.tgz",
"integrity": "sha512-sxmnewSwAixkP1TrLdE6yRG53eEhHhDTYUykUwdV9x8f91WcbhunIHk9x1PZLALdBZKRPUO2HRcm4kezZ79HoA==",
"dev": true,
"dependencies": {
"@octokit/request": "^6.0.0",
"@octokit/types": "^7.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/openapi-types": {
"version": "13.9.1",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.9.1.tgz",
"integrity": "sha512-98zOxAAR8MDHjXI2xGKgn/qkZLwfcNjHka0baniuEpN1fCv3kDJeh5qc0mBwim5y31eaPaYer9QikzwOkQq3wQ==",
"dev": true
},
"node_modules/@octokit/plugin-paginate-rest": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-4.2.3.tgz",
"integrity": "sha512-1RXJZ7hnxSANMtxKSVIEByjhYqqlu2GaKmLJJE/OVDya1aI++hdmXP4ORCUlsN2rt4hJzRYbWizBHlGYKz3dhQ==",
"dev": true,
"dependencies": {
"@octokit/types": "^7.3.1"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"@octokit/core": ">=4"
}
},
"node_modules/@octokit/plugin-request-log": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz",
"integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==",
"dev": true,
"peerDependencies": {
"@octokit/core": ">=3"
}
},
"node_modules/@octokit/plugin-rest-endpoint-methods": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-6.5.2.tgz",
"integrity": "sha512-zUscUePMC3KEKyTAfuG/dA6hw4Yn7CncVJs2kM9xc4931Iqk3ZiwHfVwTUnxkqQJIVgeBRYUk3rM4hMfgASUxg==",
"dev": true,
"dependencies": {
"@octokit/types": "^7.3.1",
"deprecation": "^2.3.1"
},
"engines": {
"node": ">= 14"
},
"peerDependencies": {
"@octokit/core": ">=3"
}
},
"node_modules/@octokit/request": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.1.tgz",
"integrity": "sha512-gYKRCia3cpajRzDSU+3pt1q2OcuC6PK8PmFIyxZDWCzRXRSIBH8jXjFJ8ZceoygBIm0KsEUg4x1+XcYBz7dHPQ==",
"dev": true,
"dependencies": {
"@octokit/endpoint": "^7.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^7.0.0",
"is-plain-object": "^5.0.0",
"node-fetch": "^2.6.7",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/request-error": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.1.tgz",
"integrity": "sha512-ym4Bp0HTP7F3VFssV88WD1ZyCIRoE8H35pXSKwLeMizcdZAYc/t6N9X9Yr9n6t3aG9IH75XDnZ6UeZph0vHMWQ==",
"dev": true,
"dependencies": {
"@octokit/types": "^7.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/rest": {
"version": "19.0.4",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.4.tgz",
"integrity": "sha512-LwG668+6lE8zlSYOfwPj4FxWdv/qFXYBpv79TWIQEpBLKA9D/IMcWsF/U9RGpA3YqMVDiTxpgVpEW3zTFfPFTA==",
"dev": true,
"dependencies": {
"@octokit/core": "^4.0.0",
"@octokit/plugin-paginate-rest": "^4.0.0",
"@octokit/plugin-request-log": "^1.0.4",
"@octokit/plugin-rest-endpoint-methods": "^6.0.0"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/@octokit/types": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.3.1.tgz",
"integrity": "sha512-Vefohn8pHGFYWbSc6du0wXMK/Pmy6h0H4lttBw5WqquEuxjdXwyYX07CeZpJDkzSzpdKxBoWRNuDJGTE+FvtqA==",
"dev": true,
"dependencies": {
"@octokit/openapi-types": "^13.9.1"
}
},
"node_modules/@popperjs/core": {
"name": "@sxzz/popperjs-es",
"version": "2.11.7",
......@@ -1180,6 +1340,12 @@
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/before-after-hook": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz",
"integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==",
"dev": true
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz",
......@@ -1479,6 +1645,12 @@
"node": ">= 0.8"
}
},
"node_modules/deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
"dev": true
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
......@@ -2846,6 +3018,15 @@
"node": ">=0.12.0"
}
},
"node_modules/is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-type-of": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.2.1.tgz",
......@@ -3246,6 +3427,26 @@
"node": ">= 0.4.0"
}
},
"node_modules/node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
......@@ -4134,6 +4335,12 @@
"node": ">=0.6"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
},
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz",
......@@ -4263,6 +4470,12 @@
"node": ">=12"
}
},
"node_modules/universal-user-agent": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==",
"dev": true
},
"node_modules/universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz",
......@@ -4560,6 +4773,31 @@
"node": ">=10"
}
},
"node_modules/vite-plugin-mkcert": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.9.0.tgz",
"integrity": "sha512-lKIOVJvbt1ijAMeF31TYGwEzH8iqK+Q8BiInpQRcAW543y8HYttZTRVGDAFqVaej6iE5o7XAisZ1aCg+q5tOfA==",
"dev": true,
"dependencies": {
"@octokit/rest": "^19.0.3",
"axios": "^0.21.4",
"debug": "^4.3.4",
"picocolors": "^1.0.0",
"vite": "^3.0.0"
},
"engines": {
"node": ">=v16.0.0"
}
},
"node_modules/vite-plugin-mkcert/node_modules/axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dev": true,
"dependencies": {
"follow-redirects": "^1.14.0"
}
},
"node_modules/vm2": {
"version": "3.9.10",
"resolved": "https://registry.npmmirror.com/vm2/-/vm2-3.9.10.tgz",
......@@ -4720,6 +4958,12 @@
"typescript": "*"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true
},
"node_modules/webpack-sources": {
"version": "3.2.3",
"resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz",
......@@ -4735,6 +4979,16 @@
"integrity": "sha512-h9atBP/bsZohWpHnr+2sic8Iecb60GxftXsWNLLLSqewgIsGzByd2gcIID4nXcG+3tNe4GQG3dLcff3kXupdRA==",
"dev": true
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
......@@ -5044,6 +5298,130 @@
"fastq": "^1.6.0"
}
},
"@octokit/auth-token": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.1.tgz",
"integrity": "sha512-/USkK4cioY209wXRpund6HZzHo9GmjakpV9ycOkpMcMxMk7QVcVFVyCMtzvXYiHsB2crgDgrtNYSELYFBXhhaA==",
"dev": true,
"requires": {
"@octokit/types": "^7.0.0"
}
},
"@octokit/core": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.0.5.tgz",
"integrity": "sha512-4R3HeHTYVHCfzSAi0C6pbGXV8UDI5Rk+k3G7kLVNckswN9mvpOzW9oENfjfH3nEmzg8y3AmKmzs8Sg6pLCeOCA==",
"dev": true,
"requires": {
"@octokit/auth-token": "^3.0.0",
"@octokit/graphql": "^5.0.0",
"@octokit/request": "^6.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^7.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/endpoint": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.2.tgz",
"integrity": "sha512-8/AUACfE9vpRpehE6ZLfEtzkibe5nfsSwFZVMsG8qabqRt1M81qZYUFRZa1B8w8lP6cdfDJfRq9HWS+MbmR7tw==",
"dev": true,
"requires": {
"@octokit/types": "^7.0.0",
"is-plain-object": "^5.0.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/graphql": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.1.tgz",
"integrity": "sha512-sxmnewSwAixkP1TrLdE6yRG53eEhHhDTYUykUwdV9x8f91WcbhunIHk9x1PZLALdBZKRPUO2HRcm4kezZ79HoA==",
"dev": true,
"requires": {
"@octokit/request": "^6.0.0",
"@octokit/types": "^7.0.0",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/openapi-types": {
"version": "13.9.1",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-13.9.1.tgz",
"integrity": "sha512-98zOxAAR8MDHjXI2xGKgn/qkZLwfcNjHka0baniuEpN1fCv3kDJeh5qc0mBwim5y31eaPaYer9QikzwOkQq3wQ==",
"dev": true
},
"@octokit/plugin-paginate-rest": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-4.2.3.tgz",
"integrity": "sha512-1RXJZ7hnxSANMtxKSVIEByjhYqqlu2GaKmLJJE/OVDya1aI++hdmXP4ORCUlsN2rt4hJzRYbWizBHlGYKz3dhQ==",
"dev": true,
"requires": {
"@octokit/types": "^7.3.1"
}
},
"@octokit/plugin-request-log": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz",
"integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==",
"dev": true,
"requires": {}
},
"@octokit/plugin-rest-endpoint-methods": {
"version": "6.5.2",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-6.5.2.tgz",
"integrity": "sha512-zUscUePMC3KEKyTAfuG/dA6hw4Yn7CncVJs2kM9xc4931Iqk3ZiwHfVwTUnxkqQJIVgeBRYUk3rM4hMfgASUxg==",
"dev": true,
"requires": {
"@octokit/types": "^7.3.1",
"deprecation": "^2.3.1"
}
},
"@octokit/request": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.1.tgz",
"integrity": "sha512-gYKRCia3cpajRzDSU+3pt1q2OcuC6PK8PmFIyxZDWCzRXRSIBH8jXjFJ8ZceoygBIm0KsEUg4x1+XcYBz7dHPQ==",
"dev": true,
"requires": {
"@octokit/endpoint": "^7.0.0",
"@octokit/request-error": "^3.0.0",
"@octokit/types": "^7.0.0",
"is-plain-object": "^5.0.0",
"node-fetch": "^2.6.7",
"universal-user-agent": "^6.0.0"
}
},
"@octokit/request-error": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.1.tgz",
"integrity": "sha512-ym4Bp0HTP7F3VFssV88WD1ZyCIRoE8H35pXSKwLeMizcdZAYc/t6N9X9Yr9n6t3aG9IH75XDnZ6UeZph0vHMWQ==",
"dev": true,
"requires": {
"@octokit/types": "^7.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
}
},
"@octokit/rest": {
"version": "19.0.4",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.4.tgz",
"integrity": "sha512-LwG668+6lE8zlSYOfwPj4FxWdv/qFXYBpv79TWIQEpBLKA9D/IMcWsF/U9RGpA3YqMVDiTxpgVpEW3zTFfPFTA==",
"dev": true,
"requires": {
"@octokit/core": "^4.0.0",
"@octokit/plugin-paginate-rest": "^4.0.0",
"@octokit/plugin-request-log": "^1.0.4",
"@octokit/plugin-rest-endpoint-methods": "^6.0.0"
}
},
"@octokit/types": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-7.3.1.tgz",
"integrity": "sha512-Vefohn8pHGFYWbSc6du0wXMK/Pmy6h0H4lttBw5WqquEuxjdXwyYX07CeZpJDkzSzpdKxBoWRNuDJGTE+FvtqA==",
"dev": true,
"requires": {
"@octokit/openapi-types": "^13.9.1"
}
},
"@popperjs/core": {
"version": "npm:@sxzz/popperjs-es@2.11.7",
"resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
......@@ -5728,6 +6106,12 @@
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"before-after-hook": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz",
"integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==",
"dev": true
},
"binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.2.0.tgz",
......@@ -5959,6 +6343,12 @@
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"deprecation": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
"dev": true
},
"destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
......@@ -6936,6 +7326,12 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true
},
"is-plain-object": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"dev": true
},
"is-type-of": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/is-type-of/-/is-type-of-1.2.1.tgz",
......@@ -7270,6 +7666,15 @@
"resolved": "https://registry.npmmirror.com/netmask/-/netmask-2.0.2.tgz",
"integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="
},
"node-fetch": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
"dev": true,
"requires": {
"whatwg-url": "^5.0.0"
}
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
......@@ -7964,6 +8369,12 @@
"resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-1.14.1.tgz",
......@@ -8054,6 +8465,12 @@
}
}
},
"universal-user-agent": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==",
"dev": true
},
"universalify": {
"version": "0.1.2",
"resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.1.2.tgz",
......@@ -8256,6 +8673,30 @@
}
}
},
"vite-plugin-mkcert": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.9.0.tgz",
"integrity": "sha512-lKIOVJvbt1ijAMeF31TYGwEzH8iqK+Q8BiInpQRcAW543y8HYttZTRVGDAFqVaej6iE5o7XAisZ1aCg+q5tOfA==",
"dev": true,
"requires": {
"@octokit/rest": "^19.0.3",
"axios": "^0.21.4",
"debug": "^4.3.4",
"picocolors": "^1.0.0",
"vite": "^3.0.0"
},
"dependencies": {
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dev": true,
"requires": {
"follow-redirects": "^1.14.0"
}
}
}
},
"vm2": {
"version": "3.9.10",
"resolved": "https://registry.npmmirror.com/vm2/-/vm2-3.9.10.tgz",
......@@ -8382,6 +8823,12 @@
"@volar/vue-typescript": "0.40.5"
}
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true
},
"webpack-sources": {
"version": "3.2.3",
"resolved": "https://registry.npmmirror.com/webpack-sources/-/webpack-sources-3.2.3.tgz",
......@@ -8394,6 +8841,16 @@
"integrity": "sha512-h9atBP/bsZohWpHnr+2sic8Iecb60GxftXsWNLLLSqewgIsGzByd2gcIID4nXcG+3tNe4GQG3dLcff3kXupdRA==",
"dev": true
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
......
......@@ -52,6 +52,7 @@
"unplugin-auto-import": "^0.11.2",
"vite": "^3.1.0",
"vite-plugin-checker": "^0.5.1",
"vite-plugin-mkcert": "^1.9.0",
"vue-tsc": "^0.40.5"
}
}
<script></script>
<template>
<el-button></el-button>
</template>
......@@ -71,7 +71,7 @@ const fetchList = (isReset = false) => {
httpRequest(requestParams)
.then((res: any) => {
const { list = [], total = 0 } = callback ? callback(res.data, requestParams) : res.data || {}
page.total = total
page.total = parseInt(total)
dataList.value = list
})
// .catch(() => {
......
export function useCountdown(optionsSecond = 90) {
const second = ref(optionsSecond)
const disabled = ref(false)
let timer: number | null = null
function start() {
disabled.value = true
timer = window.setInterval(() => {
second.value--
if (second.value <= 0) {
stop()
}
}, 1000)
}
function stop() {
timer && clearInterval(timer)
disabled.value = false
}
onUnmounted(() => {
timer && clearInterval(timer)
timer = null
})
return { second, disabled, start, stop }
}
......@@ -16,8 +16,6 @@ import modules from './modules'
import { permissionDirective } from '@/utils/permission'
import { useMapStore } from '@/stores/map'
const app = createApp(App)
// 注册公共组件
app.component('AppCard', AppCard).component('AppList', AppList).component('AppUpload', AppUpload)
......@@ -30,5 +28,3 @@ app.use(router)
app.use(ElementPlus, { locale: zhCn })
app.mount('#app')
useMapStore().getMapList()
import httpRequest from '@/utils/axios'
import type { ExperimentCreateItem } from './types'
import type { ContestCreateParams, ContestUpdateParams } from './types'
// 获取赛项列表
export function getContestItemList(params?: { name?: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/experiment/list', { params })
export function getContestItemList(params?: { page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/competition/list', { params })
}
// 获取实验详情
export function getContestItem(params: { experiment_id: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/experiment/view', { params })
// 获取赛项详情
export function getContestItem(params: { id: string }) {
return httpRequest.get('/api/resource/v1/backend/competition/detail', { params })
}
// 创建实验
export function createExperiment(data: ExperimentCreateItem) {
return httpRequest.post('/api/resource/v1/backend/experiment/create', data)
// 创建赛项
export function createContest(data: ContestCreateParams) {
return httpRequest.post('/api/resource/v1/backend/competition/create', data)
}
// 更新实验
export function updateExperiment(data: ExperimentCreateItem) {
return httpRequest.post('/api/resource/v1/backend/experiment/update', data)
// 更新赛项
export function updateContest(data: ContestUpdateParams) {
return httpRequest.post('/api/resource/v1/backend/competition/update', data)
}
// 获取实验课程列表
export function getExperimentCourseList(params: { organ_id: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment/courses', { params })
// 更新评分规则
export function updateContestRules(data: ContestUpdateParams) {
return httpRequest.post('/api/resource/v1/backend/competition-rule/save', data)
}
// 获取实验指导老师列表
export function getExperimentTeacherList(params: { organ_id: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment/teachers', { params })
// 获取赛项课程列表
export function getContestCourseList(params: { organ_id: string }) {
return httpRequest.get('/api/resource/v1/backend/competition/courses', { params })
}
// 获取实验关联班级列表
export function getExperimentClassList(params: { experiment_id: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/experiment/class-add', { params })
// 获取赛项指导老师列表
export function getContestTeacherList(params: { organ_id: string }) {
return httpRequest.get('/api/resource/v1/backend/competition/teachers', { params })
}
// 实验关联班级
// 获取赛项关联班级列表
export function getContestClassList(params: { experiment_id: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/competition/class-add', { params })
}
// 赛项关联班级
export function experimentAddClass(data: { experiment_id: string; classes_id: string; type: 'add' | 'delete' }) {
return httpRequest.post('/api/resource/v1/backend/experiment/class-add', data)
return httpRequest.post('/api/resource/v1/backend/competition/class-add', data)
}
// 获取班级学生列表
export function getClassStudentList(params: { class_id: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/experiment/class-students', { params })
return httpRequest.get('/api/resource/v1/backend/competition/class-students', { params })
}
// 获取班级小组列表
export function getExperimentClassGroupsList(params: { experiment_id: string; class_id: string }) {
return httpRequest.get('/api/resource/v1/backend/experiment/class-teams', { params })
export function getContestClassGroupsList(params: { experiment_id: string; class_id: string }) {
return httpRequest.get('/api/resource/v1/backend/competition/class-teams', { params })
}
// 获取实验小组
export function getExperimentGroup(params: { team_id: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/experiment/team-view', { params })
// 获取赛项小组
export function getContestGroup(params: { team_id: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/competition/team-view', { params })
}
// 新增实验小组
// 新增赛项小组
export function experimentAddClassGroup(data: { experiment_id: string; class_id: string; name: string }) {
return httpRequest.post('/api/resource/v1/backend/experiment/team-add', data)
return httpRequest.post('/api/resource/v1/backend/competition/team-add', data)
}
// 实验小组添加学生
// 赛项小组添加学生
export function experimentGroupAddStudent(data: { team_id: string; students_id: string; type: 'add' | 'delete' }) {
return httpRequest.post('/api/resource/v1/backend/experiment/team-add-student', data)
return httpRequest.post('/api/resource/v1/backend/competition/team-add-student', data)
}
// 获取赛项关联班级列表
export function getContestGroupStudentList(params: { team_id: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/competition/team-add-student', { params })
}
// 获取实验关联班级列表
export function getExperimentGroupStudentList(params: { team_id: string; page?: number; 'per-page'?: number }) {
return httpRequest.get('/api/resource/v1/backend/experiment/team-add-student', { params })
// 获取指导老师列表
export function getTeacherList(params?: { name?: string }) {
return httpRequest.get('/api/resource/v1/backend/teacher/faculty-advisers', { params })
}
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import type { ContestItem, ExperimentCreateItem } from '../types'
import type { ContestItem, ContestCreateParams, ContestUpdateParams } from '../types'
import { ElMessage } from 'element-plus'
import { createExperiment, updateExperiment } from '../api'
import { pick } from 'lodash-es'
import { createContest, updateContest } from '../api'
import { useMapStore } from '@/stores/map'
import { useGetTeacherList } from '../composables/useGetTeacherList'
interface Props {
data?: ContestItem | null
......@@ -18,38 +20,71 @@ const emit = defineEmits<{
}>()
// 赛项类型
const types = useMapStore().getMapValuesByKey('experiment_type')
const types = useMapStore().getMapValuesByKey('competition_type')
// 主办单位
const hostUnitList = useMapStore().getMapValuesByKey('host_unit')
// 承办单位
const organizerList = useMapStore().getMapValuesByKey('organizer')
// 技术支持单位
const technicalSupportUnitList = useMapStore().getMapValuesByKey('technical_support_unit')
// 数据状态
const status = useMapStore().getMapValuesByKey('system_status')
// 指导老师
const { teachers } = useGetTeacherList()
const formRef = $ref<FormInstance>()
const form = reactive<ExperimentCreateItem>({
organ_id: '',
status: '1',
course_id: '',
const form = reactive({
id: '',
name: '',
length: 10,
type: '',
score: 100,
teachers_id: '',
teachers_ids: []
host_unit_id: '',
organizer_ids: [],
technical_support_unit_id: '',
type: '1',
teacher_ids: [],
apply_expiration_date: '',
status: '1',
logo: '',
cover: '',
train_platform_uri: '',
competition_uri: '',
dateRange: undefined,
datetimeRange: undefined
})
watchEffect(() => {
if (!props.data) return
const score = parseFloat(props.data.score)
const length = parseFloat(props.data.length)
const teachers_ids = props.data.teacher.map(item => item.id)
Object.assign(form, props.data, { score, length, teachers_ids })
const host_unit_id = props.data.host_unit.id
const organizer_ids = props.data.organizers.map(item => item.id)
const technical_support_unit_id = props.data.technical_support_unit.id
const teacher_ids = props.data.teachers.map(item => item.id)
const dateRange = [new Date(parseInt(props.data.start_range) * 1000), new Date(parseInt(props.data.end_range) * 1000)]
const datetimeRange = [new Date(parseInt(props.data.start_at) * 1000), new Date(parseInt(props.data.end_at) * 1000)]
const apply_expiration_date = parseInt(props.data.apply_expiration_date) * 1000
Object.assign(form, props.data, {
host_unit_id,
organizer_ids,
technical_support_unit_id,
teacher_ids,
dateRange,
datetimeRange,
apply_expiration_date
})
})
const rules = ref<FormRules>({
organ_id: [{ required: true, message: '请选择赛项所属部门/学校' }],
course_id: [{ required: true, message: '请选择赛项课程' }],
name: [{ required: true, message: '请输入赛项名称' }],
length: [{ required: true, message: '请输入赛项学时' }],
host_unit_id: [{ required: true, message: '请选择主办单位' }],
organizer_ids: [{ type: 'array', required: true, message: '请选择承办单位' }],
technical_support_unit_id: [{ required: true, message: '请选择技术支持单位' }],
type: [{ required: true, message: '请选择赛项类型' }],
teachers_ids: [{ type: 'array', required: true, message: '请选择指导教师', trigger: 'change' }],
score: [{ required: true, message: '请输入赛项总成绩' }]
teacher_ids: [{ type: 'array', required: true, message: '请选择指导教师', trigger: 'change' }],
dateRange: [{ type: 'array', required: true, message: '请选择赛项周期', trigger: 'change' }],
datetimeRange: [{ type: 'array', required: true, message: '请选择正式比赛时间', trigger: 'change' }],
apply_expiration_date: [{ required: true, message: '请选择报名截止日期' }],
train_platform_uri: [{ required: true, message: '请输入训练平台地址' }],
competition_uri: [{ required: true, message: '请输入正式比赛地址' }],
status: [{ required: true, message: '请选择有效状态' }],
logo: [{ required: true, message: '请上传赛项LOGO' }],
cover: [{ required: true, message: '请上传赛项封面' }]
})
const isUpdate = $computed(() => {
return !!form.id
......@@ -61,24 +96,51 @@ const title = $computed(() => {
// 提交
function handleSubmit() {
formRef?.validate().then(() => {
const params = {
const [firstDate, secondDate] = form.dateRange || []
const [firstDatetime, secondDatetime] = form.datetimeRange || []
const mergedForm = {
...form,
teachers_id: form.teachers_ids?.join(',') || ''
organizer_ids: JSON.stringify(form.organizer_ids),
// teacher_ids: form.teacher_ids.join(','),
start_range: new Date(firstDate).getTime() / 1000,
end_range: new Date(secondDate).getTime() / 1000,
start_at: new Date(firstDatetime).getTime() / 1000,
end_at: new Date(secondDatetime).getTime() / 1000,
apply_expiration_date: new Date(form.apply_expiration_date).getTime() / 1000
}
const params: ContestUpdateParams = pick(mergedForm, [
'id',
'name',
'host_unit_id',
'organizer_ids',
'technical_support_unit_id',
'type',
'start_range',
'end_range',
'start_at',
'end_at',
'apply_expiration_date',
'status',
'logo',
'cover',
'train_platform_uri',
'competition_uri',
'teacher_ids'
])
isUpdate ? handleUpdate(params) : handleCreate(params)
})
}
// 新增
function handleCreate(params: ExperimentCreateItem) {
createExperiment(params).then(() => {
function handleCreate(params: ContestCreateParams) {
createContest(params).then(() => {
ElMessage({ message: '创建成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
// 修改
function handleUpdate(params: ExperimentCreateItem) {
updateExperiment(params).then(() => {
function handleUpdate(params: ContestUpdateParams) {
updateContest(params).then(() => {
ElMessage({ message: '修改成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
......@@ -87,47 +149,61 @@ function handleUpdate(params: ExperimentCreateItem) {
</script>
<template>
<el-dialog :title="title" :close-on-click-modal="false" width="600px" @update:modelValue="$emit('update:modelValue')">
<el-dialog
:title="title"
:close-on-click-modal="false"
align-center
width="600px"
@update:modelValue="$emit('update:modelValue')"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
<el-form-item label="赛项名称" prop="name">
<el-input v-model="form.name" :disabled="isUpdate" />
</el-form-item>
<el-form-item label="主办单位" prop="course_id">
<el-select v-model="form.course_id" style="width: 100%" :disabled="isUpdate">
<el-option v-for="item in types" :key="item.id" :label="item.label" :value="item.id"></el-option>
<el-form-item label="主办单位" prop="host_unit_id">
<el-select v-model="form.host_unit_id" style="width: 100%">
<el-option v-for="item in hostUnitList" :key="item.id" :label="item.label" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="承办单位" prop="course_id">
<el-select v-model="form.course_id" style="width: 100%" :disabled="isUpdate">
<el-option v-for="item in types" :key="item.id" :label="item.label" :value="item.id"></el-option>
<el-form-item label="承办单位" prop="organizer_ids">
<el-select v-model="form.organizer_ids" multiple style="width: 100%">
<el-option v-for="item in organizerList" :key="item.id" :label="item.label" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="技术支持单位" prop="course_id">
<el-select v-model="form.course_id" style="width: 100%" :disabled="isUpdate">
<el-option v-for="item in types" :key="item.id" :label="item.label" :value="item.id"></el-option>
<el-form-item label="技术支持单位" prop="technical_support_unit_id">
<el-select v-model="form.technical_support_unit_id" style="width: 100%">
<el-option
v-for="item in technicalSupportUnitList"
:key="item.id"
:label="item.label"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="赛项类型" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="item in status" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
<el-form-item label="赛项类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio v-for="item in types" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="指导教师" prop="teachers_ids">
<el-select v-model="form.teachers_ids" multiple style="width: 100%">
<el-option v-for="item in types" :key="item.id" :label="item.label" :value="item.id"></el-option>
<el-form-item label="指导教师" prop="teacher_ids">
<el-select v-model="form.teacher_ids" multiple style="width: 100%">
<el-option v-for="item in teachers" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="赛项周期" prop="type">
<el-date-picker type="daterange" v-model="form.type" />
<el-form-item label="赛项周期" prop="dateRange">
<el-date-picker type="daterange" v-model="form.dateRange" style="width: 100%" />
</el-form-item>
<el-form-item label="正式比赛时间" prop="datetimeRange">
<el-date-picker type="datetimerange" v-model="form.datetimeRange" style="width: 100%" />
</el-form-item>
<el-form-item label="正式比赛日期" prop="type">
<el-date-picker v-model="form.type" />
<el-form-item label="报名截止日期" prop="apply_expiration_date">
<el-date-picker v-model="form.apply_expiration_date" style="width: 100%" />
</el-form-item>
<el-form-item label="正式比赛时间" prop="type">
<el-date-picker type="daterange" v-model="form.type" />
<el-form-item label="训练平台地址" prop="train_platform_uri">
<el-input v-model="form.train_platform_uri" />
</el-form-item>
<el-form-item label="报名截止日期" prop="type">
<el-date-picker v-model="form.type" />
<el-form-item label="正式比赛地址" prop="competition_uri">
<el-input v-model="form.competition_uri" />
</el-form-item>
<el-form-item label="有效状态" prop="status">
<el-radio-group v-model="form.status">
......@@ -135,10 +211,10 @@ function handleUpdate(params: ExperimentCreateItem) {
</el-radio-group>
</el-form-item>
<el-form-item label="赛项LOGO" prop="logo">
<AppUpload v-model="form.type"></AppUpload>
<AppUpload v-model="form.logo" accept="image/*"></AppUpload>
</el-form-item>
<el-form-item label="赛项封面" prop="logo">
<AppUpload v-model="form.type"></AppUpload>
<el-form-item label="赛项封面" prop="cover">
<AppUpload v-model="form.cover" accept="image/*"></AppUpload>
</el-form-item>
<el-row justify="center">
<el-button type="primary" round auto-insert-space @click="handleSubmit">保存</el-button>
......
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import type { ContestItem, ExperimentCreateItem } from '../types'
import type { ContestItem } from '../types'
import { ElMessage } from 'element-plus'
import { createExperiment, updateExperiment } from '../api'
import { updateContestRules } from '../api'
import { useMapStore } from '@/stores/map'
interface Props {
data?: ContestItem | null
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
// 数据状态
const status = useMapStore().getMapValuesByKey('system_status')
const detail = $ref<ContestItem>(inject('detail'))
const formRef = $ref<FormInstance>()
const form = reactive<ExperimentCreateItem>({
organ_id: '',
status: '1',
course_id: '',
const form = reactive({
name: '',
length: 10,
type: '',
score: 100,
teachers_id: '',
teachers_ids: []
competition_id: '',
is_more_status: 1,
rule_type: 1,
lowest_number: undefined,
detail_list: []
})
watchEffect(() => {
if (!props.data) return
const score = parseFloat(props.data.score)
const length = parseFloat(props.data.length)
const teachers_ids = props.data.teacher.map(item => item.id)
Object.assign(form, props.data, { score, length, teachers_ids })
})
const rules = ref<FormRules>({
name: [{ required: true, message: '请输入赛项名称' }]
})
const isUpdate = $computed(() => {
return !!form.id
watchEffect(() => {
form.name = detail.name
form.competition_id = detail.id
})
// 提交
function handleSubmit() {
formRef?.validate().then(() => {
const params = {
...form,
teachers_id: form.teachers_ids?.join(',') || ''
...form
}
isUpdate ? handleUpdate(params) : handleCreate(params)
})
}
// 新增
function handleCreate(params: ExperimentCreateItem) {
createExperiment(params).then(() => {
ElMessage({ message: '创建成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
// 修改
function handleUpdate(params: ExperimentCreateItem) {
updateExperiment(params).then(() => {
ElMessage({ message: '修改成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
updateContestRules(params).then(() => {
ElMessage({ message: '保存成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
})
}
</script>
......@@ -83,12 +52,13 @@ function handleUpdate(params: ExperimentCreateItem) {
@update:modelValue="$emit('update:modelValue')"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="160px">
<el-form-item label="赛项名称" prop="name">
<el-input v-model="form.name" :disabled="isUpdate" />
<el-form-item label="赛项名称">
<el-input v-model="form.name" disabled />
</el-form-item>
<el-form-item label="多人评分" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="item in status" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
<el-form-item label="多人评分" prop="is_more_status">
<el-radio-group v-model="form.is_more_status">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="多人评分成绩计算规则" prop="status">
......@@ -96,8 +66,8 @@ function handleUpdate(params: ExperimentCreateItem) {
<el-radio v-for="item in status" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="最低评分人数" prop="status">
<el-input v-model="form.name" :disabled="isUpdate" />
<el-form-item label="最低评分人数" prop="lowest_number">
<el-input-number v-model="form.lowest_number" step-strictly :controls="false" />
</el-form-item>
<el-form-item>
<el-table>
......
import { getTeacherList } from '../api'
interface TeacherType {
id: string
name: string
}
const teachers = ref<TeacherType[]>([])
export function useGetTeacherList() {
!teachers.value.length &&
getTeacherList().then((res: any) => {
teachers.value = res.data.list
})
return { teachers }
}
export interface ExperimentItemTeacher {
import type { SystemDictionary } from '@/types'
export interface ContestItemTeacher {
id: string
name: string
}
export interface ContestItem {
course_id: string
course_name: string
created_operator: string
created_operator_name: string
created_time: string
delete_time: string
id: string
length: string
name: string
organ_id: string
organ_id_name: string
project_id: string
project_id_name: string
score: string
status: string
status_name: string
teacher: ExperimentItemTeacher[]
type: string
type_name: string
updated_operator: string
updated_operator_name: string
updated_time: string
}
export interface ExperimentCreateItem {
id?: string
organ_id: string
start_range: string
end_range: string
start_at: string
end_at: string
apply_expiration_date: string
status: string
course_id: string
name: string
length: number
type: string
score: number
teachers_id: string
teachers_ids?: string[]
}
export interface ClassItem {
code: string
logo: string
cover: string
train_platform_uri: string
competition_uri: string
publish_status: string
created_operator: string
created_operator_name: string
created_time: string
delete_time: string
id: string
name: string
organ_id: string
organ_id_name: string
project_id: string
project_id_name: string
specialty_id: string
specialty_id_name: string
start_year: string
start_year_name: string
status: string
status_name: string
student_nums: number
teacher_id: string
teacher_id_name: string
updated_operator: string
updated_operator_name: string
updated_time: string
}
export interface StudentItem {
birthday: string
city: string
city_name: string
class_id: string
class_id_name: string
county: string
county_name: string
created_operator: string
created_operator_name: string
created_time: string
delete_time: string
gender: string
gender_name: string
id: string
id_number: string
id_type: string
id_type_name: string
mobile: string
name: string
organ_id: string
organ_id_name: string
project_id: string
project_id_name: string
province: string
province_name: string
sno_number: string
specialty_id: string
specialty_id_name: string
sso_id: string
status: string
status_name: string
updated_operator: string
updated_operator_name: string
updated_time: string
teachers: ContestItemTeacher[]
host_unit: SystemDictionary
technical_support_unit: SystemDictionary
organizers: SystemDictionary[]
apply_count: number
expert_count: number
}
export interface GroupItem {
class_id: string
created_operator: string
created_time: string
experiment_id: string
id: string
export interface ContestCreateParams {
name: string
host_unit_id: string
organizer_ids: string
technical_support_unit_id: string
type: string
start_range: number
end_range: number
start_at: number
end_at: number
apply_expiration_date: number
status: string
student_num: number
updated_operator: string
updated_time: string
experiment_name?: string
course_name?: string
class_name?: string
logo: string
cover: string
train_platform_uri: string
competition_uri: string
teacher_ids: string[]
}
export type ContestUpdateParams = ContestCreateParams & { id: string }
......@@ -16,13 +16,25 @@ const listOptions = {
columns: [
{ label: '序号', type: 'index', width: 60 },
{ label: '赛项名称', prop: 'name' },
{ label: '主办单位', prop: 'length' },
{ label: '承办单位', prop: 'teacher_names' },
{ label: '技术支持单位', prop: 'type_name' },
{ label: '赛项类型', prop: 'score' },
{ label: '指导老师', prop: 'score' },
{ label: '报名人数', prop: 'score' },
{ label: '专家人数', prop: 'score' },
{ label: '主办单位', prop: 'host_unit.label' },
{
label: '承办单位',
prop: 'organizers',
computed({ row }: { row: ContestItem }) {
return row.organizers.map(item => item.label).join(',')
}
},
{ label: '技术支持单位', prop: 'technical_support_unit.label' },
{ label: '赛项类型', prop: 'type' },
{
label: '指导老师',
prop: 'teachers',
computed({ row }: { row: ContestItem }) {
return row.teachers.map(item => item.name).join(',')
}
},
{ label: '报名人数', prop: 'apply_count' },
{ label: '专家人数', prop: 'expert_count' },
{ label: '更新时间', prop: 'updated_time' },
{ label: '操作', slots: 'table-x', width: 200 }
]
......
<script setup lang="ts">
import type { ContestItem } from '../types'
import { getContestItem } from '../api'
import { useMapStore } from '@/stores/map'
const ViewBook = defineAsyncComponent(() => import('../components/ViewBook.vue'))
const ViewVideo = defineAsyncComponent(() => import('../components/ViewVideo.vue'))
const JudgingRulesDialog = defineAsyncComponent(() => import('../components/JudgingRulesDialog.vue'))
......@@ -10,12 +12,34 @@ interface Props {
}
const props = defineProps<Props>()
// 赛项类型
const types = useMapStore().getMapValuesByKey('competition_type')
// 数据状态
const status = useMapStore().getMapValuesByKey('system_status')
let detail = $ref<ContestItem | null>(null)
provide('detail', $$(detail))
// 赛项类型
const typeText = $computed(() => {
return types.find(item => item.value == detail?.type)?.label
})
// 承办单位
const orgText = $computed(() => {
return detail?.organizers.map(item => item.label).join('、')
})
// 指导教师
const teacherText = $computed(() => {
return detail?.teachers.map(item => item.name).join('、')
})
// 生效状态
const statusText = $computed(() => {
return status.find(item => item.value == detail?.status)?.label
})
function fetchInfo() {
getContestItem({ experiment_id: props.id }).then(res => {
detail = res.data
getContestItem({ id: props.id }).then(res => {
detail = res.data.detail
})
}
onMounted(fetchInfo)
......@@ -24,19 +48,32 @@ const judgingRulesVisible = $ref(true)
</script>
<template>
<AppCard title="赛项信息">
<AppCard title="查看赛项信息">
<template #header-aside>
<el-button type="primary" @click="judgingRulesVisible = true">评分规则</el-button>
<el-button type="primary">评分专家</el-button>
<el-button type="primary">参赛选手</el-button>
<el-button type="primary">评分细则</el-button>
</template>
<el-descriptions class="descriptions-box" v-if="detail">
<el-descriptions-item label="赛项名称:">{{ detail.name }}</el-descriptions-item>
<el-descriptions-item label="赛项课程:">{{ detail.course_name }}</el-descriptions-item>
<el-descriptions-item label="所属机构/学校:">{{ detail.organ_id_name }}</el-descriptions-item>
</el-descriptions>
<div class="top" v-if="detail">
<div class="top-cover">
<p>赛项封面:</p>
<img :src="detail.cover" />
</div>
<el-descriptions class="descriptions-box">
<el-descriptions-item label="赛项名称:">{{ detail.name }}</el-descriptions-item>
<el-descriptions-item label="赛项类型:">{{ typeText }}</el-descriptions-item>
<el-descriptions-item label="主办单位:">{{ detail.host_unit.label }}</el-descriptions-item>
<el-descriptions-item label="指导教师:">{{ teacherText }}</el-descriptions-item>
<el-descriptions-item label="承办单位:">{{ orgText }}</el-descriptions-item>
<el-descriptions-item label="赛项周期:"
>{{ detail.start_range }} ~ {{ detail.end_range }}</el-descriptions-item
>
<el-descriptions-item label="技术支持单位:">{{ detail.technical_support_unit.label }}</el-descriptions-item>
<el-descriptions-item label="正式比赛日期:">{{ detail.start_at }} ~ {{ detail.end_at }}</el-descriptions-item>
<el-descriptions-item label="生效状态:">{{ statusText }}</el-descriptions-item>
</el-descriptions>
</div>
</AppCard>
<AppCard title="训练指导书">
<ViewBook></ViewBook>
......@@ -44,5 +81,27 @@ const judgingRulesVisible = $ref(true)
<AppCard title="操作视频">
<ViewVideo></ViewVideo>
</AppCard>
<JudgingRulesDialog v-model="judgingRulesVisible" v-if="judgingRulesVisible"></JudgingRulesDialog>
<JudgingRulesDialog v-model="judgingRulesVisible" v-if="judgingRulesVisible && detail"></JudgingRulesDialog>
</template>
<style lang="scss">
.top {
display: flex;
.el-descriptions {
flex: 1;
margin-top: 30px;
}
}
.top-cover {
width: 200px;
margin-right: 20px;
p {
font-weight: normal;
line-height: 30px;
font-size: 14px;
}
img {
width: 100%;
}
}
</style>
......@@ -23,7 +23,7 @@ function handleChange(id: string, type: number) {
<div class="bg">
<router-link to="/student/lab" class="link1"></router-link>
<router-link to="/student/lab" class="link2"></router-link>
<router-link to="/student/lab" class="link3"></router-link>
<router-link to="/student/contest" class="link3"></router-link>
</div>
<div class="select-group">
<el-select size="large" placeholder="我的实验课程" @change="handleChange($event, 1)">
......@@ -31,13 +31,13 @@ function handleChange(id: string, type: number) {
</el-select>
<!-- <el-select size="large" placeholder="我的陪练项目" @change="handleChange($event, 2)">
<el-option v-for="item in list" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-select> -->
<el-select size="large" placeholder="我的大赛项目" @change="handleChange($event, 3)">
<el-option v-for="item in list" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
<el-select size="large" placeholder="我的大赛成绩" @change="handleChange($event, 4)">
<el-option v-for="item in list" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> -->
</el-select>
</div>
</template>
......
import httpRequest from '@/utils/axios'
import type { ContestJoinParams } from './types'
// 获取学员可以报名的所有赛项
export function getContestList() {
return httpRequest.get('/api/lab/v1/student/competition/list')
}
// 获取学员已报名的赛项列表
export function getMyContestList() {
return httpRequest.get('/api/lab/v1/student/competition/my-list')
}
// 获取学员详情
export function getStudentInfo() {
return httpRequest.get('/api/lab/v1/student/competition/student-detail')
}
// 分发参赛短信
export function sendApplySMS(data: { competition_id: string; mobile: string }) {
return httpRequest.post('/api/lab/v1/student/competition/send-apply-sms', data)
}
// 大赛报名
export function joinContest(data: ContestJoinParams) {
return httpRequest.post('/api/lab/v1/student/competition/apply', data)
}
// 获取实验指导书
export function getExperimentBook(params: { competition_id: string }) {
return httpRequest.get('/api/lab/v1/student/competition/book', { params })
}
// 获取实验视频
export function getExperimentVideoList(params: { competition_id: string }) {
return httpRequest.get('/api/lab/v1/student/competition/videos', { params })
}
// 获取实验视频播放信息
export function getExperimentVideoPlayInfo(params: { source_id: string }) {
return httpRequest.get('/api/lab/v1/student/competition/replay-list', { params })
}
// 获取实验讨论交流
export function getExperimentDiscussList(params: {
competition_id: string
tag: number
page?: number
'per-page'?: number
}) {
return httpRequest.get('/api/lab/v1/student/competition/discussion-list', { params })
}
// 发表新话题
export function addExperimentDiscuss(data: { competition_id: string; title: string; content: string }) {
return httpRequest.post('/api/lab/v1/student/competition/discussion-create', data)
}
// 发表回复
export function addExperimentDiscussComment(data: { discussion_id: string; content: string }) {
return httpRequest.post('/api/lab/v1/student/competition/discussion-reply-create', data)
}
// 获取实验截图记录
export function getExperimentRecord(params: { competition_id: string }) {
return httpRequest.get('/api/lab/v1/student/competition/pictures', { params })
}
// 截图
export function uploadExperimentPicture(data: { competition_id: string; pictures: string }) {
return httpRequest.post('/api/lab/v1/student/competition/save-pictures', data)
}
<script setup lang="ts">
import type { ExperimentBookType } from '../types'
import Preview from '@/components/Preview.vue'
import { getExperimentBook } from '../api'
interface Props {
competition_id: string
}
const props = defineProps<Props>()
let detail = $ref<ExperimentBookType>()
function fetchInfo() {
getExperimentBook({ competition_id: props.competition_id }).then(res => {
detail = res.data.detail
})
}
watchEffect(() => {
fetchInfo()
})
const isEmpty = $computed(() => {
return !props.competition_id || !detail?.id
})
</script>
<template>
<el-empty description="暂无数据" v-if="isEmpty" />
<template v-else>
<Preview :url="detail.url"></Preview>
</template>
</template>
<script setup lang="ts">
import type { Contest } from '../types'
interface Props {
data: Contest
}
const props = defineProps<Props>()
const isMy = $computed(() => {
return !!props.data.login_id
})
</script>
<template>
<div class="contest-item">
<div class="contest-item-main">
<div class="contest-item-main__bg"><img :src="data.cover" /></div>
<div class="contest-item-main__inner">
<img :src="data.logo" class="logo" />
<div class="cover">
<router-link :to="`/student/contest/lab/${data.id}`" target="_blank" v-if="isMy">
<el-button round type="primary">我要训练</el-button>
</router-link>
<router-link :to="{ path: '/student/contest/join', query: { id: data.id, name: data.name } }" v-else>
<el-button round type="primary">我要报名</el-button>
</router-link>
</div>
</div>
</div>
<div class="contest-item-name">{{ data.name }}</div>
</div>
</template>
<style lang="scss">
.contest-item-main {
position: relative;
height: 200px;
border-radius: 10px;
overflow: hidden;
}
.contest-item-main__bg {
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.5;
}
}
.contest-item-main__inner {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 1;
.logo {
width: 100%;
height: 100%;
object-fit: cover;
}
&:hover {
.cover {
display: flex;
}
}
.cover {
display: none;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
}
}
.contest-item-name {
margin-top: 10px;
padding: 10px;
font-size: 16px;
color: #fff;
text-align: center;
background-color: var(--main-color);
border-radius: 10px;
}
</style>
<script setup lang="ts">
import type { ExperimentDiscussType } from '../types'
import { Loading } from '@element-plus/icons-vue'
import { useInfiniteScroll } from '@vueuse/core'
import DiscussItem from './DiscussItem.vue'
import { getExperimentDiscussList } from '../api'
const DiscussPublish = defineAsyncComponent(() => import('./DiscussPublish.vue'))
interface Props {
competition_id: string
}
const props = defineProps<Props>()
const params = reactive({ tag: 1, page: 1, 'per-page': 10 })
let list = $ref<ExperimentDiscussType[]>([])
let hasMore = $ref(false)
let isLoading = $ref(false)
function fetchInfo(isForce = false) {
if (!props.competition_id) return
if (isForce) {
params.page = 1
}
isLoading = true
getExperimentDiscussList({ ...params, competition_id: props.competition_id })
.then(res => {
list = isForce ? res.data.list : [...list, ...res.data.list]
hasMore = !!res.data.list.length
})
.finally(() => {
isLoading = false
})
}
watch(
() => props.competition_id,
() => {
fetchInfo(true)
},
{ immediate: true }
)
const isEmpty = $computed(() => {
return !props.competition_id || !list.length
})
// 滚动加载
const scrollRef = ref<HTMLElement>()
useInfiniteScroll(
scrollRef,
() => {
if (!hasMore || isLoading) return
params.page++
fetchInfo()
},
{ distance: 10 }
)
</script>
<template>
<div class="discuss">
<el-radio-group size="small" v-model="params.tag" @change="fetchInfo(true)">
<el-radio :label="1">我的话题</el-radio>
<!-- <el-radio :label="2">我回复的</el-radio>
<el-radio :label="3">我的小组</el-radio>
<el-radio :label="4">我的班级</el-radio> -->
</el-radio-group>
<!-- 发表新话题 -->
<DiscussPublish :competition_id="competition_id" @update="fetchInfo(true)" v-if="competition_id"></DiscussPublish>
<el-empty description="暂无数据" v-if="isEmpty" />
<template v-else>
<div class="discuss-scroll" ref="scrollRef">
<DiscussItem v-for="item in list" :key="item.id" :data="item" @update="fetchInfo(true)"></DiscussItem>
<div class="tips" v-if="isLoading">
<el-icon class="is-loading">
<Loading />
</el-icon>
加载中...
</div>
<div class="tips" v-if="!hasMore">没有更多了</div>
</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.discuss {
display: flex;
height: 100%;
flex-direction: column;
.el-radio {
margin-right: 12px;
}
}
.discuss-scroll {
flex: 1;
overflow-y: auto;
}
.tips {
padding: 40px;
color: #555;
text-align: center;
}
.el-icon.is-loading {
animation: rotating 2s linear infinite;
}
</style>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import type { ExperimentDiscussType } from '../types'
import { ElMessage } from 'element-plus'
import { addExperimentDiscussComment } from '../api'
interface Props {
data: ExperimentDiscussType
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update'): void
}>()
const formRef = $ref<FormInstance>()
const form = reactive({ content: '' })
const rules = ref<FormRules>({
content: [{ required: true, message: '请输入话题评论内容', trigger: 'blur' }]
})
// 提交
function handleSubmit() {
formRef?.validate().then(handleAdd)
}
// 发布评论
function handleAdd() {
const params = { ...form, discussion_id: props.data.id }
addExperimentDiscussComment(params).then(() => {
ElMessage({ message: '评论成功', type: 'success' })
emit('update')
formRef?.resetFields()
})
}
</script>
<template>
<el-form
ref="formRef"
:model="form"
:rules="rules"
style="border-top: 1px dashed #e6e6e6; border-bottom: 1px dashed #e6e6e6; padding: 20px 0"
>
<el-form-item prop="content">
<el-input type="textarea" v-model="form.content" :autosize="{ minRows: 4, maxRows: 6 }" />
</el-form-item>
<el-row justify="end">
<el-button type="primary" @click="handleSubmit">评论</el-button>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import type { ExperimentDiscussType } from '../types'
import { ChatLineRound } from '@element-plus/icons-vue'
import Avatar from '@/components/Avatar.vue'
const DiscussCommentPublish = defineAsyncComponent(() => import('./DiscussCommentPublish.vue'))
interface Props {
data: ExperimentDiscussType
}
defineProps<Props>()
defineEmits<{
(e: 'update'): void
}>()
const commentVisible = $ref(false)
</script>
<template>
<div class="discuss-item">
<div class="discuss-box">
<div class="discuss-box-user">
<div class="discuss-box-user__avatar"><Avatar :src="data.sso_user.avatar" /></div>
<div class="discuss-box-user__content">
<h3>{{ data.sso_user.real_name || data.sso_user.nickname || data.sso_user.username }}</h3>
<p>{{ data.created_time }}</p>
</div>
</div>
<div class="discuss-box-main">
<h3>{{ data.title }}</h3>
<div class="discuss-box-content" v-html="data.content"></div>
</div>
<div class="discuss-box-footer">
<div class="button-comment" :class="{ 'is-active': commentVisible }" @click="commentVisible = !commentVisible">
<el-icon><ChatLineRound></ChatLineRound></el-icon>{{ data.reply_count }}
</div>
</div>
</div>
<template v-if="commentVisible">
<!-- 我要评论 -->
<DiscussCommentPublish :data="data" @update="$emit('update')"></DiscussCommentPublish>
<div class="discuss-comment" v-for="item in data.competitionDiscussionReplies" :key="item.id">
<div class="discuss-comment__avatar">
<Avatar :src="item.sso_user.avatar" />
</div>
<div class="discuss-comment-content">
<span class="discuss-comment__username">{{
item.sso_user.real_name || item.sso_user.nickname || item.sso_user.username
}}</span
>
<span v-html="item.content"></span>
<p class="discuss-comment-time">{{ item.created_time }}</p>
</div>
</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.discuss-item {
margin-top: 10px;
border-top: 1px solid #e6e6e6;
}
.discuss-box {
padding: 20px 0 10px;
}
.discuss-box-user {
display: flex;
align-items: center;
}
.discuss-box-user__avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.discuss-box-user__content {
flex: 1;
margin-left: 10px;
overflow: hidden;
h3 {
font-size: 16px;
font-weight: 500;
line-height: 20px;
color: #333333;
}
p {
font-size: 14px;
font-weight: 400;
line-height: 20px;
color: #999999;
}
}
.discuss-box-main {
margin-left: 50px;
h3 {
padding: 16px 0;
font-size: 16px;
font-weight: 500;
line-height: 20px;
color: #333333;
}
}
.discuss-box-content {
font-size: 16px;
font-weight: 400;
line-height: 24px;
color: #333333;
}
.discuss-box-footer {
display: flex;
align-items: center;
justify-content: flex-end;
margin-top: 10px;
.button-comment {
display: flex;
align-items: center;
justify-content: center;
color: #333333;
cursor: pointer;
&.is-active {
color: var(--main-color);
}
}
.el-icon {
font-size: 16px;
margin-left: 10px;
margin-right: 5px;
}
}
.discuss-comment {
display: flex;
margin-top: 20px;
}
.discuss-comment__avatar {
width: 32px;
height: 32px;
border-radius: 50%;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.discuss-comment-content {
flex: 1;
margin-left: 10px;
color: #333;
overflow: hidden;
}
.discuss-comment__username {
font-size: 16px;
line-height: 20px;
color: var(--main-color);
}
.discuss-comment-time {
margin-top: 10px;
font-size: 14px;
line-height: 20px;
color: #999999;
text-align: right;
}
</style>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import { ElMessage } from 'element-plus'
import { addExperimentDiscuss } from '../api'
interface Props {
competition_id: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update'): void
}>()
const formRef = $ref<FormInstance>()
const form = reactive({ title: '', content: '' })
const rules = ref<FormRules>({
title: [{ required: true, message: '请输入话题标题' }],
content: [{ required: true, message: '请输入话题描述' }]
})
// 提交
function handleSubmit() {
formRef?.validate().then(handleAdd)
}
// 创建话题
function handleAdd() {
const params = { ...form, competition_id: props.competition_id }
addExperimentDiscuss(params).then(() => {
ElMessage({ message: '发表成功', type: 'success' })
emit('update')
formRef?.resetFields()
})
}
</script>
<template>
<el-form ref="formRef" :model="form" :rules="rules" style="padding: 20px 0 10px">
<el-form-item prop="title">
<el-input v-model="form.title" placeholder="话题标题"></el-input>
</el-form-item>
<el-form-item prop="content">
<el-input type="textarea" v-model="form.content" placeholder="话题描述" :autosize="{ minRows: 4, maxRows: 6 }" />
</el-form-item>
<el-row justify="end">
<el-button type="primary" @click="handleSubmit">发表</el-button>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import type { ExperimentRecord } from '../types'
import { ElMessageBox } from 'element-plus'
import ImageViewer from '@/components/ImageViewer.vue'
import { uploadExperimentPicture } from '../api'
interface Props {
competition_id: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'update'): void
}>()
const detail = $ref<ExperimentRecord>(inject('detail'))
const isEmpty = $computed(() => {
return !props.competition_id || !detail
})
let imageViewerVisible = $ref<boolean>(false)
let imageViewerIndex = $ref<number>(0)
// 查看
function handlePreview(index: number) {
imageViewerVisible = true
imageViewerIndex = index
}
// 删除
function handleRemove(index: number) {
ElMessageBox.confirm('删除之后无法恢复,确认删除该截图吗?', '提示').then(() => {
const pictures = detail.pictures.filter((item, i) => i !== index)
uploadExperimentPicture({ competition_id: props.competition_id, pictures: JSON.stringify(pictures) }).then(() => {
emit('update')
})
})
}
</script>
<template>
<el-empty description="暂无数据" v-if="isEmpty" />
<template v-else>
<h3>实验过程</h3>
<ul class="picture-list">
<li v-for="(item, index) in detail.pictures" :key="item.url">
<img :src="item.url" />
<p>截图时间:{{ item.upload_time }}</p>
<div class="cover">
<div class="cover-inner">
<el-button type="primary" plain round @click="handlePreview(index)">查看</el-button>
<el-button type="primary" plain round @click="handleRemove(index)">删除</el-button>
</div>
</div>
</li>
</ul>
<ImageViewer v-model="imageViewerVisible" :index="imageViewerIndex" :items="detail.pictures"></ImageViewer>
</template>
</template>
<style lang="scss" scoped>
.result-score {
margin-bottom: 40px;
text-align: center;
h2 {
font-size: 18px;
font-weight: 400;
line-height: 30px;
color: #333333;
text-align: center;
}
.t1 {
margin: 20px auto 10px;
padding-top: 64px;
width: 80px;
height: 110px;
font-size: 24px;
font-weight: bold;
line-height: 1;
color: #ffffff;
background: url(@/assets/images/score_bg.png) no-repeat;
background-size: contain;
box-sizing: border-box;
}
.t2 {
font-size: 14px;
line-height: 24px;
color: #666666;
}
}
h3 {
font-size: 16px;
font-weight: 400;
line-height: 27px;
color: #333333;
}
.picture-list {
li {
position: relative;
height: 200px;
margin: 20px 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
p {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 0 10px;
font-size: 14px;
color: #fff;
line-height: 30px;
text-align: right;
background-color: rgba(0, 0, 0, 0.5);
}
&:hover {
.cover {
display: block;
}
}
}
.cover {
display: none;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.cover-inner {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.el-button {
width: 100px;
margin: 10px 0;
}
}
}
</style>
<script setup lang="ts">
import type { ExperimentVideoType } from '../types'
import VideoItem from './VideoItem.vue'
import { getExperimentVideoList } from '../api'
interface Props {
competition_id: string
}
const props = defineProps<Props>()
let list = $ref<ExperimentVideoType[]>([])
function fetchInfo() {
if (!props.competition_id) return
getExperimentVideoList({ competition_id: props.competition_id }).then(res => {
list = res.data.list
})
}
watchEffect(() => {
fetchInfo()
})
const isEmpty = $computed(() => {
return !props.competition_id || !list.length
})
</script>
<template>
<el-empty description="暂无数据" v-if="isEmpty" />
<template v-else>
<VideoItem v-for="item in list" :key="item.id" :data="item" :competition_id="competition_id"></VideoItem>
</template>
</template>
<style lang="scss" scoped></style>
<script setup lang="ts">
import type { ExperimentVideoType, PlayInfo } from '../types'
import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue'
import { getExperimentVideoPlayInfo } from '../api'
// import { useLog } from '@/composables/useLog'
// const log = useLog()
interface Props {
competition_id: string
data: ExperimentVideoType
}
const props = defineProps<Props>()
let playList = $ref<PlayInfo[]>([])
function fetchInfo() {
getExperimentVideoPlayInfo({ source_id: props.data.source_id }).then(res => {
playList = res.data.play_info_list
// 日志上送
// log.upload({
// event: 'video_event',
// action: 'experiment_video_stu_watch_action',
// data: {
// competition_id: props.competition_id,
// course_id: props.course_id,
// video_id: props.data.id
// }
// })
})
}
const playUrl = $computed(() => {
return playList[0]?.PlayURL || ''
})
onMounted(() => {
fetchInfo()
})
</script>
<template>
<div class="video-item">
<h2>{{ data.name }}</h2>
<!-- <img :src="data.cover" /> -->
<div style="height: 200px" v-if="playUrl">
<AppVideoPlayer
:options="{ sources: [{ src: playUrl, type: 'application/x-mpegURL' }] }"
style="width: 100%; height: 100%"
></AppVideoPlayer>
</div>
</div>
</template>
<style lang="scss" scoped>
.video-item {
h2 {
font-size: 14px;
font-weight: 500;
color: #333;
padding: 10px 0;
text-align: center;
}
img {
width: 100%;
height: 200px;
object-fit: cover;
}
}
</style>
import type { RouteRecordRaw } from 'vue-router'
import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/student/contest',
component: AppLayout,
children: [
{ path: '', component: () => import('./views/Index.vue') },
{ path: 'join', component: () => import('./views/Join.vue') },
{ path: 'lab/:id', component: () => import('./views/Lab.vue'), props: true }
]
}
]
import type { SystemDictionary } from '@/types'
export interface Contest {
apply_expiration_date: string
competition_uri: string
cover: string
end_at: string
end_range: string
host_unit: SystemDictionary
id: string
login_id: string
logo: string
name: string
org_name: string
organizers: SystemDictionary[]
start_at: string
start_range: string
student_id: string
student_name: string
technical_support_unit: SystemDictionary
train_platform_uri: string
type: string
}
export type ContestJoinParams = {
competition_id: string
mode: string
picture: string
grade: string
teacher_name: string
sms_code: string
}
export type ExperimentBookType = {
id: string
name: string
competition_id: string
type: string
url: string
}
export interface ExperimentVideoType {
id: string
name: string
size: string
type: string
source_id: string
length: number
cover: string
}
export interface PlayInfo {
BitDepth: number
Bitrate: string
CreationTime: string
Definition: string
Duration: string
Encrypt: number
Format: string
Fps: string
HDRType: string
Height: number
JobId: string
ModificationTime: string
NarrowBandType: string
PlayURL: string
PreprocessStatus: string
Size: number
Specification: string
Status: string
StreamType: string
Width: number
}
export interface ExperimentDiscussType {
content: string
created_operator: string
created_time: string
id: string
is_reply: string
competitionDiscussionReplies: ExperimentDiscussCommentType[]
reply_count: number
sso_user: UserType
student_id: string
title: string
updated_time: string
}
export interface ExperimentDiscussCommentType {
content: string
created_time: string
discussion_id: string
id: string
role: string
sso_id: string
sso_user: UserType
}
export interface UserType {
avatar: string
id: string
nickname: string
real_name: string
username: string
}
export type ExperimentRecord = {
competition_id: string
created_time: string
id: string
pictures: ExperimentRecordFile[]
student_id: string
updated_time: string
}
export interface ExperimentRecordFile {
url: string
name: string
upload_time: string
size?: number
}
<script setup lang="ts">
import type { Contest } from '../types'
import ContestItem from '../components/ContestItem.vue'
import { getMyContestList, getContestList } from '../api'
let myContestList = $ref<Contest[]>([])
function fetchMyList() {
getMyContestList().then(res => {
myContestList = res.data.list
})
}
let contestList = $ref<Contest[]>([])
function fetchList() {
getContestList().then(res => {
contestList = res.data.list
})
}
onMounted(() => {
fetchMyList()
fetchList()
})
</script>
<template>
<AppCard title="我的赛项">
<div class="contest-list" v-if="myContestList.length">
<ContestItem :data="item" v-for="item in myContestList" :key="item.id"></ContestItem>
</div>
<el-empty description="暂无数据" v-else />
</AppCard>
<AppCard title="可参与赛项">
<div class="contest-list" v-if="contestList.length">
<ContestItem :data="item" v-for="item in contestList" :key="item.id"></ContestItem>
</div>
<el-empty description="暂无数据" v-else />
</AppCard>
</template>
<style lang="scss">
.contest-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
</style>
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import type { ContestJoinParams } from '../types'
import AppUpload from '@/components/base/AppUpload.vue'
import { pick } from 'lodash-es'
import { contestModeList } from '@/utils/dictionary'
import { useMapStore } from '@/stores/map'
import { useCountdown } from '@/composables/useCountdown'
import { joinContest, getStudentInfo, sendApplySMS } from '../api'
const { second, disabled, start } = useCountdown()
const countdownText = $computed(() => {
return disabled.value ? `(${second.value})秒` : '点击获取'
})
// 发送验证码
function sendSMS() {
start()
sendApplySMS({ competition_id: form.competition_id, mobile: form.mobile }).catch(() => {
stop()
})
}
const router = useRouter()
const route = useRoute()
// 性别
const genderList = useMapStore().getMapValuesByKey('system_gender')
// 证件类型
const systemIdTypeList = useMapStore().getMapValuesByKey('system_id_type')
function fetchStudentInfo() {
getStudentInfo().then(res => {
const { student = {}, specialty = {}, org = {}, classes = [] } = res.data
const [studentClass = {}] = classes
Object.assign(form, {
student_name: student.name,
gender: student.gender.toString(),
id_type: student.id_type.toString(),
id_number: student.id_number,
mobile: student.mobile,
org_name: org.name,
province_name: student.info?.province_name,
city_name: student.info?.city_name,
county_name: student.info?.county_name,
specialty_name: specialty.name,
class_name: studentClass.name
})
})
}
onMounted(fetchStudentInfo)
const formRef = $ref<FormInstance>()
const form = reactive({
name: route.query.name as string,
competition_id: route.query.id as string,
mode: '1',
student_name: '',
gender: '1',
id_type: '1',
id_number: '',
mobile: '',
org_name: '',
province_name: '',
city_name: '',
county_name: '',
specialty_name: '',
grade: '',
class_name: '',
teacher_name: '',
picture: '',
sms_code: '',
protocol: false
})
const checkProtocol = (rule: any, value: any, callback: any) => {
if (!value) {
return callback(new Error('请确认'))
} else {
callback()
}
}
const rules = ref<FormRules>({
name: [{ required: true, message: '请输入实验指导书名称', trigger: 'blur' }],
grade: [{ required: true, message: '请输入年级' }],
teacher_name: [{ required: true, message: '请输入指导教师' }],
picture: [{ required: true, message: '请上传证件照' }],
sms_code: [{ required: true, message: '请输入验证码' }],
protocol: [{ validator: checkProtocol, trigger: 'change' }]
})
let dialogVisible = $ref(false)
// 提交
function handleSubmit() {
formRef?.validate().then(handleJoin)
}
// 报名
function handleJoin() {
const params: ContestJoinParams = pick(form, [
'competition_id',
'mode',
'picture',
'grade',
'teacher_name',
'sms_code'
])
joinContest(params).then(() => {
dialogVisible = true
})
}
function handleCancel() {
router.replace('/student/contest')
}
</script>
<template>
<AppCard title="大赛报名">
<el-form ref="formRef" :model="form" :rules="rules" label-width="124px" style="width: 600px; margin: 0 auto">
<el-form-item label="报名赛项">
<el-input v-model="form.name" disabled />
</el-form-item>
<el-form-item label="参赛形式" prop="mode">
<el-radio-group v-model="form.mode">
<el-radio v-for="item in contestModeList" :key="item.value" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="姓名">
<el-input v-model="form.student_name" disabled />
</el-form-item>
<el-form-item label="性别">
<el-radio-group v-model="form.gender" disabled>
<el-radio v-for="item in genderList" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="证件类型">
<el-radio-group v-model="form.id_type" disabled>
<el-radio v-for="item in systemIdTypeList" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="证件号码">
<el-input v-model="form.id_number" disabled />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="form.mobile" disabled />
</el-form-item>
<el-form-item label="学校名称">
<el-input v-model="form.org_name" disabled />
</el-form-item>
<el-form-item label="省">
<el-input v-model="form.province_name" disabled />
</el-form-item>
<el-form-item label="市">
<el-input v-model="form.city_name" disabled />
</el-form-item>
<el-form-item label="区/县">
<el-input v-model="form.county_name" disabled />
</el-form-item>
<el-form-item label="专业名称">
<el-input v-model="form.specialty_name" disabled />
</el-form-item>
<el-form-item label="年级" prop="grade">
<el-input v-model="form.grade" />
</el-form-item>
<el-form-item label="班级">
<el-input v-model="form.class_name" disabled />
</el-form-item>
<el-form-item label="指导教师" prop="teacher_name">
<el-input v-model="form.teacher_name" />
</el-form-item>
<el-form-item label="证件照" prop="picture">
<AppUpload v-model="form.picture" accept="image/*">
<template #tip>证件照说明:请上传蓝底1寸或蓝底2寸证件照,照片大小控制在500kb以内。</template>
</AppUpload>
</el-form-item>
<el-form-item label="验证码" prop="sms_code">
<el-input v-model="form.sms_code">
<template #append>
<el-button type="primary" :disabled="disabled" @click="sendSMS">{{ countdownText }}</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item prop="protocol">
<el-checkbox label="我已确认上述信息输入无误,并对上述信息填写内容负责。" v-model="form.protocol" />
</el-form-item>
<el-row justify="center">
<el-button type="primary" round auto-insert-space @click="handleSubmit">保存</el-button>
<el-button round auto-insert-space @click="handleCancel">取消</el-button>
</el-row>
</el-form>
</AppCard>
<el-dialog title="报名成功" v-model="dialogVisible" :width="400" align-center center @closed="handleCancel">
<el-result icon="success" title="您已经成功报名赛项!">
<template #extra>
<el-button type="primary" @click="handleCancel">确定</el-button>
</template>
</el-result>
</el-dialog>
</template>
<script setup lang="ts">
import type { ExperimentRecord } from '../types'
import { HomeFilled } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import { upload } from '@/utils/upload'
import { getExperimentRecord, uploadExperimentPicture } from '../api'
import dayjs from 'dayjs'
const Book = defineAsyncComponent(() => import('../components/Book.vue'))
const Video = defineAsyncComponent(() => import('../components/Video.vue'))
const Discuss = defineAsyncComponent(() => import('../components/Discuss.vue'))
const Result = defineAsyncComponent(() => import('../components/Result.vue'))
interface Props {
id: string
}
const props = defineProps<Props>()
// 左侧
const leftPanelVisible = $ref<boolean>(true)
let detail = $ref<ExperimentRecord>()
provide('detail', $$(detail))
function fetchInfo() {
getExperimentRecord({ competition_id: props.id }).then(res => {
const data = res.data.detail || {}
let pictures = []
try {
pictures = JSON.parse(data.pictures)
} catch (error) {
console.log(error)
}
detail = Object.assign(data, { pictures })
})
}
watchEffect(() => {
fetchInfo()
})
// 右侧
const LAB_URL = import.meta.env.VITE_LAB_URL
let iframeKey = $ref(Date.now())
// 返回首页
function handleBackHome() {
ElMessageBox.confirm('此操作将会强制返回到实验室首页,您当前的操作内容有可能丢失,确定返回首页吗?', '提示').then(
() => {
iframeKey = Date.now()
}
)
}
const iframeRef = $ref<HTMLIFrameElement>()
let screenshotLoading = $ref(false)
let screenshotTimestamp = $ref(0)
// 截图
function handleCapture() {
const pictures = detail?.pictures || []
if (pictures.length >= 20) {
ElMessage.error('已达到单个实验最大截图数量,无法截图,请在过程与结果功能中删除多余截图之后再进行操作。')
return
}
const iframeWindow = iframeRef?.contentWindow
if (!iframeWindow) return
screenshotTimestamp = Date.now()
screenshotLoading = true
// 发送截图消息
iframeWindow?.postMessage({ action: 'screenshot', timestamp: screenshotTimestamp }, '*')
}
// 截图之后
function handleCaptureCallback(event: MessageEvent) {
const { data } = event
if (data.action === 'screenshot' && data.timestamp === screenshotTimestamp) {
upload(data.blob).then(url => {
uploadPicture(url)
})
}
}
onMounted(() => {
window.addEventListener('message', handleCaptureCallback, false)
})
onUnmounted(() => {
window.removeEventListener('message', handleCaptureCallback, false)
})
// 上传截图
function uploadPicture(url: string) {
const pictures = detail?.pictures || []
pictures.unshift({ url, name: Date.now() + '.png', upload_time: dayjs().format('YYYY-MM-DD HH:mm:ss'), size: 1024 })
uploadExperimentPicture({ competition_id: props.id, pictures: JSON.stringify(pictures) }).then(() => {
fetchInfo()
screenshotLoading = false
})
}
</script>
<template>
<section class="lab">
<div class="lab-left" :class="{ 'is-hidden': !leftPanelVisible }">
<div class="lab-left__inner">
<el-tabs type="border-card" stretch>
<el-tab-pane label="实训指导" lazy>
<Book :competition_id="id"></Book>
</el-tab-pane>
<el-tab-pane label="操作视频" lazy>
<Video :competition_id="id"></Video>
</el-tab-pane>
<el-tab-pane label="讨论交流" lazy>
<Discuss :competition_id="id"></Discuss>
</el-tab-pane>
<el-tab-pane label="过程与结果" lazy>
<Result :competition_id="id" @update="fetchInfo"></Result>
</el-tab-pane>
</el-tabs>
</div>
<div class="panel-icon" @click="leftPanelVisible = !leftPanelVisible">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 86" aria-hidden="true" width="16" height="86">
<g fill="none" fill-rule="evenodd">
<path
class="path-wapper"
d="M0 0l14.12 8.825A4 4 0 0116 12.217v61.566a4 4 0 01-1.88 3.392L0 86V0z"
fill="#e1e4eb"
></path>
<path
class="path-arrow"
d="M10.758 48.766a.778.778 0 000-1.127L6.996 43l3.762-4.639a.778.778 0 000-1.127.85.85 0 00-1.172 0l-4.344 5.202a.78.78 0 000 1.128l4.344 5.202a.85.85 0 001.172 0z"
fill="#8D9EA7"
fill-rule="nonzero"
></path>
</g>
</svg>
</div>
</div>
<div class="lab-right">
<AppCard>
<el-row justify="space-between">
<el-button type="primary" :icon="HomeFilled" @click="handleBackHome">返回首页</el-button>
<div>
<el-button type="primary" :loading="screenshotLoading" @click="handleCapture">截图</el-button>
</div>
</el-row>
</AppCard>
<div class="lab-box">
<iframe :src="LAB_URL" :key="iframeKey" frameborder="0" class="iframe" ref="iframeRef"></iframe>
</div>
</div>
</section>
</template>
<style lang="scss" scoped>
.lab {
display: flex;
height: calc(100vh - 110px);
}
.lab-left {
position: relative;
width: 400px;
padding: 20px;
background-color: #e1e4eb;
border-radius: 6px;
box-sizing: border-box;
transition: all 0.1s;
&.is-hidden {
width: 0;
padding: 0;
.panel-icon {
left: -20px;
right: 0;
}
.path-arrow {
transform-origin: center center;
transform: rotate(180deg);
}
}
.lab-left__inner {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
}
.el-tabs {
flex: 1;
border: 0;
overflow: hidden;
}
:deep(.el-tabs__header) {
background-color: #e1e4eb;
}
:deep(.el-tabs__item) {
padding: 0 14px !important;
border: 0;
border-radius: 6px 6px 0px 0px;
}
:deep(.el-tabs__content) {
height: calc(100% - 40px);
box-sizing: border-box;
}
:deep(.el-tab-pane) {
height: 100%;
overflow-y: auto;
}
}
.panel-icon {
position: absolute;
top: 50%;
right: -16px;
width: 16px;
height: 86px;
transform: translateY(-50%);
z-index: 100;
cursor: pointer;
}
.lab-right {
margin-left: 20px;
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
}
.lab-box {
flex: 1;
width: 100%;
margin-top: 20px;
}
.iframe {
width: 100%;
height: 100%;
}
</style>
......@@ -12,7 +12,7 @@ const studentMenus: IMenuItem[] = [
{
name: '智能营销',
path: '/student/lab'
}
},
// {
// name: '智能陪练',
// path: '/student/ai'
......@@ -21,10 +21,10 @@ const studentMenus: IMenuItem[] = [
// name: '成绩分析',
// path: '/admin/contest/score'
// },
// {
// name: '技能大赛',
// path: '/student/contest'
// }
{
name: '技能大赛',
path: '/student/contest'
}
]
// 教师、管理员菜单
const adminMenus: IMenuItem[] = [
......
import type { UserType, ProjectType, OrganizationType, RoleType, PermissionType } from '@/types'
import { defineStore } from 'pinia'
import { getUser, logout } from '@/api/base'
import type { UserType, ProjectType, OrganizationType, RoleType, PermissionType } from '@/types'
import { useMapStore } from '@/stores/map'
// 角色信息(1学员;5教师)
interface Role {
......@@ -40,6 +41,7 @@ export const useUserStore = defineStore({
this.project = project
this.roles = roles
this.permissions = permissions
await useMapStore().getMapList()
},
async logout() {
await logout()
......
......@@ -78,3 +78,9 @@ export interface MessageType {
type: 1 | 2
updated_at: string
}
export interface SystemDictionary {
id: string
label: string
value: string
}
// json to array
export const json2Array = function (data, isValueToNumber = true) {
export const json2Array = function (data: any, isValueToNumber = true) {
return Object.keys(data).map(value => ({ label: data[value], value: isValueToNumber ? parseInt(value) : value }))
}
// 组卷模式
export const paperType = {
1: '选题组卷',
2: '自动组卷',
3: '自由组卷'
// 参赛模式
export const contestMode = {
1: '个人赛'
}
// 组卷模式列表
export const paperTypeList = json2Array(paperType)
// 试题类型
export const questionType = {
1: '单选题',
2: '多选题',
3: '问答题',
5: '案例题',
6: '判断题',
7: '实操题',
8: '情景题'
}
// 试题类型列表
export const questionTypeList = json2Array(questionType, false)
// 试题难度
export const questionDifficulty = {
1: '易',
2: '中',
3: '难',
0: '无'
}
// 试题难度列表
export const questionDifficultyList = json2Array(questionDifficulty, false)
// 参赛模式列表
export const contestModeList = json2Array(contestMode, false)
import fs from 'node:fs'
import path from 'node:path'
// import fs from 'node:fs'
// import path from 'node:path'
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// import checker from 'vite-plugin-checker'
import AutoImport from 'unplugin-auto-import/vite'
import mkcert from 'vite-plugin-mkcert'
export default defineConfig(({ mode }) => ({
base: mode === 'prod' ? 'https://webapp-pub.ezijing.com/website/prod/saas-lab/' : '/',
......@@ -15,19 +16,21 @@ export default defineConfig(({ mode }) => ({
imports: ['vue', 'vue/macros', 'vue-router', '@vueuse/core'],
dts: true,
eslintrc: { enabled: true }
})
}),
// checker({ vueTsc: true, eslint: { lintCommand: 'eslint "./src/**/*.{vue,js,jsx,ts,tsx}"' } })
mkcert()
],
server: {
open: true,
host: 'dev.ezijing.com',
https: {
key: fs.readFileSync(path.join(__dirname, './https/dev.ezijing.com.key')),
cert: fs.readFileSync(path.join(__dirname, './https/dev.ezijing.com.pem'))
},
https: true,
// https: {
// key: fs.readFileSync(path.join(__dirname, './https/dev.ezijing.com.key')),
// cert: fs.readFileSync(path.join(__dirname, './https/dev.ezijing.com.pem'))
// },
proxy: {
// '/api/lab': {
// target: 'https://resource-api-test.ezijing.com',
// target: 'http://test-resource-api.ezijing.com:8001',
// changeOrigin: true,
// rewrite: path => path.replace(/^\/api\/lab/, '')
// },
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论