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

chore: update

上级 1d99e640
......@@ -10,13 +10,16 @@
"dependencies": {
"@element-plus/icons-vue": "^2.0.6",
"@tinymce/tinymce-vue": "^5.0.0",
"@vueuse/core": "^8.9.4",
"@vueuse/core": "^9.0.0",
"axios": "^0.27.2",
"blueimp-md5": "^2.19.0",
"dayjs": "^1.11.4",
"element-plus": "^2.2.10",
"file-saver": "^2.0.5",
"pinia": "^2.0.16",
"lodash-es": "^4.17.21",
"pinia": "^2.0.17",
"qs": "^6.11.0",
"swiper": "^8.3.2",
"video.js": "^7.20.1",
"vue": "^3.2.37",
"vue-router": "^4.1.2"
......@@ -33,12 +36,12 @@
"@vue/tsconfig": "^0.1.3",
"ali-oss": "^6.17.1",
"eslint": "^8.5.0",
"eslint-plugin-vue": "^9.2.0",
"eslint-plugin-vue": "^9.3.0",
"npm-run-all": "^4.1.5",
"sass": "^1.54.0",
"typescript": "~4.7.4",
"unplugin-auto-import": "^0.10.1",
"vite": "^3.0.2",
"vite": "^3.0.3",
"vue-tsc": "^0.39.0"
}
},
......@@ -276,9 +279,9 @@
"dev": true
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.14",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.14.tgz",
"integrity": "sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A=="
"version": "0.0.15",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz",
"integrity": "sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA=="
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.30.6",
......@@ -730,55 +733,64 @@
}
},
"node_modules/@vueuse/core": {
"version": "8.9.4",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-8.9.4.tgz",
"integrity": "sha512-B/Mdj9TK1peFyWaPof+Zf/mP9XuGAngaJZBwPaXBvU3aCTZlx3ltlrFFFyMV4iGBwsjSCeUCgZrtkEj9dS2Y3Q==",
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.0.0.tgz",
"integrity": "sha512-hMMc2ajuVknkL7Z39JdP9gFFND2OgnDTSS5mmuinWGAE1Vxy1AwDvTHm3+juyk+GzJjYRAktnBIPy7Fq53iOnw==",
"dependencies": {
"@types/web-bluetooth": "^0.0.14",
"@vueuse/metadata": "8.9.4",
"@vueuse/shared": "8.9.4",
"@types/web-bluetooth": "^0.0.15",
"@vueuse/metadata": "9.0.0",
"@vueuse/shared": "9.0.0",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.13.5",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.5.tgz",
"integrity": "sha512-tO3K2bML3AwiHmVHeKCq6HLef2st4zBXIV5aEkoJl6HZ+gJWxWv2O8wLH8qrA3SX3lDoTDHNghLX1xZg83MXvw==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.1.0",
"vue": "^2.6.0 || ^3.2.0"
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/@vueuse/core/node_modules/@vueuse/shared": {
"version": "8.9.4",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-8.9.4.tgz",
"integrity": "sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==",
"node_modules/@vueuse/metadata": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.0.0.tgz",
"integrity": "sha512-79YVIsAP1bbWm5GdQuG7jDVF/9uuExzhkO0Sd4/TLuSfzH2uZOrHvGwy+ZNJHjbyRn3uf56rKINWLJdBuTLSqQ==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.0.0.tgz",
"integrity": "sha512-WRCyr/wIz5e/2gR/+qFucbCUcGMyJKkQZAzlECl3e71ebQQ9X/w3aBWT9FbnogJX+DNZ/t3Pj+TqPbC7TH1Yog==",
"dependencies": {
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.1.0",
"vue": "^2.6.0 || ^3.2.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.13.5",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.5.tgz",
"integrity": "sha512-tO3K2bML3AwiHmVHeKCq6HLef2st4zBXIV5aEkoJl6HZ+gJWxWv2O8wLH8qrA3SX3lDoTDHNghLX1xZg83MXvw==",
......@@ -803,14 +815,6 @@
}
}
},
"node_modules/@vueuse/metadata": {
"version": "8.9.4",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.9.4.tgz",
"integrity": "sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz",
......@@ -1305,9 +1309,9 @@
}
},
"node_modules/dayjs": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz",
"integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A=="
"version": "1.11.4",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.4.tgz",
"integrity": "sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g=="
},
"node_modules/debug": {
"version": "4.3.4",
......@@ -1455,6 +1459,14 @@
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
},
"node_modules/dom7": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.4.tgz",
"integrity": "sha512-DSSgBzQ4rJWQp1u6o+3FVwMNnT5bzQbMb+o31TjYYeRi05uAcpF8koxdfzeoe5ElzPmua7W7N28YJhF7iEKqIw==",
"dependencies": {
"ssr-window": "^4.0.0"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
......@@ -1486,6 +1498,93 @@
"vue": "^3.2.0"
}
},
"node_modules/element-plus/node_modules/@types/web-bluetooth": {
"version": "0.0.14",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.14.tgz",
"integrity": "sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A=="
},
"node_modules/element-plus/node_modules/@vueuse/core": {
"version": "8.9.4",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-8.9.4.tgz",
"integrity": "sha512-B/Mdj9TK1peFyWaPof+Zf/mP9XuGAngaJZBwPaXBvU3aCTZlx3ltlrFFFyMV4iGBwsjSCeUCgZrtkEj9dS2Y3Q==",
"dependencies": {
"@types/web-bluetooth": "^0.0.14",
"@vueuse/metadata": "8.9.4",
"@vueuse/shared": "8.9.4",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.1.0",
"vue": "^2.6.0 || ^3.2.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/element-plus/node_modules/@vueuse/core/node_modules/@vueuse/shared": {
"version": "8.9.4",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-8.9.4.tgz",
"integrity": "sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==",
"dependencies": {
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.1.0",
"vue": "^2.6.0 || ^3.2.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/element-plus/node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.13.5",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.5.tgz",
"integrity": "sha512-tO3K2bML3AwiHmVHeKCq6HLef2st4zBXIV5aEkoJl6HZ+gJWxWv2O8wLH8qrA3SX3lDoTDHNghLX1xZg83MXvw==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/element-plus/node_modules/@vueuse/metadata": {
"version": "8.9.4",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.9.4.tgz",
"integrity": "sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw==",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
......@@ -2065,9 +2164,9 @@
}
},
"node_modules/eslint-plugin-vue": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.2.0.tgz",
"integrity": "sha512-W2hc+NUXoce8sZtWgZ45miQTy6jNyuSdub5aZ1IBune4JDeAyzucYX0TzkrQ1jMO52sNUDYlCIHDoaNePe0p5g==",
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.3.0.tgz",
"integrity": "sha512-iscKKkBZgm6fGZwFt6poRoWC0Wy2dQOlwUPW++CiPoQiw1enctV2Hj5DBzzjJZfyqs+FAXhgzL4q0Ww03AgSmQ==",
"dev": true,
"dependencies": {
"eslint-utils": "^3.0.0",
......@@ -3998,11 +4097,11 @@
}
},
"node_modules/pinia": {
"version": "2.0.16",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.16.tgz",
"integrity": "sha512-9/LMVO+/epny1NBfC77vnps4g3JRezxhhoF1xLUk8mZkUIxVnwfEAIRiAX8mYBTD/KCwZqnDMqXc8w3eU0FQGg==",
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.17.tgz",
"integrity": "sha512-AtwLwEWQgIjofjgeFT+nxbnK5lT2QwQjaHNEDqpsi2AiCwf/NY78uWTeHUyEhiiJy8+sBmw0ujgQMoQbWiZDfA==",
"dependencies": {
"@vue/devtools-api": "^6.1.4",
"@vue/devtools-api": "^6.2.1",
"vue-demi": "*"
},
"funding": {
......@@ -4645,6 +4744,11 @@
"integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==",
"dev": true
},
"node_modules/ssr-window": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz",
"integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ=="
},
"node_modules/statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
......@@ -4799,6 +4903,29 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/swiper": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-8.3.2.tgz",
"integrity": "sha512-8wsC7ORYvVSnLUoxs2+xmfLrDPNjBVQXMCFbOlqtHeON6wtu/blOyySDr8TCBCdse1bdcIbn7m8xJNxVFL8o4Q==",
"funding": [
{
"type": "patreon",
"url": "https://www.patreon.com/swiperjs"
},
{
"type": "open_collective",
"url": "http://opencollective.com/swiper"
}
],
"hasInstallScript": true,
"dependencies": {
"dom7": "^4.0.4",
"ssr-window": "^4.0.2"
},
"engines": {
"node": ">= 4.7.0"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
......@@ -5208,9 +5335,9 @@
}
},
"node_modules/vite": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.0.2.tgz",
"integrity": "sha512-TAqydxW/w0U5AoL5AsD9DApTvGb2iNbGs3sN4u2VdT1GFkQVUfgUldt+t08TZgi23uIauh1TUOQJALduo9GXqw==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.0.3.tgz",
"integrity": "sha512-sDIpIcl3mv1NUaSzZwiXGEy1ZoWwwC2vkxUHY6yiDacR6zf//ZFuBJrozO62gedpE43pmxnLATNR5IYUdAEkMQ==",
"dev": true,
"dependencies": {
"esbuild": "^0.14.47",
......@@ -5679,9 +5806,9 @@
"dev": true
},
"@types/web-bluetooth": {
"version": "0.0.14",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.14.tgz",
"integrity": "sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A=="
"version": "0.0.15",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz",
"integrity": "sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA=="
},
"@typescript-eslint/eslint-plugin": {
"version": "5.30.6",
......@@ -6004,24 +6131,37 @@
"requires": {}
},
"@vueuse/core": {
"version": "8.9.4",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-8.9.4.tgz",
"integrity": "sha512-B/Mdj9TK1peFyWaPof+Zf/mP9XuGAngaJZBwPaXBvU3aCTZlx3ltlrFFFyMV4iGBwsjSCeUCgZrtkEj9dS2Y3Q==",
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.0.0.tgz",
"integrity": "sha512-hMMc2ajuVknkL7Z39JdP9gFFND2OgnDTSS5mmuinWGAE1Vxy1AwDvTHm3+juyk+GzJjYRAktnBIPy7Fq53iOnw==",
"requires": {
"@types/web-bluetooth": "^0.0.14",
"@vueuse/metadata": "8.9.4",
"@vueuse/shared": "8.9.4",
"@types/web-bluetooth": "^0.0.15",
"@vueuse/metadata": "9.0.0",
"@vueuse/shared": "9.0.0",
"vue-demi": "*"
},
"dependencies": {
"vue-demi": {
"version": "0.13.5",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.5.tgz",
"integrity": "sha512-tO3K2bML3AwiHmVHeKCq6HLef2st4zBXIV5aEkoJl6HZ+gJWxWv2O8wLH8qrA3SX3lDoTDHNghLX1xZg83MXvw==",
"requires": {}
}
}
},
"@vueuse/metadata": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.0.0.tgz",
"integrity": "sha512-79YVIsAP1bbWm5GdQuG7jDVF/9uuExzhkO0Sd4/TLuSfzH2uZOrHvGwy+ZNJHjbyRn3uf56rKINWLJdBuTLSqQ=="
},
"@vueuse/shared": {
"version": "8.9.4",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-8.9.4.tgz",
"integrity": "sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==",
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.0.0.tgz",
"integrity": "sha512-WRCyr/wIz5e/2gR/+qFucbCUcGMyJKkQZAzlECl3e71ebQQ9X/w3aBWT9FbnogJX+DNZ/t3Pj+TqPbC7TH1Yog==",
"requires": {
"vue-demi": "*"
}
},
"dependencies": {
"vue-demi": {
"version": "0.13.5",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.5.tgz",
......@@ -6030,11 +6170,6 @@
}
}
},
"@vueuse/metadata": {
"version": "8.9.4",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.9.4.tgz",
"integrity": "sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw=="
},
"@xmldom/xmldom": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz",
......@@ -6428,9 +6563,9 @@
"dev": true
},
"dayjs": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.3.tgz",
"integrity": "sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A=="
"version": "1.11.4",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.4.tgz",
"integrity": "sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g=="
},
"debug": {
"version": "4.3.4",
......@@ -6538,6 +6673,14 @@
"resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz",
"integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="
},
"dom7": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.4.tgz",
"integrity": "sha512-DSSgBzQ4rJWQp1u6o+3FVwMNnT5bzQbMb+o31TjYYeRi05uAcpF8koxdfzeoe5ElzPmua7W7N28YJhF7iEKqIw==",
"requires": {
"ssr-window": "^4.0.0"
}
},
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
......@@ -6564,6 +6707,45 @@
"lodash-unified": "^1.0.2",
"memoize-one": "^6.0.0",
"normalize-wheel-es": "^1.1.2"
},
"dependencies": {
"@types/web-bluetooth": {
"version": "0.0.14",
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.14.tgz",
"integrity": "sha512-5d2RhCard1nQUC3aHcq/gHzWYO6K0WJmAbjO7mQJgCQKtZpgXxv1rOM6O/dBDhDYYVutk1sciOgNSe+5YyfM8A=="
},
"@vueuse/core": {
"version": "8.9.4",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-8.9.4.tgz",
"integrity": "sha512-B/Mdj9TK1peFyWaPof+Zf/mP9XuGAngaJZBwPaXBvU3aCTZlx3ltlrFFFyMV4iGBwsjSCeUCgZrtkEj9dS2Y3Q==",
"requires": {
"@types/web-bluetooth": "^0.0.14",
"@vueuse/metadata": "8.9.4",
"@vueuse/shared": "8.9.4",
"vue-demi": "*"
},
"dependencies": {
"@vueuse/shared": {
"version": "8.9.4",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-8.9.4.tgz",
"integrity": "sha512-wt+T30c4K6dGRMVqPddexEVLa28YwxW5OFIPmzUHICjphfAuBFTTdDoyqREZNDOFJZ44ARH1WWQNCUK8koJ+Ag==",
"requires": {
"vue-demi": "*"
}
},
"vue-demi": {
"version": "0.13.5",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.5.tgz",
"integrity": "sha512-tO3K2bML3AwiHmVHeKCq6HLef2st4zBXIV5aEkoJl6HZ+gJWxWv2O8wLH8qrA3SX3lDoTDHNghLX1xZg83MXvw==",
"requires": {}
}
}
},
"@vueuse/metadata": {
"version": "8.9.4",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-8.9.4.tgz",
"integrity": "sha512-IwSfzH80bnJMzqhaapqJl9JRIiyQU0zsRGEgnxN6jhq7992cPUJIRfV+JHRIZXjYqbwt07E1gTEp0R0zPJ1aqw=="
}
}
},
"end-of-stream": {
......@@ -6927,9 +7109,9 @@
}
},
"eslint-plugin-vue": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.2.0.tgz",
"integrity": "sha512-W2hc+NUXoce8sZtWgZ45miQTy6jNyuSdub5aZ1IBune4JDeAyzucYX0TzkrQ1jMO52sNUDYlCIHDoaNePe0p5g==",
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.3.0.tgz",
"integrity": "sha512-iscKKkBZgm6fGZwFt6poRoWC0Wy2dQOlwUPW++CiPoQiw1enctV2Hj5DBzzjJZfyqs+FAXhgzL4q0Ww03AgSmQ==",
"dev": true,
"requires": {
"eslint-utils": "^3.0.0",
......@@ -8392,11 +8574,11 @@
"dev": true
},
"pinia": {
"version": "2.0.16",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.16.tgz",
"integrity": "sha512-9/LMVO+/epny1NBfC77vnps4g3JRezxhhoF1xLUk8mZkUIxVnwfEAIRiAX8mYBTD/KCwZqnDMqXc8w3eU0FQGg==",
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/pinia/-/pinia-2.0.17.tgz",
"integrity": "sha512-AtwLwEWQgIjofjgeFT+nxbnK5lT2QwQjaHNEDqpsi2AiCwf/NY78uWTeHUyEhiiJy8+sBmw0ujgQMoQbWiZDfA==",
"requires": {
"@vue/devtools-api": "^6.1.4",
"@vue/devtools-api": "^6.2.1",
"vue-demi": "*"
},
"dependencies": {
......@@ -8855,6 +9037,11 @@
"integrity": "sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g==",
"dev": true
},
"ssr-window": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz",
"integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ=="
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
......@@ -8967,6 +9154,15 @@
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true
},
"swiper": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/swiper/-/swiper-8.3.2.tgz",
"integrity": "sha512-8wsC7ORYvVSnLUoxs2+xmfLrDPNjBVQXMCFbOlqtHeON6wtu/blOyySDr8TCBCdse1bdcIbn7m8xJNxVFL8o4Q==",
"requires": {
"dom7": "^4.0.4",
"ssr-window": "^4.0.2"
}
},
"text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
......@@ -9287,9 +9483,9 @@
}
},
"vite": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.0.2.tgz",
"integrity": "sha512-TAqydxW/w0U5AoL5AsD9DApTvGb2iNbGs3sN4u2VdT1GFkQVUfgUldt+t08TZgi23uIauh1TUOQJALduo9GXqw==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-3.0.3.tgz",
"integrity": "sha512-sDIpIcl3mv1NUaSzZwiXGEy1ZoWwwC2vkxUHY6yiDacR6zf//ZFuBJrozO62gedpE43pmxnLATNR5IYUdAEkMQ==",
"dev": true,
"requires": {
"esbuild": "^0.14.47",
......
......@@ -15,13 +15,16 @@
"dependencies": {
"@element-plus/icons-vue": "^2.0.6",
"@tinymce/tinymce-vue": "^5.0.0",
"@vueuse/core": "^8.9.4",
"@vueuse/core": "^9.0.0",
"axios": "^0.27.2",
"blueimp-md5": "^2.19.0",
"dayjs": "^1.11.4",
"element-plus": "^2.2.10",
"file-saver": "^2.0.5",
"pinia": "^2.0.16",
"lodash-es": "^4.17.21",
"pinia": "^2.0.17",
"qs": "^6.11.0",
"swiper": "^8.3.2",
"video.js": "^7.20.1",
"vue": "^3.2.37",
"vue-router": "^4.1.2"
......@@ -38,12 +41,12 @@
"@vue/tsconfig": "^0.1.3",
"ali-oss": "^6.17.1",
"eslint": "^8.5.0",
"eslint-plugin-vue": "^9.2.0",
"eslint-plugin-vue": "^9.3.0",
"npm-run-all": "^4.1.5",
"sass": "^1.54.0",
"typescript": "~4.7.4",
"unplugin-auto-import": "^0.10.1",
"vite": "^3.0.2",
"vite": "^3.0.3",
"vue-tsc": "^0.39.0"
}
}
......@@ -2,7 +2,7 @@
const DEFAULT_OPTIONS = {
controls: true,
autoplay: false,
fluid: true,
// fluid: true,
playbackRates: [0.5, 1, 1.5, 2],
restoreEl: true
}
......@@ -80,7 +80,8 @@ function initPlayer() {
return player
}
function changeSrc(src: string | { src: string; type?: string }) {
if (!player) return
if (!player || !src) return
// if (!player.paused()) {
// console.log(2)
// player.pause()
......@@ -98,7 +99,5 @@ onUnmounted(() => {
</script>
<template>
<div>
<video class="video-js vjs-default-skin vjs-big-play-centered vjs-16-9" ref="videoRef"></video>
</div>
<video class="video-js vjs-default-skin vjs-big-play-centered" ref="videoRef"></video>
</template>
......@@ -4,7 +4,9 @@ export default { name: 'AppMain' }
<template>
<section class="app-main">
<div class="app-main-inner">
<router-view></router-view>
</div>
</section>
</template>
......@@ -15,4 +17,9 @@ export default { name: 'AppMain' }
margin: 20px;
overflow: hidden;
}
.app-main-inner {
max-width: 1440px;
height: 100%;
margin: 0 auto;
}
</style>
......@@ -26,6 +26,12 @@ export function getCourse(params: { course_id: string; semester_id: string }) {
export function getChapterTreeList(data: { course_id: string; semester_id: string }) {
return httpRequest.post('/api/saas/api/v1/chapter/tree', data)
}
// 课程考核列表
export function getChapterVideoTreeList(data: { course_id: string; semester_id: string }) {
return httpRequest.post('/api/saas/api/v1/chapter/video-tree', data)
}
// 收藏/取消收藏
export function collectionResource(data: {
course_id: string
......@@ -38,7 +44,8 @@ export function collectionResource(data: {
}) {
return httpRequest.post('/api/saas/api/v1/collection/resource', data)
}
// 获取大作业详情
// 获取课程大作业详情
export function getCourseWork(data: { course_id: string; semester_id: string }) {
return httpRequest.post('/api/saas/api/v1/job/detail', data)
}
......@@ -53,6 +60,16 @@ export function submitCourseWork(data: {
return httpRequest.post('/api/saas/api/v1/job/submit', data)
}
// 获取课程直播列表
export function getCourseExamList(params: { course_id: string; semester_id: string }) {
return httpRequest.get('/api/saas/api/v1/question-bank/papers', { params })
}
// 获取课程直播列表
export function getCourseLiveList(data: { course_id: string; semester_id: string }) {
return httpRequest.post('/api/saas/api/v1/meeting/list', data)
}
// 获取课程小节信息
export function getCourseSection(data: { section_id: string; semester_id: string }) {
return httpRequest.post('/api/saas/api/v1/chapter/section/detail', data)
......@@ -64,26 +81,25 @@ export function getCoursePlayInfo(params: { source_id: string }) {
}
// 获取视频观看记录
export function getVideoRecords(data: {
sso_id: string
semester_id: string
chapter_id: string
course_id: string
video_id: string
section_id: string
current_playing_time: string
max_playing_time: string
valid_playing_time: string
cumulative_playing_time: string
semester_id: string
video_id: string
}) {
return httpRequest.post('/api/saas/api/v1/course/video/recent-viewings', data)
}
// 上传视频观看记录
export function uploadVideoRecords(data: {
semester_id: string
chapter_id: string
course_id: string
video_id: string
cumulative_playing_time: string
current_playing_time: number
max_playing_time: number
section_id: string
semester_id: string
valid_playing_time: number
video_id: string
}) {
return httpRequest.post('/api/saas/api/v1/course/video/upload-records', data)
}
<script setup lang="ts">
import type { CourseListItemTypes } from '../types'
import type { CourseListItemType } from '../types'
import { useMapStore } from '@/stores/map'
import { topCourse } from '../api'
import { ElMessage } from 'element-plus'
interface Props {
data: CourseListItemTypes
data: CourseListItemType
}
const props = defineProps<Props>()
......@@ -28,7 +28,7 @@ const electiveTypeText = computed(() => {
// 是否选中
const isActive = computed(() => props.data.course_id === courseId)
// 置顶
function handleTop(data: CourseListItemTypes) {
function handleTop(data: CourseListItemType) {
topCourse({ id: data.id, status: data.is_top ? 0 : 1 }).then(() => {
emit('change')
ElMessage.success('操作成功')
......
<script setup lang="ts">
import { Search, Filter } from '@element-plus/icons-vue'
import type { SemesterType } from '@/types'
import type { CourseListParamsTypes } from '../types'
import type { CourseListParamsType } from '../types'
import { searchCourseList, getSemesterList } from '../api'
import { useMapStore } from '@/stores/map'
......@@ -12,7 +12,7 @@ const mapStore = useMapStore()
const electiveTypes = mapStore.getMapValuesByKey('system_elective_type')
// 列表参数
const params = reactive<CourseListParamsTypes>({ id: '', semester_ids: [], elective_types: [] })
const params = reactive<CourseListParamsType>({ id: '', semester_ids: [], elective_types: [] })
// 搜索课程
const searchValue = $ref('')
......
<!-- 学习 -->
<script setup lang="ts">
import * as api from '../api'
let chapterList = $ref<CourseType[]>([])
const { query } = useRoute()
const courseId = $ref(query.course_id as string)
const semesterId = $ref(query.semester_id as string)
// 获取章节列表
function fetchList() {
if (!courseId || !semesterId) {
return
}
api.getChapterTreeList({ course_id: courseId, semester_id: semesterId }).then(res => {
chapterList = res.data.items
})
import type { CourseChapterType } from '../types'
interface Props {
chapterList: CourseChapterType[]
}
onMounted(() => {
fetchList()
defineProps<Props>()
const route = useRoute()
let courseId = $ref<string>('')
let sectionId = $ref<string>('')
let semesterId = $ref<string>('')
watchEffect(() => {
courseId = route.query.course_id as string
sectionId = route.query.section_id as string
semesterId = route.query.semester_id as string
})
</script>
<template>
<div class="course-player-chapter">
<el-tabs>
<el-tab-pane label="章节"></el-tab-pane>
<el-tab-pane label="讲义"></el-tab-pane>
</el-tabs>
<el-collapse>
<el-collapse-item :title="item.name" :name="item.id" v-for="item in chapterList" :key="item.id">
<el-collapse>
<el-collapse-item :title="section.name" :name="section.id" v-for="section in item.sections" :key="section.id">
<ul>
<li v-for="resource in section.resources" :key="resource.id">
<el-tab-pane label="章节">
<dl v-for="(item, index) in chapterList" :key="item.id">
<dt>
<span>{{ index + 1 }}</span>
<p>{{ item.name }}</p>
</dt>
<dd v-for="section in item.sections" :key="section.id" :class="{ 'is-active': section.id === sectionId }">
<router-link
:to="`/course/player?course_id=${courseId}&section_id=${section.id}&semester_id=${semesterId}`"
:to="`/course/player?course_id=${courseId}&chapter_id=${item.id}&section_id=${section.id}&semester_id=${semesterId}`"
>
{{ resource.name }}
{{ section.name }}
</router-link>
</li>
</ul>
</el-collapse-item>
</el-collapse>
</el-collapse-item>
</el-collapse>
</dd>
</dl>
</el-tab-pane>
<el-tab-pane label="讲义"></el-tab-pane>
</el-tabs>
</div>
</template>
<style lang="scss" scoped>
.course-player-chapter {
width: 258px;
height: 100%;
padding: 20px;
background-color: #1f1e24;
}
li {
box-sizing: border-box;
:deep(.el-tabs__nav) {
float: none;
display: flex;
align-items: center;
height: 48px;
line-height: 48px;
border-bottom: 1px solid #e6e6e6;
a {
}
:deep(.el-tabs__item) {
flex: 1;
height: 40px;
font-size: 16px;
line-height: 40px;
color: #fff;
text-align: center;
&.is-active {
color: var(--main-color);
}
}
dl {
color: #fff;
padding: 20px 0;
}
dl + dl {
border-top: 1px dashed #b2b2b2;
}
dt {
display: flex;
align-items: center;
margin-bottom: 10px;
span {
display: inline-block;
width: 26px;
height: 26px;
color: var(--main-color);
line-height: 26px;
text-align: center;
background-color: #fff;
border-radius: 50%;
}
p {
margin-left: 8px;
font-size: 16px;
font-weight: 500;
line-height: 26px;
color: #fff;
}
}
dd {
margin-left: 34px;
padding: 5px 0;
font-size: 14px;
font-weight: 400;
line-height: 24px;
color: #ffffff;
&.is-active {
color: var(--main-color);
}
}
}
</style>
<script setup lang="ts">
import type { CourseResourceType } from '../types'
import ResourceIcon from '@/components/ResourceIcon.vue'
import { Download } from '@element-plus/icons-vue'
import { saveAs } from 'file-saver'
import { collectionResource } from '../api'
const { query } = useRoute()
const courseId = $ref(query.course_id as string)
const chapterId = $ref(query.chapter_id as string)
const semesterId = $ref(query.semester_id as string)
const sectionId = $ref(query.section_id as string)
interface Props {
data: CourseResourceType
}
const props = defineProps<Props>()
// 跳转链接
const targetHref = computed(() => {
const info = props.data.info
if (['pptx', 'doc', 'docx', 'xls', 'xlsx'].includes(info.type)) {
return `https://view.officeapps.live.com/op/view.aspx?src=${info.url}`
} else {
return info.url
}
})
// 收藏/取消收藏
function toggleCollection(data: CourseResourceType) {
// 资源类型: 1章节,2视频,3作业,4其他,6腾讯会议,9考试,10课件,11教案
// 收藏类型: 1其他, 2视频,3课件,4教案,5作业,6帖子
const typeMap: Record<number, number> = { 4: 1, 2: 2, 10: 3, 11: 4, 3: 5, 9: 5 }
collectionResource({
course_id: courseId,
semester_id: semesterId,
chapter_id: chapterId,
section_id: sectionId,
source_id: data.resource_id,
type: typeMap[data.resource_type],
status: data.collection_count ? 0 : 1
}).then(() => {
data.collection_count = data.collection_count ? 0 : 1
})
}
// 是否可以下载
function canDownload(type: number) {
return [4, 10, 11].includes(type)
}
// 下载资源
function downloadFile(data: CourseResourceType) {
data.info.url && saveAs(data.info.url, data.info.name)
}
</script>
<template>
<div class="course-resource-item">
<p>
<a :href="targetHref" target="_blank">
<ResourceIcon :resourceType="data.resource_type" />
{{ data.name }}
</a>
</p>
<div class="actions">
<i class="icon-star" :class="!!data.collection_count ? 'is-active' : ''" @click="toggleCollection(data)"></i>
<i class="icon-download" @click="downloadFile(data)" v-if="canDownload(data.resource_type)"><Download /></i>
</div>
</div>
</template>
<style lang="scss" scoped>
.course-resource-item {
display: flex;
align-items: center;
height: 48px;
padding: 0 20px;
border-bottom: 1px solid #e6e6e6;
p {
flex: 1;
line-height: 48px;
}
&:hover {
color: var(--main-color);
}
.actions {
width: 60px;
display: flex;
align-items: center;
justify-content: space-between;
}
}
.icon-star {
display: inline-block;
width: 16px;
height: 16px;
background: url(@/assets/images/icon_star.png) no-repeat;
background-size: contain;
cursor: pointer;
&.is-active {
background: url(@/assets/images/icon_star_hover.png) no-repeat;
background-size: contain;
}
}
.icon-download {
display: inline-block;
width: 20px;
height: 20px;
cursor: pointer;
}
</style>
<script setup lang="ts">
import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue'
import type { CourseChapterType, CourseSectionType, CourseResourceType, VideoRecordType, PlayItemType } from '../types'
import type { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js'
import { throttle } from 'lodash'
import { useStorage } from '@vueuse/core'
import { getCoursePlayInfo } from '../api'
import type { PlayItemTypes } from '../types'
import { Swiper, SwiperSlide } from 'swiper/vue'
import 'swiper/css'
import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue'
import { getCoursePlayInfo, getVideoRecords, uploadVideoRecords } from '../api'
interface Props {
chapterList: CourseChapterType[]
}
const props = defineProps<Props>()
const options = $ref<VideoJsPlayerOptions>()
const { query } = useRoute()
const sourceId = $ref(query.source_id as string)
const route = useRoute()
let courseId = $ref<string>('')
let chapterId = $ref<string>('')
let sectionId = $ref<string>('')
let resourceId = $ref<string>('')
let semesterId = $ref<string>('')
watchEffect(() => {
courseId = route.query.course_id as string
chapterId = route.query.chapter_id as string
sectionId = route.query.section_id as string
resourceId = route.query.resource_id as string
semesterId = route.query.semester_id as string
})
// 当前章
const chapter = $computed<CourseSectionType>(() => {
return props.chapterList.find(item => item.id === chapterId)
})
// 当前节
const section = $computed<CourseSectionType>(() => {
return chapter?.sections.find(item => item.id === sectionId)
})
// 当前节
const resource = $computed<CourseResourceType>(() => {
return section?.resources.find(item => item.resource_id === resourceId)
})
// 资源视频列表
const videoList = $computed<CourseResourceType[]>(() => {
const list = section?.resources ?? []
return list.filter(item => item.resource_type === 2)
})
watchEffect(() => {
if (videoList.length) {
const found = videoList.find(item => item.resource_id === resourceId)
resourceId = found ? resourceId : videoList[0]?.resource_id
}
})
const progress = reactive<VideoRecordType>({
cumulative_playing_time: '',
current_playing_time: 0,
max_playing_time: 0,
valid_playing_time: 0,
watchedTimePoint: []
})
let playList = $ref<PlayItemTypes[]>([])
const currentPlayList = $computed<PlayItemTypes[]>(() => {
let playList = $ref<PlayItemType[]>([])
const currentPlayList = $computed<PlayItemType[]>(() => {
return playList.filter(item => item.StreamType === 'video' && item.Format === 'mp4')
})
// 获取视频信息
function fetchInfo() {
getCoursePlayInfo({ source_id: sourceId }).then(res => {
if (!resource?.info.source_id) return
return getCoursePlayInfo({ source_id: resource.info.source_id }).then(res => {
playList = res.data.playing_list
changeSrc(currentPlayList[0])
})
}
onMounted(fetchInfo)
// 获取视频记录
function fetchVideoRecords() {
if (!resource) return
getVideoRecords({
chapter_id: chapterId,
course_id: courseId,
section_id: sectionId,
semester_id: semesterId,
video_id: resourceId
}).then(res => {
const { detail = {} } = res.data
progress.current_playing_time = detail.current_playing_time ? parseFloat(detail.current_playing_time) : 0
progress.max_playing_time = detail.max_playing_time ? parseFloat(detail.max_playing_time) : 0
progress.valid_playing_time = detail.valid_playing_time ? parseFloat(detail.valid_playing_time) : 0
progress.watchedTimePoint = []
if (videoJsPlayer && progress.current_playing_time) {
videoJsPlayer.currentTime(progress.current_playing_time)
}
})
}
watchEffect(async () => {
await fetchInfo()
fetchVideoRecords()
})
const throttledFn = throttle(
() => {
if (progress.watchedTimePoint.length < 5) return
uploadVideoRecords({
chapter_id: chapterId,
course_id: courseId,
section_id: sectionId,
semester_id: semesterId,
video_id: resourceId,
valid_playing_time: progress.valid_playing_time,
current_playing_time: progress.current_playing_time,
max_playing_time: progress.max_playing_time,
cumulative_playing_time: progress.watchedTimePoint.join(',')
})
// 清空已经上传过的观看时间点
progress.watchedTimePoint = []
},
5000,
{ leading: false }
)
let watchedTime = 0
function onTimeUpdate() {
console.log('onTimeUpdate')
if (!videoJsPlayer) return
const time = Math.floor(videoJsPlayer.currentTime() ?? 0)
// 更新当前播放时间
progress.current_playing_time = time
// 观看的最大点
progress.max_playing_time = Math.max(time, progress.max_playing_time)
// 观看时间点
const hasTimePoint = progress.watchedTimePoint.includes(time)
if (!hasTimePoint) {
progress.watchedTimePoint.push(time)
}
// 更新观看累计时长
if (time !== watchedTime) {
watchedTime = time
// // 增加跳过片头时间
// if (this.isSkip && !this.progress.pt) {
// this.progress.pt = this.skipTime + 20
// }
// 默认增加时间
progress.valid_playing_time = progress.valid_playing_time || 20
progress.valid_playing_time++
}
throttledFn()
}
let src = $ref({ type: '', src: '' })
/**
* 视频播放器相关
*/
let src = $ref<{ src: string; type: string }>()
// 跳过片头
const isSkip = useStorage('isSkip', false)
// 连续播放
......@@ -34,16 +164,26 @@ let videoJsPlayer = $ref<VideoJsPlayer | null>()
const onReady = (player: VideoJsPlayer) => {
videoJsPlayer = player
isReady = true
console.log(videoJsPlayer)
}
function changeSrc(data: PlayItemTypes) {
function changeSrc(data: PlayItemType) {
// src = { src: data.PlayURL, type: 'application/x-mpegURL' }
src = { src: data.PlayURL, type: 'video/mp4' }
}
function changeResource(data: CourseResourceType) {
throttledFn && throttledFn.cancel()
resourceId = data.resource_id
}
</script>
<template>
<AppVideoPlayer :options="options" :src="src" @ready="onReady"></AppVideoPlayer>
<AppVideoPlayer
:options="options"
:src="src"
@ready="onReady"
@timeupdate="onTimeUpdate"
style="width: 100%; height: 510px"
v-if="src"
></AppVideoPlayer>
<!-- 设置 -->
<teleport to=".vjs-control-bar" v-if="isReady">
<el-popover trigger="hover" effect="dark" placement="top" :teleported="false">
......@@ -68,6 +208,18 @@ function changeSrc(data: PlayItemTypes) {
</ul>
</el-popover>
</teleport>
<swiper :slidesPerView="'auto'" :spaceBetween="30">
<swiper-slide
v-for="item in videoList"
:key="item.id"
class="video-item"
:class="{ 'is-active': item.resource_id === resourceId }"
@click="changeResource(item)"
>
<img :src="item.info?.cover" />
<p>{{ item.info.name }}</p>
</swiper-slide>
</swiper>
</template>
<style lang="scss" scoped>
......@@ -75,4 +227,37 @@ function changeSrc(data: PlayItemTypes) {
.vjs-icon-cog {
font-size: 1.8em;
}
.video-item {
position: relative;
margin: 20px 0;
width: 200px;
height: 100px;
border-radius: 5px;
cursor: pointer;
overflow: hidden;
box-sizing: border-box;
p {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 0 10px;
font-size: 14px;
line-height: 30px;
color: #fff;
background-color: rgba(0, 0, 0, 0.5);
text-align: center;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
&.is-active {
border: 2px solid var(--main-color);
}
}
</style>
<!-- 课程考核 -->
<script setup lang="ts"></script>
<template>课程考核</template>
<script setup lang="ts">
import { getChapterVideoTreeList } from '../api'
const { query } = useRoute()
const courseId = $ref(query.course_id as string)
const semesterId = $ref(query.semester_id as string)
let list = $ref([])
// 树列表
const treeList = $computed(() => {
return makeTree(list)
})
// 扁平列表
const flatList = $computed(() => {
return tree2List(list)
})
// 累计学习时长
const studyTime = $computed(() => {
return list.reduce((total, item) => {
return total + item.watch_video_length || 0
}, 0)
})
// 视频统计
const videoStatistics = $computed(() => {
return flatList.reduce(
(results, item) => {
if (item.depth === 3) {
results.length++
if (item.is_finished) {
results.completedLength++
}
}
return results
},
{ length: 0, completedLength: 0 }
)
})
// 完成率
const completedPercent = $computed(() => {
const percent = (videoStatistics.completedLength / videoStatistics.length) * 100
return parseFloat(percent.toFixed(2)) || 0
})
// 树结构转换为列表
function tree2List(tree, parent) {
return tree.reduce(function (acc, item) {
if (item.depth === 2) {
item.chapter_id = parent.id
}
if (item.depth === 3) {
item.chapter_id = parent.chapter_id
item.section_id = parent.id
}
acc.push(item)
if (item.sections) acc = acc.concat(tree2List(item.sections, item))
if (item.resources) acc = acc.concat(tree2List(item.resources, item))
return acc
}, [])
}
// 将列表转换为树结构
function makeTree(list: any) {
return list.map(item => {
if (item.sections || item.resources) item.children = makeTree(item.sections || item.resources)
return item
})
}
function fetchList() {
getChapterVideoTreeList({ course_id: courseId, semester_id: semesterId }).then(res => {
list = res.data.items
})
}
onMounted(() => {
fetchList()
})
</script>
<template>
<div class="course-assess">
<div class="course-assess-hd">
<p class="t1">课程“音视频”观看统计(累计学习时长:{{ studyTime }}</p>
<div class="course-assess-hd__aside">
<p class="t2">完成率</p>
<el-progress :percentage="completedPercent" style="width: 200px" />
</div>
</div>
<el-table
:data="treeList"
default-expand-all
row-key="id"
style="width: 100%"
:header-cell-style="{ 'background-color': '#F7F8FA' }"
>
<el-table-column prop="name" label="章节">
<template #default="{ row }">
<template v-if="row.depth === 3">
<router-link
:to="`/course/player?course_id=${courseId}&chapter_id=${row.chapter_id}&section_id=${row.section_id}&resource_id=${row.resource_id}&semester_id=${semesterId}`"
target="_blank"
>
{{ row.name }}
</router-link>
</template>
<template v-else>{{ row.name }}</template>
</template>
</el-table-column>
<el-table-column prop="watch_video_length" label="学习时长" width="180" align="center" />
<el-table-column prop="name" label="百分比" width="180" align="center">
<template #default="{ row }">
<el-progress :percentage="row.schedule" />
</template>
</el-table-column>
</el-table>
</div>
</template>
<style lang="scss" scoped>
.course-assess-hd {
padding-bottom: 20px;
display: flex;
align-items: center;
.t1 {
flex: 1;
font-size: 16px;
font-weight: 400;
color: var(--main-color);
}
}
.course-assess-hd__aside {
display: flex;
.t2 {
padding: 0 20px;
font-size: 16px;
color: #333333;
}
}
</style>
<!-- 学习 -->
<script setup lang="ts">
import type { CourseChapterTypes, CourseSectionTypes, CourseResourceTypes } from '../types'
import type { CourseChapterType, CourseSectionType, CourseResourceType } from '../types'
import ResourceIcon from '@/components/ResourceIcon.vue'
import { getChapterTreeList, collectionResource } from '../api'
let chapterList = $ref<CourseChapterTypes[]>([])
let chapterList = $ref<CourseChapterType[]>([])
const { query } = useRoute()
const courseId = $ref(query.course_id as string)
const semesterId = $ref(query.semester_id as string)
......@@ -21,14 +21,14 @@ onMounted(() => {
fetchList()
})
// 是否可以收藏
function canCollection(data: CourseResourceTypes) {
function canCollection(data: CourseResourceType) {
return [4, 2, 10, 11, 3].includes(data.resource_type)
}
// 收藏/取消收藏
function toggleCollection(resource: CourseResourceTypes, section: CourseSectionTypes, chapter: CourseChapterTypes) {
function toggleCollection(resource: CourseResourceType, section: CourseSectionType, chapter: CourseChapterType) {
// 资源类型: 1章节,2视频,3作业,4其他,6腾讯会议,9考试,10课件,11教案
// 收藏类型: 1其他, 2视频,3课件,4教案,5作业,6帖子
const typeMap: Record<number, number> = { 4: 1, 2: 2, 10: 3, 11: 4, 3: 5 }
const typeMap: Record<number, number> = { 4: 1, 2: 2, 10: 3, 11: 4, 3: 5, 9: 5 }
collectionResource({
course_id: courseId,
semester_id: semesterId,
......@@ -49,13 +49,11 @@ function toggleCollection(resource: CourseResourceTypes, section: CourseSectionT
<template #title><i class="icon-chapter"></i>{{ item.name }}</template>
<el-collapse class="course-sections">
<el-collapse-item :name="section.id" v-for="section in item.sections" :key="section.id">
<template #title><i class="icon-chapter"></i>{{ item.name }}</template>
<template #title><i class="icon-chapter"></i>{{ section.name }}</template>
<ul>
<li class="course-resource-item" v-for="resource in section.resources" :key="resource.id">
<router-link
:to="`/course/player?course_id=${courseId}&section_id=${
section.id
}&semester_id=${semesterId}&&source_id=${resource.info?.source_id || ''}`"
:to="`/course/player?course_id=${courseId}&chapter_id=${item.id}&section_id=${section.id}&resource_id=${resource.resource_id}&semester_id=${semesterId}`"
target="_blank"
>
<ResourceIcon :resourceType="resource.resource_type" />
......
<!-- 考试 -->
<script setup lang="ts"></script>
<template>考试</template>
<script setup lang="ts">
import type { PaperType } from '@/types'
import CourseViewExamItem from './CourseViewExamItem.vue'
import { getCourseExamList } from '../api'
const { query } = useRoute()
const courseId = $ref(query.course_id as string)
const semesterId = $ref(query.semester_id as string)
let list = $ref<PaperType[]>([])
function fetchList() {
getCourseExamList({ course_id: courseId, semester_id: semesterId }).then(res => {
list = res.data.items
})
}
onMounted(() => {
fetchList()
})
</script>
<template>
<div class="course-exam">
<CourseViewExamItem v-for="item in list" :data="item" :key="item.id"></CourseViewExamItem>
</div>
</template>
<script setup lang="ts">
import type { PaperType } from '@/types'
interface Props {
data: PaperType
}
defineProps<Props>()
</script>
<template>
<div class="course-exam-item" :key="data.id">
<p>
{{ data.paper_title }}
</p>
</div>
</template>
<style lang="scss" scoped>
.course-exam-item {
display: flex;
align-items: center;
height: 48px;
border-bottom: 1px solid #e6e6e6;
p {
flex: 1;
line-height: 48px;
}
&:hover {
color: var(--main-color);
}
.actions {
display: flex;
align-items: center;
justify-content: space-between;
}
}
</style>
<!-- 直播 -->
<script setup lang="ts"></script>
<template>直播</template>
<script setup lang="ts">
import type { LiveType } from '@/types'
import CourseViewLiveItem from './CourseViewLiveItem.vue'
import { getCourseLiveList } from '../api'
const { query } = useRoute()
const courseId = $ref(query.course_id as string)
const semesterId = $ref(query.semester_id as string)
let list = $ref<LiveType[]>([])
function fetchList() {
getCourseLiveList({ course_id: courseId, semester_id: semesterId }).then(res => {
list = res.data.items
})
}
onMounted(() => {
fetchList()
})
</script>
<template>
<div class="course-live">
<CourseViewLiveItem v-for="item in list" :data="item" :key="item.id"></CourseViewLiveItem>
</div>
</template>
<script setup lang="ts">
import type { LiveType } from '@/types'
import dayjs from 'dayjs'
import { VideoPlay } from '@element-plus/icons-vue'
interface Props {
data: LiveType
}
defineProps<Props>()
function formatDate(startTime: number) {
return dayjs(startTime * 1000).format('YYYY-MM-DD HH:mm')
}
</script>
<template>
<div class="course-live-item">
<p>
<a :href="data.join_url" target="_blank">
<el-icon><VideoPlay /></el-icon>
{{ data.subject }}
</a>
</p>
<div class="actions">{{ formatDate(data.start_time) }}</div>
</div>
</template>
<style lang="scss" scoped>
.course-live-item {
display: flex;
align-items: center;
height: 48px;
border-bottom: 1px solid #e6e6e6;
p {
flex: 1;
line-height: 48px;
}
&:hover {
color: var(--main-color);
}
.actions {
display: flex;
align-items: center;
justify-content: space-between;
}
}
</style>
......@@ -16,7 +16,10 @@ const form = reactive({
semester_id: semesterId,
title: '',
content: '',
attachments: []
attachments: [],
is_critiqued: 0,
reviews: '',
score: ''
})
const rules = ref<FormRules>({
......@@ -38,6 +41,7 @@ const update = () => {
}
// 获取作业
let workInfo = $ref('')
function fetchInfo() {
getCourseWork({ course_id: courseId, semester_id: semesterId }).then(res => {
const { detail, essay } = res.data
......@@ -52,6 +56,11 @@ onMounted(() => {
<template>
<div class="course-work">
<div class="course-work-item" v-if="form.is_critiqued">
<h2>作业评价</h2>
<h3>分数:{{ form.score }}</h3>
<div v-html="form.reviews"></div>
</div>
<div class="course-work-item">
<h2>作业说明</h2>
<div v-html="workInfo"></div>
......
import type { CourseType, ChapterType, ResourceType } from '@/types'
export interface CourseListParamsTypes {
export interface CourseListParamsType {
[key: string]: any
elective_types: string[]
id: string
semester_ids: string[]
}
export interface CourseListItemTypes extends CourseType {
export interface CourseListItemType extends CourseType {
schedule: string
section?: ChapterType
semester_id: string
......@@ -14,25 +14,25 @@ export interface CourseListItemTypes extends CourseType {
}
// 课程章类型
export interface CourseChapterTypes {
export interface CourseChapterType {
id: string
name: string
resource_id: string
resource_type: number
sections: CourseSectionTypes[]
sections: CourseSectionType[]
}
// 课程节类型
export interface CourseSectionTypes {
export interface CourseSectionType {
id: string
name: string
resource_id: string
resource_type: number
resources: CourseResourceTypes[]
resources: CourseResourceType[]
}
// 课程资源类型
export interface CourseResourceTypes {
export interface CourseResourceType {
id: string
name: string
resource_id: string
......@@ -41,7 +41,15 @@ export interface CourseResourceTypes {
info: ResourceType
}
export interface PlayItemTypes {
export interface VideoRecordType {
cumulative_playing_time: string
current_playing_time: number
max_playing_time: number
valid_playing_time: number
watchedTimePoint: number[]
}
export interface PlayItemType {
BitDepth: number
Bitrate: string
CreationTime: string
......
<script setup lang="ts">
import CourseListSearch from '../components/CourseListSearch.vue'
import CourseListItem from '../components/CourseListItem.vue'
import type { CourseListParamsTypes, CourseListItemTypes } from '../types'
import type { CourseListParamsType, CourseListItemType } from '../types'
import { getCourseList } from '../api'
// 列表参数
const listParams = reactive<CourseListParamsTypes>({ id: '', semester_ids: [], elective_types: [] })
let courseList = $ref<CourseListItemTypes[]>([])
const listParams = reactive<CourseListParamsType>({ id: '', semester_ids: [], elective_types: [] })
let courseList = $ref<CourseListItemType[]>([])
// 获取课程列表
function fetchList() {
const params = Object.assign({}, listParams, {
......@@ -18,7 +18,7 @@ function fetchList() {
})
}
// 筛选
function handleSearch(params: CourseListParamsTypes) {
function handleSearch(params: CourseListParamsType) {
Object.assign(listParams, params)
fetchList()
}
......
<script setup lang="ts">
import { getCourseSection } from '../api'
import type { CourseResourceType, CourseChapterType } from '../types'
import { getCourseSection, getChapterTreeList } from '../api'
import CoursePlayerResourceItem from '../components/CoursePlayerResourceItem.vue'
const CoursePlayerVideo = defineAsyncComponent(() => import('../components/CoursePlayerVideo.vue'))
const CoursePlayerChapter = defineAsyncComponent(() => import('../components/CoursePlayerChapter.vue'))
const { query } = useRoute()
const semesterId = $ref(query.semester_id as string)
const sectionId = $ref(query.section_id as string)
const sourceId = $ref(query.source_id as string)
const route = useRoute()
let courseId = $ref<string>('')
let sectionId = $ref<string>('')
let semesterId = $ref<string>('')
watchEffect(() => {
courseId = route.query.course_id as string
sectionId = route.query.section_id as string
semesterId = route.query.semester_id as string
})
const detail = reactive<{
course_name: string
coursewares: CourseResourceType[]
exams: CourseResourceType[]
jobs: CourseResourceType[]
lesson_plans: CourseResourceType[]
other_infos: CourseResourceType[]
}>({ course_name: '', coursewares: [], exams: [], jobs: [], lesson_plans: [], other_infos: [] })
let loading = $ref<boolean>(false)
// 获取详情信息
function fetchInfo() {
if (!sectionId) return
loading = true
getCourseSection({ section_id: sectionId, semester_id: semesterId }).then(res => {
console.log(res)
Object.assign(detail, res.data)
loading = false
})
}
watchEffect(() => {
fetchInfo()
})
let chapterList = $ref<CourseChapterType[]>([])
// 获取章节列表
function fetchList() {
getChapterTreeList({ course_id: courseId, semester_id: semesterId }).then(res => {
chapterList = res.data.items
})
}
onMounted(fetchInfo)
onMounted(() => {
fetchList()
})
</script>
<template>
<div class="course-player">
<div class="course-player-main">
<CoursePlayerVideo v-if="sourceId" />
<div class="course-player-hd">
<h1 class="course-player-main__title">{{ detail.course_name }}</h1>
</div>
<div class="course-player-bd">
<div class="course-player-main" v-loading="loading">
<CoursePlayerVideo :chapterList="chapterList" :key="sectionId" />
<el-tabs>
<el-tab-pane label="课件"> </el-tab-pane>
<el-tab-pane label="教案" lazy> </el-tab-pane>
<el-tab-pane label="作业" lazy> </el-tab-pane>
<el-tab-pane label="资料" lazy> </el-tab-pane>
<el-tab-pane label="考试/测验" lazy></el-tab-pane>
<el-tab-pane label="直播" lazy></el-tab-pane>
<el-tab-pane label="课件">
<CoursePlayerResourceItem
v-for="item in detail.coursewares"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
</el-tab-pane>
<el-tab-pane label="教案" lazy>
<CoursePlayerResourceItem
v-for="item in detail.lesson_plans"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
</el-tab-pane>
<el-tab-pane label="作业" lazy>
<CoursePlayerResourceItem
v-for="item in detail.jobs"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
</el-tab-pane>
<el-tab-pane label="资料" lazy>
<CoursePlayerResourceItem
v-for="item in detail.other_infos"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
</el-tab-pane>
<el-tab-pane label="考试/测验" lazy>
<CoursePlayerResourceItem
v-for="item in detail.exams"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
</el-tab-pane>
<!-- <el-tab-pane label="直播" lazy></el-tab-pane> -->
</el-tabs>
</div>
<div class="course-player-aside">
<CoursePlayerChapter />
<CoursePlayerChapter :chapterList="chapterList" />
</div>
</div>
</div>
</template>
......@@ -38,8 +107,26 @@ onMounted(fetchInfo)
<style lang="scss" scoped>
.course-player {
display: flex;
flex-direction: column;
height: 100%;
padding: 20px;
background-color: #fff;
border-radius: 6px;
box-sizing: border-box;
}
.course-player-bd {
flex: 1;
display: flex;
}
.course-player-main {
flex: 1;
overflow: hidden;
}
.course-player-main__title {
margin-bottom: 10px;
font-size: 20px;
font-weight: 500;
line-height: 30px;
color: #333333;
}
</style>
......@@ -85,7 +85,7 @@ function downloadFile(data: CollectionType) {
<li class="collection-item" v-for="item in list" :key="item.id">
<p>
<router-link
:to="`/course/player?course_id=${item.course_id}&section_id=${item.section_id}&semester_id=${item.semester_id}&source_id=${item.info?.source_id}`"
:to="`/course/player?course_id=${item.course_id}&chapter_id=${item.chapter_id}&section_id=${item.section_id}&resource_id=${item.source_id}&semester_id=${item.semester_id}`"
target="_blank"
>
<ResourceIcon :resourceType="item.type" />
......@@ -137,8 +137,8 @@ function downloadFile(data: CollectionType) {
}
.icon-download {
display: inline-block;
width: 16px;
height: 16px;
width: 20px;
height: 20px;
cursor: pointer;
}
</style>
......@@ -102,9 +102,52 @@ export interface ResourceType {
id: string
knowledge_points: string
name: string
type: string
pdf?: string
url?: string
cover?: string
size: number
source_id: string
paper_title?: string
}
// 直播类型
export interface LiveType {
end_time: number
format_status: number
id: string
join_url: string
meeting_code: string
meeting_id: string
meeting_type: number
start_time: number
status: number
sub_meetings: []
subject: string
}
// 试卷类型
export interface PaperType {
id: string
is_multiple_exams: 0 | 1
minimum_paper_handing_time: 0 | 1
multiple_test_score_rule: 0 | 1
paper_category: PaperCategoryType
paper_labels: string
paper_question_order: 0 | 1
paper_times: number
paper_title: string
paper_total_score: number
paper_type: number
paper_uses: number
pass_score: number
permission: number
project_prefix: string
}
// 试卷分类类型
export interface PaperCategoryType {
id: string
category_name: string
name: string
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论