2 Commits edc87ec3e0 ... c496ffabb2

Auteur SHA1 Message Date
  zjq c496ffabb2 Merge remote-tracking branch 'origin/test/exam' into dev il y a 2 semaines
  zjq a3a7c02a97 自我练习 il y a 2 semaines

+ 121 - 50
package-lock.json

@@ -20,7 +20,7 @@
 				"axios": "^1.4.0",
 				"dayjs": "^1.11.9",
 				"echarts": "^5.5.0",
-				"element-plus": "^2.7.7",
+				"element-plus": "^2.9.2",
 				"file-saver": "^2.0.5",
 				"html-docx-js-typescript": "^0.1.5",
 				"html2canvas": "^1.4.1",
@@ -2347,9 +2347,9 @@
 			}
 		},
 		"node_modules/@types/babel__generator": {
-			"version": "7.6.8",
-			"resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.6.8.tgz",
-			"integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==",
+			"version": "7.27.0",
+			"resolved": "https://registry.npmmirror.com/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+			"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
 			"dependencies": {
 				"@babel/types": "^7.0.0"
 			}
@@ -2364,9 +2364,9 @@
 			}
 		},
 		"node_modules/@types/babel__traverse": {
-			"version": "7.20.6",
-			"resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz",
-			"integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==",
+			"version": "7.20.7",
+			"resolved": "https://registry.npmmirror.com/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
+			"integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
 			"dependencies": {
 				"@babel/types": "^7.20.7"
 			}
@@ -3337,7 +3337,8 @@
 		"node_modules/abab": {
 			"version": "2.0.6",
 			"resolved": "https://registry.npmmirror.com/abab/-/abab-2.0.6.tgz",
-			"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA=="
+			"integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==",
+			"deprecated": "Use your platform's native atob() and btoa() methods instead"
 		},
 		"node_modules/abort-controller": {
 			"version": "3.0.0",
@@ -3774,9 +3775,9 @@
 			}
 		},
 		"node_modules/call-bind-apply-helpers": {
-			"version": "1.0.1",
-			"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
-			"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
+			"version": "1.0.2",
+			"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+			"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
 			"dependencies": {
 				"es-errors": "^1.3.0",
 				"function-bind": "^1.1.2"
@@ -3908,9 +3909,9 @@
 			}
 		},
 		"node_modules/cjs-module-lexer": {
-			"version": "1.4.1",
-			"resolved": "https://registry.npmmirror.com/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz",
-			"integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA=="
+			"version": "1.4.3",
+			"resolved": "https://registry.npmmirror.com/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz",
+			"integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="
 		},
 		"node_modules/cliui": {
 			"version": "7.0.4",
@@ -4124,9 +4125,9 @@
 			}
 		},
 		"node_modules/decimal.js": {
-			"version": "10.4.3",
-			"resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.4.3.tgz",
-			"integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="
+			"version": "10.5.0",
+			"resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.5.0.tgz",
+			"integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw=="
 		},
 		"node_modules/dedent": {
 			"version": "0.7.0",
@@ -4226,6 +4227,7 @@
 			"version": "2.0.1",
 			"resolved": "https://registry.npmmirror.com/domexception/-/domexception-2.0.1.tgz",
 			"integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==",
+			"deprecated": "Use your platform's native DOMException instead",
 			"dependencies": {
 				"webidl-conversions": "^5.0.0"
 			},
@@ -4260,11 +4262,11 @@
 			}
 		},
 		"node_modules/dunder-proto": {
-			"version": "1.0.0",
-			"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.0.tgz",
-			"integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==",
+			"version": "1.0.1",
+			"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+			"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
 			"dependencies": {
-				"call-bind-apply-helpers": "^1.0.0",
+				"call-bind-apply-helpers": "^1.0.1",
 				"es-errors": "^1.3.0",
 				"gopd": "^1.2.0"
 			},
@@ -4287,9 +4289,9 @@
 			"integrity": "sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw=="
 		},
 		"node_modules/element-plus": {
-			"version": "2.9.0",
-			"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.9.0.tgz",
-			"integrity": "sha512-ccOFXKsauo2dtokAr4OX7gZsb7TuAoVxA2zGRZo5o2yyDDBLBaZxOoFQPoxITSLcHbBfQuNDGK5Iag5hnyKkZA==",
+			"version": "2.9.7",
+			"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.9.7.tgz",
+			"integrity": "sha512-6vjZh5SXBncLhUwJGTVKS5oDljfgGMh6J4zVTeAZK3YdMUN76FgpvHkwwFXocpJpMbii6rDYU3sgie64FyPerQ==",
 			"dependencies": {
 				"@ctrl/tinycolor": "^3.4.1",
 				"@element-plus/icons-vue": "^2.3.1",
@@ -4420,6 +4422,31 @@
 			"integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==",
 			"dev": true
 		},
+		"node_modules/es-object-atoms": {
+			"version": "1.1.1",
+			"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+			"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+			"dependencies": {
+				"es-errors": "^1.3.0"
+			},
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
+		"node_modules/es-set-tostringtag": {
+			"version": "2.1.0",
+			"resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+			"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+			"dependencies": {
+				"es-errors": "^1.3.0",
+				"get-intrinsic": "^1.2.6",
+				"has-tostringtag": "^1.0.2",
+				"hasown": "^2.0.2"
+			},
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
 		"node_modules/es5-ext": {
 			"version": "0.10.64",
 			"resolved": "https://registry.npmmirror.com/es5-ext/-/es5-ext-0.10.64.tgz",
@@ -5149,18 +5176,20 @@
 			}
 		},
 		"node_modules/get-intrinsic": {
-			"version": "1.2.5",
-			"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.2.5.tgz",
-			"integrity": "sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==",
+			"version": "1.3.0",
+			"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+			"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
 			"dependencies": {
-				"call-bind-apply-helpers": "^1.0.0",
-				"dunder-proto": "^1.0.0",
+				"call-bind-apply-helpers": "^1.0.2",
 				"es-define-property": "^1.0.1",
 				"es-errors": "^1.3.0",
+				"es-object-atoms": "^1.1.1",
 				"function-bind": "^1.1.2",
+				"get-proto": "^1.0.1",
 				"gopd": "^1.2.0",
 				"has-symbols": "^1.1.0",
-				"hasown": "^2.0.2"
+				"hasown": "^2.0.2",
+				"math-intrinsics": "^1.1.0"
 			},
 			"engines": {
 				"node": ">= 0.4"
@@ -5177,6 +5206,18 @@
 				"node": ">=8.0.0"
 			}
 		},
+		"node_modules/get-proto": {
+			"version": "1.0.1",
+			"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+			"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+			"dependencies": {
+				"dunder-proto": "^1.0.1",
+				"es-object-atoms": "^1.0.0"
+			},
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
 		"node_modules/get-stream": {
 			"version": "6.0.1",
 			"resolved": "https://registry.npmmirror.com/get-stream/-/get-stream-6.0.1.tgz",
@@ -5192,6 +5233,7 @@
 			"version": "7.2.3",
 			"resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz",
 			"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+			"deprecated": "Glob versions prior to v9 are no longer supported",
 			"dependencies": {
 				"fs.realpath": "^1.0.0",
 				"inflight": "^1.0.4",
@@ -5306,6 +5348,20 @@
 				"url": "https://github.com/sponsors/ljharb"
 			}
 		},
+		"node_modules/has-tostringtag": {
+			"version": "1.0.2",
+			"resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+			"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+			"dependencies": {
+				"has-symbols": "^1.0.3"
+			},
+			"engines": {
+				"node": ">= 0.4"
+			},
+			"funding": {
+				"url": "https://github.com/sponsors/ljharb"
+			}
+		},
 		"node_modules/hasown": {
 			"version": "2.0.2",
 			"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
@@ -5522,6 +5578,7 @@
 			"version": "1.0.6",
 			"resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
 			"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+			"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
 			"dependencies": {
 				"once": "^1.3.0",
 				"wrappy": "1"
@@ -5538,9 +5595,9 @@
 			"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
 		},
 		"node_modules/is-core-module": {
-			"version": "2.15.1",
-			"resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.15.1.tgz",
-			"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
+			"version": "2.16.1",
+			"resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz",
+			"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
 			"dependencies": {
 				"hasown": "^2.0.2"
 			},
@@ -6386,13 +6443,14 @@
 			}
 		},
 		"node_modules/jsdom/node_modules/form-data": {
-			"version": "3.0.2",
-			"resolved": "https://registry.npmmirror.com/form-data/-/form-data-3.0.2.tgz",
-			"integrity": "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==",
+			"version": "3.0.3",
+			"resolved": "https://registry.npmmirror.com/form-data/-/form-data-3.0.3.tgz",
+			"integrity": "sha512-q5YBMeWy6E2Un0nMGWMgI65MAKtaylxfNJGJxpGh45YDciZB4epbWpaAfImil6CPAPTYB4sh0URQNDRIZG5F2w==",
 			"dependencies": {
 				"asynckit": "^0.4.0",
 				"combined-stream": "^1.0.8",
-				"mime-types": "^2.1.12"
+				"es-set-tostringtag": "^2.1.0",
+				"mime-types": "^2.1.35"
 			},
 			"engines": {
 				"node": ">= 6"
@@ -6677,6 +6735,14 @@
 				"tmpl": "1.0.5"
 			}
 		},
+		"node_modules/math-intrinsics": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+			"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+			"engines": {
+				"node": ">= 0.4"
+			}
+		},
 		"node_modules/memoize-one": {
 			"version": "6.0.0",
 			"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
@@ -6908,9 +6974,9 @@
 			}
 		},
 		"node_modules/nwsapi": {
-			"version": "2.2.16",
-			"resolved": "https://registry.npmmirror.com/nwsapi/-/nwsapi-2.2.16.tgz",
-			"integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ=="
+			"version": "2.2.20",
+			"resolved": "https://registry.npmmirror.com/nwsapi/-/nwsapi-2.2.20.tgz",
+			"integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA=="
 		},
 		"node_modules/object-inspect": {
 			"version": "1.13.3",
@@ -7172,9 +7238,9 @@
 			}
 		},
 		"node_modules/pirates": {
-			"version": "4.0.6",
-			"resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.6.tgz",
-			"integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==",
+			"version": "4.0.7",
+			"resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz",
+			"integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==",
 			"engines": {
 				"node": ">= 6"
 			}
@@ -7525,17 +7591,20 @@
 			"integrity": "sha512-R/tCuvuOHQ8o2boRP6vgx8hXCCy87H1eY9V5imBYeVNyNVpuL9ciReSccLj2gDcax9+2weXy3bc8Vv+NRXeEvQ=="
 		},
 		"node_modules/resolve": {
-			"version": "1.22.8",
-			"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.8.tgz",
-			"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
+			"version": "1.22.10",
+			"resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz",
+			"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
 			"dependencies": {
-				"is-core-module": "^2.13.0",
+				"is-core-module": "^2.16.0",
 				"path-parse": "^1.0.7",
 				"supports-preserve-symlinks-flag": "^1.0.0"
 			},
 			"bin": {
 				"resolve": "bin/resolve"
 			},
+			"engines": {
+				"node": ">= 0.4"
+			},
 			"funding": {
 				"url": "https://github.com/sponsors/ljharb"
 			}
@@ -7599,6 +7668,7 @@
 			"version": "3.0.2",
 			"resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz",
 			"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+			"deprecated": "Rimraf versions prior to v4 are no longer supported",
 			"dependencies": {
 				"glob": "^7.1.3"
 			},
@@ -8569,9 +8639,9 @@
 			}
 		},
 		"node_modules/vite": {
-			"version": "4.5.5",
-			"resolved": "https://registry.npmmirror.com/vite/-/vite-4.5.5.tgz",
-			"integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==",
+			"version": "4.5.10",
+			"resolved": "https://registry.npmmirror.com/vite/-/vite-4.5.10.tgz",
+			"integrity": "sha512-f2ueoukYTMI/5kMMT7wW+ol3zL6z6PjN28zYrGKAjnbzXhRXWXPThD3uN6muCp+TbfXaDgGvRuPsg6mwVLaWwQ==",
 			"dev": true,
 			"dependencies": {
 				"esbuild": "^0.18.10",
@@ -8891,6 +8961,7 @@
 			"version": "1.0.2",
 			"resolved": "https://registry.npmmirror.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
 			"integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==",
+			"deprecated": "Use your platform's native performance.now() and performance.timeOrigin.",
 			"dependencies": {
 				"browser-process-hrtime": "^1.0.0"
 			}

+ 106 - 0
src/api/examTrain/practice.ts

@@ -0,0 +1,106 @@
+/*
+ * @Author: zjq
+ * @description 自我练习列表
+ */
+import request from '@/utils/request';
+
+/**
+ * @description 获取自我练习列表
+ * @param {object} params
+ */
+export const getPracticeData = (params?: object) => {
+    return request({
+        url: '/api/v1/Practice/GetPagedList',
+        method: 'post',
+        data: params,
+    });
+};
+
+/**
+ * @description 新增练习-生成练习ID
+ * @param {object} data
+ */
+export const addPractice = (data: object) => {
+    return request({
+        url: '/api/v1/Practice/Add',
+        method: 'post',
+        data,
+    });
+};
+
+/**
+ * @description 获取练习试题类型分组数据
+ * @param {any} Id
+ */
+export const getPracticeQuestionGroup = (Id: any) => {
+    return request({
+        url: `/api/v1/Practice/GetPracticeQuestionViewResponses?PracticeId=${Id}`,
+        method: 'get',
+    });
+};
+/**
+ * @description 获取练习试题详情
+ * @param {any} Id
+ */
+export const getPracticeQuestion = (Id: any,) => {
+    return request({
+        url: `/api/v1/Practice/GetPracticeQuestionDto?PracticeQuestionId=${Id}`,
+        method: 'get',
+    });
+};
+/**
+ * @description 提交练习答案
+ * @param {object} params
+ */
+export const answerUserPractice = (params?: object) => {
+    return request({
+        url: '/api/v1/Practice/Practice',
+        method: 'post',
+        data: params,
+    });
+};
+
+/**
+ * @description 完成练习
+ * @param {object} params
+ */
+export const submitUserPractice = (params?: object) => {
+    return request({
+        url: '/api/v1/Practice/Complete',
+        method: 'post',
+        data: params,
+    });
+};
+
+/**
+ * @description 满足条件试题数量
+ * @param {object} params
+ */
+export const getTagQuestionCount = (params: object) => {
+    return request({
+        url: `/api/v1/Practice/GetTagQuestionCount`,
+        method: 'post',
+        data: params,
+    });
+};
+
+/**
+ * @description 获取练习试题类型分组数据-查看
+ * @param {any} Id
+ */
+export const getViewPracticeQuestions = (Id: any) => {
+    return request({
+        url: `/api/v1/Practice/GetViewPracticeQuestions?PracticeId=${Id}`,
+        method: 'get',
+    });
+};
+/**
+ * @description 获取练习试题详情-查看
+ * @param {any} Id
+ */
+export const getPracticeView = (Id: any,) => {
+    return request({
+        url: `/api/v1/Practice/View?PracticeQuestionId=${Id}`,
+        method: 'get',
+    });
+};

+ 136 - 0
src/views/examTrain/exam/practice/components/Practice-add.vue

@@ -0,0 +1,136 @@
+<template>
+	<el-dialog title="新建练习" v-model="state.dialogVisible" draggable append-to-body destroy-on-close @close="close" width="500px">
+		<el-form :model="state.ruleForm" ref="ruleFormRef" label-width="80px" v-loading="state.loading">
+			<el-row :gutter="10">
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+					<el-form-item label="试题标签" prop="questionTagIds" :rules="[{ required: true, message: '请选择试题标签', trigger: 'change' }]">
+                        <el-tree-select
+                            v-model="state.ruleForm.questionTagIds"
+                            :data="state.tagData"
+                            node-key="id"
+                            :props="{ label: 'name' }"
+                            filterable
+                            multiple
+                            collapse-tags
+                            collapse-tags-tooltip
+                            :max-collapse-tags="2"
+                            :render-after-expand="false"
+                            style="width: 100%"
+                            ref="treeSelectRef"
+                            @change="selQuestionTag"
+                        />
+                    </el-form-item>
+				</el-col>
+				<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
+                    <el-form-item label="出题数量" prop="count" :rules="[{ required: true, message: '请填写出题数量', trigger: 'blur' }]">
+                        <el-input-number v-model="state.ruleForm.count" :min="0" :max="state.totalCount" class="w100" >
+                            <template #suffix>
+                                <span>共{{state.totalCount}}题</span>
+                            </template>
+                        </el-input-number>
+                    </el-form-item>
+				</el-col>
+			</el-row>
+		</el-form>
+		<template #footer>
+			<span class="dialog-footer">
+				<el-button @click="closeDialog" class="default-button">取 消</el-button>
+				<el-button type="primary" @click="onSubmit(ruleFormRef)" :loading="state.loading">确 定 </el-button>
+			</span>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue';
+import { ElMessage, FormInstance } from 'element-plus';
+import other from '@/utils/other';
+import { examTagTreeList } from '@/api/examTrain/tag';
+import { addPractice, getTagQuestionCount } from '@/api/examTrain/practice';
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['choose']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	ruleForm: {
+		questionTagIds: [], // 选中的标签id
+		practiceTagDtos: [], // 试题标签
+		count: 0, // 出题数量
+	},
+	tagData: [], // 试题标签数据
+    totalCount: 0, // 满足条件试题总数量
+	loading: false, // 加载
+});
+// 打开弹窗
+const ruleFormRef = ref<any>(); // 表单ref
+const openDialog = () => {
+	state.dialogVisible = true;
+	getTagData();
+};
+// 获取标签数据
+const getTagData = async () => {
+	state.loading = true;
+	try {
+		const { result } = await examTagTreeList();
+		state.tagData = result ?? [];
+		state.loading = false;
+	} catch (error) {
+		state.loading = false;
+	}
+};
+const close = () => {
+	ruleFormRef.value?.clearValidate();
+	ruleFormRef.value?.resetFields();
+    state.totalCount = 0;
+};
+// 关闭弹窗
+const closeDialog = () => {
+	state.dialogVisible = false;
+    state.totalCount = 0;
+};
+// 选择试题标签
+const treeSelectRef = ref<RefType>();
+const selQuestionTag = async (value: any) => {
+	state.ruleForm.practiceTagDtos = [];
+	state.ruleForm.questionTagIds.forEach(item => {
+		let tagObj = other.deepClone(treeSelectRef.value.getNode(item).data);
+		state.ruleForm.practiceTagDtos.push({tagId: tagObj.id});
+	})
+	try {
+		const { result } = await getTagQuestionCount({
+			tagIds: state.ruleForm.questionTagIds
+		});
+		state.totalCount = result.totalCount ?? 0;
+        if (state.ruleForm.count > state.totalCount){
+            state.ruleForm.count = 0;
+        }
+	} catch (error) {
+	}
+}
+// 新增
+const onSubmit = async (formEl: FormInstance | undefined) => {
+	if (!formEl) return;
+	await formEl.validate((valid: boolean) => {
+		if (!valid) return;
+		state.loading = true;
+		addPractice(state.ruleForm)
+			.then((res) => {
+				console.log(res);
+				emit('choose', res.result);
+				closeDialog(); // 关闭弹窗
+				ElMessage.success('操作成功');
+				state.loading = false;
+			})
+			.catch(() => {
+				state.loading = false;
+			});
+	});
+};
+// 暴露变量
+defineExpose({
+	openDialog,
+	closeDialog,
+});
+</script>

+ 233 - 0
src/views/examTrain/exam/practice/components/Practice-exam.vue

@@ -0,0 +1,233 @@
+<template>
+	<el-dialog v-model="state.dialogVisible" draggable append-to-body destroy-on-close :show-close="false" width="80%" :before-close="closeDialog">
+        <template #header="{ close, titleId, titleClass }">
+            <div class="topContent">
+                <div class="titleBox">
+                    <span class="title">自我练习</span>
+                </div>
+                <div class="submitBox">
+				    <el-button type="info" @click="closeDialog">关 闭</el-button>
+                    <el-button type="primary" @click="onSubmit" :loading="state.loading">完成练习</el-button>
+                </div>
+            </div>
+        </template>
+        <el-row :gutter="10" v-loading="state.loading">
+            <el-col :xs="6" :sm="6" :md="6" :lg="6" :xl="6">
+                <div class="examListContent" v-for="(item) in state.questionTypeData">
+                    <p class="examType">{{item.questionTypeDesc}}</p>
+                    <ul class="examList">
+                        <li class="examListItem" :class="questionsItem.id == state.selQuestionID ? 'select' : ''" v-for="(questionsItem) in item.questions" @click="onSelQuestion(questionsItem.id)">{{questionsItem.sortIndex}}</li>
+                    </ul>
+                </div>
+            </el-col>
+            <el-col :xs="18" :sm="18" :md="18" :lg="18" :xl="18">
+                <div class="questionBox" v-if="state.questionDetail">
+                    <p class="questionTitle">题干:{{ state.questionDetail.title }}</p>
+                    <div class="questionAnswer">
+                        <div v-if="[0,2].includes(state.questionDetail.questionType)">
+							<el-radio-group style="display: block;" v-model="state.selRadioValue">
+                                <el-form-item :label="item.label + '. '" v-for="item in state.questionDetail.practiceQuestionOptionsDtos" style="margin-bottom: 10px;">
+                                    <el-radio :label="item.content" :value="item.questionOptionId" />
+                                </el-form-item>
+                            </el-radio-group>
+                        </div>
+                        <div v-else-if="state.questionDetail.questionType == 1">
+                            <el-checkbox-group v-model="state.selCheckboxValue">
+                                <el-form-item :label="item.label + '. '" v-for="item in state.questionDetail.practiceQuestionOptionsDtos" style="margin-bottom: 10px;">
+                                    <el-checkbox :label="item.content" :value="item.questionOptionId" />
+                                </el-form-item>
+                            </el-checkbox-group>
+                        </div>
+                        <!-- <div v-else-if="state.questionDetail.questionType == 2"></div> -->
+                        <!-- <div v-else-if="state.questionDetail.questionType == 3">
+                            <el-form-item label="答:">
+                                <el-input type="textarea" v-model="state.answerValue" :rows="3" placeholder="请输入答案,如果有多个请依照顺序用英文 , 隔开" clearable></el-input>
+                            </el-form-item>
+                        </div>
+                        <div v-else-if="state.questionDetail.questionType == 4">
+                            <el-form-item label="答:">
+                                <el-input type="textarea" v-model="state.answerValue" :rows="3" placeholder="请输入答案" clearable></el-input>
+                            </el-form-item>
+                        </div> -->
+                    </div>
+                </div>
+            </el-col>
+        </el-row>
+		<template #footer>
+			<span class="dialog-footer" v-if="state.selQuestionID">
+				<el-button type="primary" @click="onLastQuestion" :loading="state.loading">上一题</el-button>
+				<el-button type="primary" @click="onNextQuestion" :loading="state.loading">下一题</el-button>
+			</span>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue';
+import { ElMessage, FormInstance } from 'element-plus';
+import { excludeSelfById } from '@/utils/tools';
+import other from '@/utils/other';
+import {getPracticeQuestionGroup, getPracticeQuestion, answerUserPractice, submitUserPractice } from '@/api/examTrain/practice';
+
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['updateList']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	loading: false, // 加载
+    practiceId: '', // 用户练习id
+	questionTypeData: [] as any[], // 试卷题型数据
+    questionsData: [] as any[], // 试题集合
+    selQuestionID: '', // 选择的问题id
+    questionDetail: null, // 选择的问题明细
+    selRadioValue: '', // 单选题选择的值
+    selCheckboxValue: [] as any[], // 多选题选择的值
+});
+// 打开弹窗
+const openDialog = async (practiceId: any) => {
+    state.dialogVisible = true;
+    state.practiceId = practiceId;
+	state.loading = true;
+    getQuestionsData();
+};
+const getQuestionsData = async () => {
+    try {
+		const { result } = await getPracticeQuestionGroup(state.practiceId);
+        let sortIndexNum = 1;
+        result.forEach(item => {
+            switch(item.questionType){
+				case 0: item.questionTypeDesc = '单选题'; break;
+				case 1: item.questionTypeDesc = '多选题'; break;
+				case 2: item.questionTypeDesc = '判断题'; break;
+				default: break;
+			}
+            item.questions.forEach(it => {
+                it.sortIndex = sortIndexNum;
+                it.questionType = item.questionType;
+                state.questionsData.push(it)
+                sortIndexNum++ ;
+            });
+        })
+		state.questionTypeData = result;
+        state.loading = false;
+	} catch (error) {
+		// 打印错误信息
+		console.error(error);
+	}
+}
+// 关闭弹窗
+const closeDialog = () => {
+	state.dialogVisible = false;
+    state.questionTypeData = []; 
+    state.questionsData = []; 
+    state.selQuestionID = ''; 
+    state.questionDetail = null; 
+    state.selRadioValue = ''; 
+    state.selCheckboxValue = []; 
+};
+// 上一题下一题
+const onLastQuestion = () => {
+    let index:number = state.questionsData.findIndex(it => it.id == state.selQuestionID);
+    index = (index - 1) == -1 ? state.questionsData.length : index - 1;
+    onSelQuestion(state.questionsData[index].id);
+}
+const onNextQuestion = () => {
+    let index:number = state.questionsData.findIndex(it => it.id == state.selQuestionID);
+    index = (index + 1) == state.questionsData.length ? 0 : index + 1;
+    onSelQuestion(state.questionsData[index].id);
+}
+// 选择的问题id 提交当前试题答案,再请求下一道题详情 value: 请求下一题详情id
+const onSelQuestion = async (value: any) => {
+	state.loading = true;
+    if (state.questionDetail){
+        // 保存当前题目选项
+        const selQuestionItem = state.questionsData.find(it => it.id == state.selQuestionID);
+        const submitObj = {
+            practiceQuestionId: selQuestionItem.id,
+            practiceResultItemDtos: [] as any[],
+        };
+        if ([0,2].includes(state.questionDetail.questionType)){
+            if (state.selRadioValue){
+                submitObj.practiceResultItemDtos.push({questionOptionId: state.selRadioValue})
+            }
+        }else if (state.questionDetail.questionType == 1){
+            if (state.selCheckboxValue.length > 0){
+                state.selCheckboxValue.forEach(it => {
+                    submitObj.practiceResultItemDtos.push({questionOptionId: it})
+                })
+            }
+        }
+        answerUserPractice(submitObj)
+            .then((res) => {
+                onGetQuestionDetail(value);
+            })
+            .catch(() => {
+                state.loading = false;
+            });
+    }else {
+        onGetQuestionDetail(value);
+    }
+}
+const onGetQuestionDetail = async (value) => {
+    // 初始化选择选择/填写的答案
+    state.selRadioValue = '';  
+    state.selCheckboxValue = []; 
+    // 请求下一道题
+    state.selQuestionID = value;
+    try {
+		const { result } = await getPracticeQuestion(state.selQuestionID);
+		state.questionDetail = result;
+        if ([0,2].includes(state.questionDetail.questionType)){
+            let obj = state.questionDetail.practiceQuestionOptionsDtos.find((x: any) => x.isSelected === true);
+            state.selRadioValue = obj ? obj.questionOptionId : '';
+        }else if (state.questionDetail.questionType == 1){
+            let arr = [] as any[];
+            state.questionDetail.practiceQuestionOptionsDtos.forEach(it => {if (it.isSelected) arr.push(it);})
+            state.selCheckboxValue = arr ? arr.map((x: any) => x.questionOptionId) : [];
+            console.log(state.selCheckboxValue)
+        }
+        state.loading = false;
+	} catch (error) {
+		// 打印错误信息
+		console.error(error);
+	}
+}
+// 结束考试
+const onSubmit = async () => {
+    state.loading = true;
+	submitUserPractice({
+        practiceId: state.practiceId
+    }).then(() => {
+        emit('updateList');
+        state.loading = false;
+        closeDialog(); // 关闭弹窗
+        ElMessage.success('操作成功');
+    }).catch(() => {
+        state.loading = false;
+    });
+};
+// 暴露变量
+defineExpose({
+	openDialog,
+	closeDialog,
+});
+</script>
+<style lang="scss">
+    .topContent{padding: 10px 30px;display: flex;justify-content: space-between;align-items: center;}
+    .topContent .surplusBox{text-align: center;}
+    .topContent .surplusBox .surplus{font-size: 18px;color: #000000;}
+    .topContent .titleBox{text-align: center;}
+    .topContent .titleBox .title{font-size: 20px;font-weight: bold;letter-spacing: 1px;color: #000000;}
+    .topContent .submitBox{text-align: right;vertical-align: top;}
+    .examListContent .examType{font-weight: bold;color: #000000;}
+    .examListContent .examList{padding: 5px 0 10px;}
+    .examListContent .examList .examListItem{display: inline-block;margin: 5px 10px 5px 0;text-align: center;width: 50px;height:35px;line-height: 35px;cursor: pointer;position: relative;border: #f2f2f2 1px solid;}
+    .examListContent .examList .marked::after{content: "*";width: 5px;height: 5px;position: absolute;top: -3px;right: 4px;color: #FF5722;font-size: 25px;}
+    .examListContent .examList .select{background-color: #1890ff;color: #fff;}
+    .questionBox{padding: 20px 10px 0;}
+    .questionBox .questionTitle{margin-bottom: 10px;font-size: 16px;}
+    .questionBox .questionAnswer{padding: 0 20px;}
+    .questionBox .questionAnswer .answerItem{width:calc(100%);}
+    .questionBox .questionAnswer .answerItem .optionIndex{vertical-align: sub;font-size: 16px;display: inline-block;}
+</style>

+ 238 - 0
src/views/examTrain/exam/practice/components/Practice-view.vue

@@ -0,0 +1,238 @@
+<template>
+	<el-dialog v-model="state.dialogVisible" draggable append-to-body destroy-on-close :show-close="false" width="80%" :before-close="closeDialog">
+        <template #header="{ close, titleId, titleClass }">
+            <div class="topContent">
+                <div class="titleBox">
+                    <span class="title">自我练习</span>
+                </div>
+                <div class="submitBox">
+				    <el-button type="info" @click="closeDialog">关 闭</el-button>
+                </div>
+            </div>
+        </template>
+        <el-row :gutter="10" v-loading="state.loading">
+            <el-col :xs="6" :sm="6" :md="6" :lg="6" :xl="6">
+                <div class="examListContent" v-for="(item) in state.questionTypeData">
+                    <p class="examType">{{item.questionTypeDesc}}</p>
+                    <ul class="examList">
+                        <li class="examListItem" :class="{'select': questionsItem.id == state.selQuestionID, 'right': questionsItem.isRight, 'error': !questionsItem.isRight}" v-for="(questionsItem) in item.questions" @click="onSelQuestion(questionsItem.id)">{{questionsItem.sortIndex}}</li>
+                    </ul>
+                </div>
+            </el-col>
+            <el-col :xs="18" :sm="18" :md="18" :lg="18" :xl="18">
+                <div class="questionBox" v-if="state.questionDetail">
+                    <p class="questionTitle">题干:{{ state.questionDetail.title }}</p>
+                    <div class="questionAnswer">
+                        <div v-if="[0,2].includes(state.questionDetail.questionType)">
+							<el-radio-group style="display: block;" v-model="state.selRadioValue">
+                                <el-form-item :label="item.label + '. '" v-for="item in state.questionDetail.practiceQuestionOptionsDtos" style="margin-bottom: 10px;">
+                                    <el-radio :label="item.content" :value="item.questionOptionId" disabled />
+                                </el-form-item>
+                            </el-radio-group>
+                        </div>
+                        <div v-else-if="state.questionDetail.questionType == 1">
+                            <el-checkbox-group v-model="state.selCheckboxValue">
+                                <el-form-item :label="item.label + '. '" v-for="item in state.questionDetail.practiceQuestionOptionsDtos" style="margin-bottom: 10px;">
+                                    <el-checkbox :label="item.content" :value="item.questionOptionId" disabled />
+                                </el-form-item>
+                            </el-checkbox-group>
+                        </div>
+                    </div>
+                </div>
+                <div class="referenceAnswer" v-if="state.questionDetail">
+                    <span>参考答案:</span>
+                    <p>{{state.questionDetail.answerDesc || '略'}}</p>
+                </div>
+                <div class="referenceAnswer" v-if="state.questionDetail" v-for="item in state.questionDetail.practiceQuestionKnowladgeDtos">
+                    <span>关联知识:</span>
+                    <p @click="onKnowladgeTo(item)">{{item.title}}</p>
+                </div>
+                <div class="referenceAnswer" v-if="state.questionDetail" v-for="item in state.questionDetail.practiceQuestionSourcewareDtos">
+                    <span>关联课件:</span>
+                    <p @click="onSourcewareTo(item)">{{item.name}}</p>
+                </div>
+            </el-col>
+        </el-row>
+		<template #footer>
+			<span class="dialog-footer" v-if="state.selQuestionID">
+				<el-button type="primary" @click="onLastQuestion" :loading="state.loading">上一题</el-button>
+				<el-button type="primary" @click="onNextQuestion" :loading="state.loading">下一题</el-button>
+			</span>
+		</template>
+	</el-dialog>
+</template>
+
+<script setup lang="ts">
+import { reactive, ref } from 'vue';
+import { useRouter } from 'vue-router';
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { excludeSelfById } from '@/utils/tools';
+import other from '@/utils/other';
+import { fileDownloadByUrl } from '@/api/public/file';
+import {getViewPracticeQuestions, getPracticeView} from '@/api/examTrain/practice';
+
+const router = useRouter(); // 路由
+// 定义子组件向父组件传值/事件
+const emit = defineEmits(['updateList']);
+
+// 定义变量内容
+const state = reactive<any>({
+	dialogVisible: false, // 弹窗
+	loading: false, // 加载
+    practiceId: '', // 用户练习id
+	questionTypeData: [] as any[], // 试卷题型数据
+    questionsData: [] as any[], // 试题集合
+    selQuestionID: '', // 选择的问题id
+    questionDetail: null, // 选择的问题明细
+    selRadioValue: '', // 单选题选择的值
+    selCheckboxValue: [] as any[], // 多选题选择的值
+});
+// 打开弹窗
+const openDialog = async (rows: any) => {
+    state.dialogVisible = true;
+    state.practiceId = rows.id;
+	state.loading = true;
+    getQuestionsData();
+};
+const getQuestionsData = async () => {
+    try {
+		const { result } = await getViewPracticeQuestions(state.practiceId);
+        let sortIndexNum = 1;
+        result.forEach(item => {
+            switch(item.questionType){
+				case 0: item.questionTypeDesc = '单选题'; break;
+				case 1: item.questionTypeDesc = '多选题'; break;
+				case 2: item.questionTypeDesc = '判断题'; break;
+				default: break;
+			}
+            item.questions.forEach(it => {
+                it.sortIndex = sortIndexNum;
+                it.questionType = item.questionType;
+                state.questionsData.push(it)
+                sortIndexNum++ ;
+            });
+        })
+		state.questionTypeData = result;
+        state.loading = false;
+	} catch (error) {
+		// 打印错误信息
+		console.error(error);
+	}
+}
+// 关闭弹窗
+const closeDialog = () => {
+	state.dialogVisible = false;
+    state.questionTypeData = []; 
+    state.questionsData = []; 
+    state.selQuestionID = ''; 
+    state.questionDetail = null; 
+    state.selRadioValue = ''; 
+    state.selCheckboxValue = []; 
+};
+// 上一题下一题
+const onLastQuestion = () => {
+    let index:number = state.questionsData.findIndex(it => it.id == state.selQuestionID);
+    index = (index - 1) == -1 ? state.questionsData.length : index - 1;
+    onSelQuestion(state.questionsData[index].id);
+}
+const onNextQuestion = () => {
+    let index:number = state.questionsData.findIndex(it => it.id == state.selQuestionID);
+    index = (index + 1) == state.questionsData.length ? 0 : index + 1;
+    onSelQuestion(state.questionsData[index].id);
+}
+// 选择的问题id 提交当前试题答案,再请求下一道题详情 value: 请求下一题详情id
+const onSelQuestion = async (value: any) => {
+	state.loading = true;
+    onGetQuestionDetail(value);
+}
+const onGetQuestionDetail = async (value) => {
+    // 初始化选择选择/填写的答案
+    state.selRadioValue = '';  
+    state.selCheckboxValue = []; 
+    // 请求下一道题
+    state.selQuestionID = value;
+    try {
+		const { result } = await getPracticeView(state.selQuestionID);
+		state.questionDetail = result;
+        let answerDesc = '';
+        state.questionDetail.practiceQuestionOptionsDtos.forEach(it => {it.isAnswer && (answerDesc += it.label)});
+        state.questionDetail.answerDesc = answerDesc;
+        if ([0,2].includes(state.questionDetail.questionType)){
+            let obj = state.questionDetail.practiceQuestionOptionsDtos.find((x: any) => x.isSelected === true);
+            state.selRadioValue = obj ? obj.questionOptionId : '';
+        }else if (state.questionDetail.questionType == 1){
+            let arr = [] as any[];
+            state.questionDetail.practiceQuestionOptionsDtos.forEach(it => {if (it.isSelected) arr.push(it);})
+            state.selCheckboxValue = arr ? arr.map((x: any) => x.questionOptionId) : [];
+        }
+        state.loading = false;
+	} catch (error) {
+		// 打印错误信息
+		console.error(error);
+	}
+}
+// 跳转知识详情页面
+const onKnowladgeTo = (row: any) => {
+	router.push({
+		name: 'knowledgePreview',
+		params: {
+			id: row.knowladgeId,
+			isAddPv: 'isAddPv',
+			tagsViewName: row.title,
+		},
+	});
+};
+// 课件预览下载
+const onSourcewareTo = (row: any) => {
+    ElMessageBox.confirm(`您确定要下载课件,是否继续?`, '提示', {
+		confirmButtonText: '确认',
+		cancelButtonText: '取消',
+		type: 'warning',
+		draggable: true,
+		cancelButtonClass: 'default-button',
+		autofocus: false,
+	})
+		.then(() => {
+			fileDownloadByUrl({
+				Source: 'hotline',
+				Id: row.attachmentId,
+			}).then((res: any) => {
+				let blob: Blob = new Blob([res.data], { type: res.data.type }); // 创建blob 设置blob文件类型 data 设置为后端返回的文件(例如mp3,jpeg) type:这里设置后端返回的类型 为 mp3
+				let down: HTMLAnchorElement = document.createElement('a'); // 创建A标签
+				let href: string = window.URL.createObjectURL(blob); // 创建下载的链接
+				down.href = href; // 下载地址
+				down.download = row.name // 下载文件名
+				document.body.appendChild(down);
+				down.click(); // 模拟点击A标签
+				document.body.removeChild(down); // 下载完成移除元素
+				window.URL.revokeObjectURL(href); // 释放blob对象
+			});
+		})
+		.catch(() => {});
+}
+// 暴露变量
+defineExpose({
+	openDialog,
+	closeDialog,
+});
+</script>
+<style lang="scss">
+    .topContent{padding: 10px 30px;display: flex;justify-content: space-between;align-items: center;}
+    .topContent .surplusBox{text-align: center;}
+    .topContent .surplusBox .surplus{font-size: 18px;color: #000000;}
+    .topContent .titleBox{text-align: center;}
+    .topContent .titleBox .title{font-size: 20px;font-weight: bold;letter-spacing: 1px;color: #000000;}
+    .topContent .submitBox{text-align: right;vertical-align: top;}
+    .examListContent .examType{font-weight: bold;color: #000000;}
+    .examListContent .examList{padding: 5px 0 10px;}
+    .examListContent .examList .examListItem{display: inline-block;margin: 5px 10px 5px 0;text-align: center;width: 50px;height:35px;line-height: 35px;cursor: pointer;position: relative;border: #f2f2f2 1px solid;}
+    .examListContent .examList .right::after{content: " ";width: 5px;height: 5px;position: absolute;top: 2px;right: 3px;background-color: #009688;border-radius: 50%;}
+    .examListContent .examList .error::after{content: " ";width: 5px;height: 5px;position: absolute;top: 2px;right: 3px;background-color: #FF5722;border-radius: 50%;}
+    .examListContent .examList .select{background-color: #1890ff;color: #fff;}
+    .questionBox{padding: 20px 10px 0;}
+    .questionBox .questionTitle{margin-bottom: 10px;font-size: 16px;}
+    .questionBox .questionAnswer{padding: 0 20px;}
+    .questionBox .referenceAnswer{background-color: #ebf9ff;margin: 15px 40px 15px 30px;padding: 10px;}
+    .questionBox .referenceAnswer span{vertical-align: top;display: inline-block;}
+    .questionBox .referenceAnswer p{width: calc(100% - 80px);display: inline-block;}
+</style>

+ 138 - 0
src/views/examTrain/exam/practice/index.vue

@@ -0,0 +1,138 @@
+<template>
+	<div class="plan-index-container layout-padding">
+		<div class="layout-padding-auto layout-padding-view pd20">
+            <vxe-toolbar
+                ref="toolbarRef"
+                :loading="state.tableLoading"
+                custom
+                :refresh="{
+                    queryMethod: handleQuery,
+                }"
+            >
+                <template #buttons>
+                    <el-button type="primary" @click="onAdd" v-auth="'practice:index:add'" :loading="state.tableLoading">
+                        <SvgIcon name="ele-Plus" class="mr10" />新增练习
+                    </el-button>
+                </template>
+            </vxe-toolbar>
+            <div style="overflow: hidden; width: 100%; height: 100%; flex: 1">
+                <vxe-table
+                    border
+                    :loading="state.tableLoading"
+                    :data="state.tableData"
+                    :column-config="{ resizable: true }"
+                    :row-config="{ isCurrent: true, isHover: true, height: 30, useKey: true }"
+                    ref="tableRef"
+                    height="auto"
+                    auto-resize
+                    show-overflow
+                    :print-config="{}"
+                    :scrollY="{ enabled: true, gt: 100 }"
+                    id="practice"
+                    :custom-config="{ storage: true }"
+                    showHeaderOverflow
+                >
+                    <vxe-column field="code" title="练习流水号" width="200"></vxe-column>
+                    <vxe-column field="count" title="出题数量" width="150"></vxe-column>
+                    <vxe-column field="practiceTime" title="练习时间" min-width="160">
+						<template #default="{ row }">
+							{{ formatDate(row.practiceTime, 'YYYY-mm-dd HH:MM:SS') }}
+						</template>
+					</vxe-column>
+                    <vxe-column title="操作" fixed="right" width="120" align="center" :show-overflow="false">
+                        <template #default="{ row }">
+                            <el-button link type="primary" @click="onView(row)" title="查看" v-auth="'practice:index:view'">
+                                查看
+                            </el-button>
+                        </template>
+                    </vxe-column>
+                </vxe-table>
+            </div>
+            <pagination
+                @pagination="queryList"
+                :total="state.total"
+                v-model:current-page="state.queryParams.PageIndex"
+                v-model:page-size="state.queryParams.PageSize"
+                :disabled="state.tableLoading"
+            />
+		</div>
+        <practice-add ref="practiceAddRef" @choose="onOpenExam" />
+		<practice-exam ref="practiceExamRef" @updateList="queryList" />
+		<practice-view ref="practiceViewRef" />
+	</div>
+</template>
+
+<script lang="tsx" setup name="practice">
+import { ref, reactive, onMounted, defineAsyncComponent, computed } from 'vue';
+import { ElMessageBox, ElMessage } from 'element-plus';
+import { useRouter } from 'vue-router';
+import type { FormInstance } from 'element-plus';
+import { formatDate } from '@/utils/formatTime';
+import 'splitpanes/dist/splitpanes.css';
+import Other from '@/utils/other';
+import { downloadFileByStream } from '@/utils/tools';
+import {getPracticeData} from '@/api/examTrain/practice';
+
+// 引入组件
+const pagination = defineAsyncComponent(() => import('@/components/ProTable/components/Pagination.vue')); // 分页
+const PracticeAdd = defineAsyncComponent(() => import('@/views/examTrain/exam/practice/components/Practice-add.vue')); // 新增练习
+const PracticeExam = defineAsyncComponent(() => import('@/views/examTrain/exam/practice/components/Practice-exam.vue')); // 练习
+const PracticeView = defineAsyncComponent(() => import('@/views/examTrain/exam/practice/components/Practice-view.vue')); // 查看
+
+const router = useRouter(); //路由
+
+// 定义变量内容
+const state = reactive<any>({
+	queryParams: {
+		PageIndex: 1, //页码
+		PageSize: 20, //每页条数
+	},
+	tableLoading: false, //表格loading
+	tableData: [], //表格数据
+	total: 0, //总条数
+});
+/** 搜索按钮操作 节流操作 */
+const handleQuery = () => {
+	state.queryParams.PageIndex = 1;
+	queryList();
+};
+/** 获取试题列表 */
+const requestParams = ref<EmptyObjectType>({});
+const queryList = () => {
+	state.tableLoading = true;
+	requestParams.value = Other.deepClone(state.queryParams);
+	getPracticeData(requestParams.value)
+		.then((response: any) => {
+			state.tableData = response?.result.items ?? [];
+			state.total = response?.result.pagination.totalCount;
+			state.tableLoading = false;
+		})
+		.catch(() => {
+			state.tableLoading = false;
+		});
+};
+// 打开新增弹窗
+const practiceAddRef = ref<RefType>(); // 新增ref
+const onAdd = () => {
+	practiceAddRef.value.openDialog(state.tableData.value);
+};
+const practiceExamRef = ref<RefType>(); // 练习ref
+const onOpenExam = (practiceId) => {
+    console.log(practiceId);
+	practiceExamRef.value.openDialog(practiceId);
+}
+// 打开查看弹窗
+const practiceViewRef = ref<RefType>(); // 修改ref
+const onView = (row: any) => {
+	practiceViewRef.value.openDialog(row);
+};
+// 表格选中状态
+const tableRef = ref<RefType>();
+const toolbarRef = ref<RefType>();
+onMounted(() => {
+	queryList();
+	if (tableRef.value && toolbarRef.value) {
+		tableRef.value.connect(toolbarRef.value);
+	}
+});
+</script>

+ 8 - 0
src/views/examTrain/exam/userExam/index.vue

@@ -19,6 +19,14 @@
                     <el-button @click="drawer = true" class="default-button"> <SvgIcon name="ele-Search" class="mr5" />更多查询</el-button>
                 </el-form-item>
             </el-form>
+            <vxe-toolbar
+                ref="toolbarRef"
+                :loading="state.tableLoading"
+                custom
+                :refresh="{
+                    queryMethod: handleQuery,
+                }"
+            ></vxe-toolbar>
             <div style="overflow: hidden; width: 100%; height: 100%; flex: 1">
                 <vxe-table
                     border

Fichier diff supprimé car celui-ci est trop grand
+ 169 - 255
yarn.lock


Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff