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

chore: update

上级 ab2d3548
...@@ -10,11 +10,11 @@ ...@@ -10,11 +10,11 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.0.6", "@element-plus/icons-vue": "^2.0.6",
"@tinymce/tinymce-vue": "^5.0.0", "@tinymce/tinymce-vue": "^5.0.0",
"@vueuse/core": "^9.0.0", "@vueuse/core": "^9.0.2",
"axios": "^0.27.2", "axios": "^0.27.2",
"blueimp-md5": "^2.19.0", "blueimp-md5": "^2.19.0",
"dayjs": "^1.11.4", "dayjs": "^1.11.4",
"element-plus": "^2.2.11", "element-plus": "^2.2.12",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"format-duration": "^2.0.0", "format-duration": "^2.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
...@@ -733,13 +733,13 @@ ...@@ -733,13 +733,13 @@
} }
}, },
"node_modules/@vueuse/core": { "node_modules/@vueuse/core": {
"version": "9.0.0", "version": "9.0.2",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.0.0.tgz", "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.0.2.tgz",
"integrity": "sha512-hMMc2ajuVknkL7Z39JdP9gFFND2OgnDTSS5mmuinWGAE1Vxy1AwDvTHm3+juyk+GzJjYRAktnBIPy7Fq53iOnw==", "integrity": "sha512-kOIqaQPSs7OSByWg1ulEKRUJbsq3FmbJiUr0RhEKpt3O1Uhl4DrDj85DUbQBABVYgPvSaY6AE/fP3/FOcRIOoQ==",
"dependencies": { "dependencies": {
"@types/web-bluetooth": "^0.0.15", "@types/web-bluetooth": "^0.0.15",
"@vueuse/metadata": "9.0.0", "@vueuse/metadata": "9.0.2",
"@vueuse/shared": "9.0.0", "@vueuse/shared": "9.0.2",
"vue-demi": "*" "vue-demi": "*"
}, },
"funding": { "funding": {
...@@ -772,17 +772,17 @@ ...@@ -772,17 +772,17 @@
} }
}, },
"node_modules/@vueuse/metadata": { "node_modules/@vueuse/metadata": {
"version": "9.0.0", "version": "9.0.2",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.0.0.tgz", "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.0.2.tgz",
"integrity": "sha512-79YVIsAP1bbWm5GdQuG7jDVF/9uuExzhkO0Sd4/TLuSfzH2uZOrHvGwy+ZNJHjbyRn3uf56rKINWLJdBuTLSqQ==", "integrity": "sha512-TRh+TNUYXiodatSAxd0xZc7sh4RfktVVgNFIN7TCQXKyancbCAcWfHvKfgdlX8LcqSBxKoHVa90n0XdUbboTkw==",
"funding": { "funding": {
"url": "https://github.com/sponsors/antfu" "url": "https://github.com/sponsors/antfu"
} }
}, },
"node_modules/@vueuse/shared": { "node_modules/@vueuse/shared": {
"version": "9.0.0", "version": "9.0.2",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.0.0.tgz", "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.0.2.tgz",
"integrity": "sha512-WRCyr/wIz5e/2gR/+qFucbCUcGMyJKkQZAzlECl3e71ebQQ9X/w3aBWT9FbnogJX+DNZ/t3Pj+TqPbC7TH1Yog==", "integrity": "sha512-KwBDefK2ljLESpt0ffe2w8EGUCb3IaMfTzeytB/uHHjHOGOEIHLHHyn8W2C48uGQEvoe5iwaW4Bfp8cRUM6IFA==",
"dependencies": { "dependencies": {
"vue-demi": "*" "vue-demi": "*"
}, },
...@@ -791,9 +791,9 @@ ...@@ -791,9 +791,9 @@
} }
}, },
"node_modules/@vueuse/shared/node_modules/vue-demi": { "node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.13.5", "version": "0.13.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.5.tgz", "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.6.tgz",
"integrity": "sha512-tO3K2bML3AwiHmVHeKCq6HLef2st4zBXIV5aEkoJl6HZ+gJWxWv2O8wLH8qrA3SX3lDoTDHNghLX1xZg83MXvw==", "integrity": "sha512-02NYpxgyGE2kKGegRPYlNQSL1UWfA/+JqvzhGCOYjhfbLWXU5QQX0+9pAm/R2sCOPKr5NBxVIab7fvFU0B1RxQ==",
"hasInstallScript": true, "hasInstallScript": true,
"bin": { "bin": {
"vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-fix": "bin/vue-demi-fix.js",
...@@ -1458,9 +1458,9 @@ ...@@ -1458,9 +1458,9 @@
"dev": true "dev": true
}, },
"node_modules/element-plus": { "node_modules/element-plus": {
"version": "2.2.11", "version": "2.2.12",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.2.11.tgz", "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.2.12.tgz",
"integrity": "sha512-JjOvz5DLBc4Jp9OHKXNcK/Cys4NX5/vxpZ+gYmH2V+pLkwJnyIOrNZ3QxfdyG6yE4+NkpoA6koEgUB7T+0Z5vQ==", "integrity": "sha512-g/hIHj3b+dND2R3YRvyvCJtJhQvR7lWvXqhJaoxaQmajjNWedoe4rttxG26fOSv9YCC2wN4iFDcJHs70YFNgrA==",
"dependencies": { "dependencies": {
"@ctrl/tinycolor": "^3.4.1", "@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.0.6", "@element-plus/icons-vue": "^2.0.6",
...@@ -5343,13 +5343,13 @@ ...@@ -5343,13 +5343,13 @@
"requires": {} "requires": {}
}, },
"@vueuse/core": { "@vueuse/core": {
"version": "9.0.0", "version": "9.0.2",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.0.0.tgz", "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.0.2.tgz",
"integrity": "sha512-hMMc2ajuVknkL7Z39JdP9gFFND2OgnDTSS5mmuinWGAE1Vxy1AwDvTHm3+juyk+GzJjYRAktnBIPy7Fq53iOnw==", "integrity": "sha512-kOIqaQPSs7OSByWg1ulEKRUJbsq3FmbJiUr0RhEKpt3O1Uhl4DrDj85DUbQBABVYgPvSaY6AE/fP3/FOcRIOoQ==",
"requires": { "requires": {
"@types/web-bluetooth": "^0.0.15", "@types/web-bluetooth": "^0.0.15",
"@vueuse/metadata": "9.0.0", "@vueuse/metadata": "9.0.2",
"@vueuse/shared": "9.0.0", "@vueuse/shared": "9.0.2",
"vue-demi": "*" "vue-demi": "*"
}, },
"dependencies": { "dependencies": {
...@@ -5362,22 +5362,22 @@ ...@@ -5362,22 +5362,22 @@
} }
}, },
"@vueuse/metadata": { "@vueuse/metadata": {
"version": "9.0.0", "version": "9.0.2",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.0.0.tgz", "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.0.2.tgz",
"integrity": "sha512-79YVIsAP1bbWm5GdQuG7jDVF/9uuExzhkO0Sd4/TLuSfzH2uZOrHvGwy+ZNJHjbyRn3uf56rKINWLJdBuTLSqQ==" "integrity": "sha512-TRh+TNUYXiodatSAxd0xZc7sh4RfktVVgNFIN7TCQXKyancbCAcWfHvKfgdlX8LcqSBxKoHVa90n0XdUbboTkw=="
}, },
"@vueuse/shared": { "@vueuse/shared": {
"version": "9.0.0", "version": "9.0.2",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.0.0.tgz", "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.0.2.tgz",
"integrity": "sha512-WRCyr/wIz5e/2gR/+qFucbCUcGMyJKkQZAzlECl3e71ebQQ9X/w3aBWT9FbnogJX+DNZ/t3Pj+TqPbC7TH1Yog==", "integrity": "sha512-KwBDefK2ljLESpt0ffe2w8EGUCb3IaMfTzeytB/uHHjHOGOEIHLHHyn8W2C48uGQEvoe5iwaW4Bfp8cRUM6IFA==",
"requires": { "requires": {
"vue-demi": "*" "vue-demi": "*"
}, },
"dependencies": { "dependencies": {
"vue-demi": { "vue-demi": {
"version": "0.13.5", "version": "0.13.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.5.tgz", "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.6.tgz",
"integrity": "sha512-tO3K2bML3AwiHmVHeKCq6HLef2st4zBXIV5aEkoJl6HZ+gJWxWv2O8wLH8qrA3SX3lDoTDHNghLX1xZg83MXvw==", "integrity": "sha512-02NYpxgyGE2kKGegRPYlNQSL1UWfA/+JqvzhGCOYjhfbLWXU5QQX0+9pAm/R2sCOPKr5NBxVIab7fvFU0B1RxQ==",
"requires": {} "requires": {}
} }
} }
...@@ -5890,9 +5890,9 @@ ...@@ -5890,9 +5890,9 @@
"dev": true "dev": true
}, },
"element-plus": { "element-plus": {
"version": "2.2.11", "version": "2.2.12",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.2.11.tgz", "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.2.12.tgz",
"integrity": "sha512-JjOvz5DLBc4Jp9OHKXNcK/Cys4NX5/vxpZ+gYmH2V+pLkwJnyIOrNZ3QxfdyG6yE4+NkpoA6koEgUB7T+0Z5vQ==", "integrity": "sha512-g/hIHj3b+dND2R3YRvyvCJtJhQvR7lWvXqhJaoxaQmajjNWedoe4rttxG26fOSv9YCC2wN4iFDcJHs70YFNgrA==",
"requires": { "requires": {
"@ctrl/tinycolor": "^3.4.1", "@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.0.6", "@element-plus/icons-vue": "^2.0.6",
......
...@@ -15,11 +15,11 @@ ...@@ -15,11 +15,11 @@
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.0.6", "@element-plus/icons-vue": "^2.0.6",
"@tinymce/tinymce-vue": "^5.0.0", "@tinymce/tinymce-vue": "^5.0.0",
"@vueuse/core": "^9.0.0", "@vueuse/core": "^9.0.2",
"axios": "^0.27.2", "axios": "^0.27.2",
"blueimp-md5": "^2.19.0", "blueimp-md5": "^2.19.0",
"dayjs": "^1.11.4", "dayjs": "^1.11.4",
"element-plus": "^2.2.11", "element-plus": "^2.2.12",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"format-duration": "^2.0.0", "format-duration": "^2.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
......
src/assets/images/icon_doc.png

659 Bytes | W: | H:

src/assets/images/icon_doc.png

1.8 KB | W: | H:

src/assets/images/icon_doc.png
src/assets/images/icon_doc.png
src/assets/images/icon_doc.png
src/assets/images/icon_doc.png
  • 2-up
  • Swipe
  • Onion skin
src/assets/images/icon_mp4.png

725 Bytes | W: | H:

src/assets/images/icon_mp4.png

1.7 KB | W: | H:

src/assets/images/icon_mp4.png
src/assets/images/icon_mp4.png
src/assets/images/icon_mp4.png
src/assets/images/icon_mp4.png
  • 2-up
  • Swipe
  • Onion skin
src/assets/images/icon_ppt.png

697 Bytes | W: | H:

src/assets/images/icon_ppt.png

1.7 KB | W: | H:

src/assets/images/icon_ppt.png
src/assets/images/icon_ppt.png
src/assets/images/icon_ppt.png
src/assets/images/icon_ppt.png
  • 2-up
  • Swipe
  • Onion skin
<script setup lang="ts"> <script setup lang="ts">
interface Props { interface Props {
resourceType: number resourceType?: number
info?: any
}
const props = defineProps<Props>()
function show(fileType: string) {
return props.info?.type === fileType
} }
defineProps<Props>()
</script> </script>
<template> <template>
<div class="icon-resource"> <div class="icon-resource">
<img src="@/assets/images/icon_mp4.png" v-if="resourceType === 2" /> <img src="@/assets/images/icon_mp4.png" v-if="resourceType === 2 || show('mp4')" />
<img src="@/assets/images/icon_work.png" v-else-if="resourceType === 3" /> <img src="@/assets/images/icon_work.png" v-else-if="resourceType === 3" />
<img src="@/assets/images/icon_live.png" v-else-if="resourceType === 6" />
<img src="@/assets/images/icon_exam.png" v-else-if="resourceType === 9" /> <img src="@/assets/images/icon_exam.png" v-else-if="resourceType === 9" />
<img src="@/assets/images/icon_ppt.png" v-else-if="show('pptx')" />
<img src="@/assets/images/icon_xls.png" v-else-if="show('xlsx')" />
<img src="@/assets/images/icon_mp3.png" v-else-if="show('mp3')" />
<img src="@/assets/images/icon_jpg.png" v-else-if="show('jpg')" />
<img src="@/assets/images/icon_png.png" v-else-if="show('png')" />
<img src="@/assets/images/icon_pdf.png" v-else-if="show('pdf')" />
<img src="@/assets/images/icon_doc.png" v-else /> <img src="@/assets/images/icon_doc.png" v-else />
<!-- <img src="@/assets/images/icon_ppt.png" v-else-if="resourceType === 10" />
<img src="@/assets/images/icon_rar.png" v-else-if="resourceType === 4" /> -->
</div> </div>
</template> </template>
......
...@@ -6,7 +6,7 @@ import { getSignature, uploadFile } from '@/api/base' ...@@ -6,7 +6,7 @@ import { getSignature, uploadFile } from '@/api/base'
const props = defineProps({ const props = defineProps({
height: { height: {
type: Number, type: Number,
default: 600 default: 400
} }
}) })
......
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
const DEFAULT_OPTIONS = { const DEFAULT_OPTIONS = {
controls: true, controls: true,
autoplay: false, autoplay: false,
controlBar: {
pictureInPictureToggle: false
},
// fluid: true, // fluid: true,
responsive: true, responsive: true,
playbackRates: [0.5, 1, 1.5, 2] playbackRates: [0.5, 1, 1.5, 2]
......
...@@ -86,7 +86,9 @@ function handleClick(index: number) { ...@@ -86,7 +86,9 @@ function handleClick(index: number) {
} }
.question-num { .question-num {
flex: 1; flex: 1;
padding-top: 20px; margin: 20px 0;
overflow-y: auto;
overflow-x: hidden;
.tit { .tit {
font-size: 12px; font-size: 12px;
color: #999999; color: #999999;
...@@ -94,33 +96,26 @@ function handleClick(index: number) { ...@@ -94,33 +96,26 @@ function handleClick(index: number) {
margin-bottom: 10px; margin-bottom: 10px;
} }
ul { ul {
display: flex; display: grid;
list-style: none; grid-template-columns: repeat(5, 1fr);
padding: 0; gap: 10px;
margin: 0;
flex-wrap: wrap;
} }
} }
.question-num-item { .question-num-item {
cursor: pointer;
position: relative; position: relative;
border-radius: 50px; margin: 0 auto;
width: 24px; width: 24px;
height: 24px; height: 24px;
font-size: 14px; font-size: 14px;
line-height: 24px; line-height: 24px;
margin-right: 20px; color: #666;
margin-bottom: 10px;
text-align: center; text-align: center;
border: 2px solid #ccc; border: 2px solid #ccc;
color: #666; border-radius: 50px;
cursor: pointer;
&:nth-child(5n) {
margin-right: 0;
}
} }
.question-num-tips { .question-num-tips {
padding-bottom: 20px; padding: 10px 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
......
...@@ -78,7 +78,9 @@ function handleClick(index: number) { ...@@ -78,7 +78,9 @@ function handleClick(index: number) {
} }
.question-num { .question-num {
flex: 1; flex: 1;
padding-top: 20px; margin: 20px 0;
overflow-y: auto;
overflow-x: hidden;
.tit { .tit {
font-size: 12px; font-size: 12px;
color: #999999; color: #999999;
...@@ -86,30 +88,23 @@ function handleClick(index: number) { ...@@ -86,30 +88,23 @@ function handleClick(index: number) {
margin-bottom: 10px; margin-bottom: 10px;
} }
ul { ul {
display: flex; display: grid;
list-style: none; grid-template-columns: repeat(10, 1fr);
padding: 0; gap: 10px;
margin: 0;
flex-wrap: wrap;
} }
} }
.question-num-item { .question-num-item {
cursor: pointer;
position: relative; position: relative;
border-radius: 50px; margin: 0 auto;
width: 24px; width: 24px;
height: 24px; height: 24px;
font-size: 14px; font-size: 14px;
line-height: 24px; line-height: 24px;
margin-right: 20px; color: #666;
margin-bottom: 10px;
text-align: center; text-align: center;
border: 2px solid #ccc; border: 2px solid #ccc;
color: #666; border-radius: 50px;
cursor: pointer;
&:nth-child(10n) {
margin-right: 0;
}
} }
.question-num-tips { .question-num-tips {
padding: 0 20px 20px; padding: 0 20px 20px;
...@@ -147,7 +142,7 @@ function handleClick(index: number) { ...@@ -147,7 +142,7 @@ function handleClick(index: number) {
} }
.is-review { .is-review {
color: #fff; color: #fff !important;
background-color: blue; background-color: blue;
border: 2px solid blue; border: 2px solid blue;
} }
......
...@@ -188,6 +188,10 @@ const filterList = computed(() => { ...@@ -188,6 +188,10 @@ const filterList = computed(() => {
.el-checkbox-group { .el-checkbox-group {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex-wrap: wrap;
}
.el-checkbox-button {
margin-bottom: 10px;
} }
.el-checkbox-button__inner { .el-checkbox-button__inner {
padding: 10px 20px; padding: 10px 20px;
......
<!-- 学习 --> <!-- 学习 -->
<script setup lang="ts"> <script setup lang="ts">
import format from 'format-duration'
import type { PptType } from '@/types'
import type { CourseChapterType } from '../types' import type { CourseChapterType } from '../types'
interface Props { interface Props {
chapterList: CourseChapterType[] chapterList: CourseChapterType[]
pptList: PptType[]
} }
defineProps<Props>() const props = defineProps<Props>()
const route = useRoute() const route = useRoute()
let courseId = $ref<string>('') let courseId = $ref<string>('')
...@@ -15,12 +19,25 @@ watchEffect(() => { ...@@ -15,12 +19,25 @@ watchEffect(() => {
sectionId = route.query.section_id as string sectionId = route.query.section_id as string
semesterId = route.query.semester_id as string semesterId = route.query.semester_id as string
}) })
let activeTab = $ref<string>('chapter')
watchEffect(() => {
if (!props.pptList.length) {
activeTab = 'chapter'
}
})
// 时长转换
function formatDuration(duration: number | undefined) {
if (!duration) return ''
return format(duration * 1000, { leading: true })
}
</script> </script>
<template> <template>
<div class="course-player-chapter"> <div class="course-player-chapter">
<el-tabs> <el-tabs v-model="activeTab">
<el-tab-pane label="章节"> <el-tab-pane label="章节" name="chapter">
<dl v-for="(item, index) in chapterList" :key="item.id"> <dl v-for="(item, index) in chapterList" :key="item.id">
<dt> <dt>
<span>{{ index + 1 }}</span> <span>{{ index + 1 }}</span>
...@@ -35,14 +52,20 @@ watchEffect(() => { ...@@ -35,14 +52,20 @@ watchEffect(() => {
</dd> </dd>
</dl> </dl>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="讲义"></el-tab-pane> <el-tab-pane label="讲义" name="ppt" v-if="pptList && pptList.length">
<ul class="lecture-list">
<li v-for="item in pptList" :key="item.url">
<img :src="`${item.url}?x-oss-process=image/resize,m_fill,h_128,w_218`" loading="lazy" />
<span>{{ formatDuration(item.point) }}</span>
</li>
</ul>
</el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.course-player-chapter { .course-player-chapter {
width: 258px;
height: 100%; height: 100%;
padding: 20px; padding: 20px;
background-color: #1f1e24; background-color: #1f1e24;
...@@ -84,6 +107,7 @@ watchEffect(() => { ...@@ -84,6 +107,7 @@ watchEffect(() => {
border-radius: 50%; border-radius: 50%;
} }
p { p {
flex: 1;
margin-left: 8px; margin-left: 8px;
font-size: 16px; font-size: 16px;
font-weight: 500; font-weight: 500;
...@@ -93,7 +117,7 @@ watchEffect(() => { ...@@ -93,7 +117,7 @@ watchEffect(() => {
} }
dd { dd {
margin-left: 34px; margin-left: 34px;
padding: 5px 0; padding: 5px 0 5px 1em;
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
line-height: 24px; line-height: 24px;
...@@ -102,5 +126,29 @@ watchEffect(() => { ...@@ -102,5 +126,29 @@ watchEffect(() => {
color: var(--main-color); color: var(--main-color);
} }
} }
.lecture-list {
li {
position: relative;
margin: 10px 0;
height: 128px;
cursor: pointer;
&.is-active {
border: solid 2px var(--main-color);
}
}
img {
width: 100%;
}
span {
position: absolute;
left: 10px;
top: 10px;
padding: 0 5px;
font-size: 12px;
background: rgba(255, 255, 255, 0.5);
border-radius: 4px;
}
}
} }
</style> </style>
<template>
<div class="ppt-player">
<template v-if="ppts.length">
<div class="ppt-player-preview">
<img :src="pptUrl" v-if="pptUrl" />
</div>
<div class="ppt-player-controls">
<div class="ppt-player-controls__page">
<template v-if="currentIndex >= 0">
<i class="el-icon-arrow-left" @click="prev"></i>
</template>
<template v-if="currentIndex + 1 < ppts.length">
<i class="el-icon-arrow-right" @click="next"></i>
</template>
</div>
<div class="ppt-player-controls__pages">
<span class="is-active">{{ currentIndex + 1 }}</span>
/
<span>{{ ppts.length }}</span
>
</div>
<div class="ppt-player-controls__tools">
<el-tooltip content="PPT同步视频播放">
<i :class="['el-icon-self-xuexiao', isSync ? 'active' : '']" @click="onToggleSync"></i>
</el-tooltip>
<el-tooltip content="放大PPT">
<i class="el-icon-self-quanping" @click="fullscreen"></i>
</el-tooltip>
<el-tooltip content="切换视频到当前PPT页">
<i class="el-icon-self-shipin" @click="setVideoTime"></i>
</el-tooltip>
<el-tooltip content="关闭PPT">
<i class="el-icon-self-guanbi" @click="$emit('close')"></i>
</el-tooltip>
</div>
</div>
</template>
</div>
</template>
<script>
export default {
name: 'ppt-player',
props: {
ppts: { type: Array },
index: { type: Number, default: 0 }
},
data() {
return {
currentIndex: this.index,
isSync: true,
isFullscreen: false
}
},
watch: {
index: {
handler(value) {
if (this.isSync) {
this.currentIndex = value
}
}
}
},
computed: {
pptUrl() {
return this.ppts[this.currentIndex] ? this.ppts[this.currentIndex].ppt_url : ''
}
},
methods: {
gotoIndex(index) {
this.currentIndex = index
},
getIndex(index) {
return Math.min(this.ppts.length - 1, Math.max(0, index))
},
prev() {
this.currentIndex = this.getIndex(this.currentIndex - 1)
this.isSync = false
},
next(e) {
this.currentIndex = this.getIndex(this.currentIndex + 1)
this.isSync = false
},
onToggleSync(e) {
this.isSync = !this.isSync
},
setVideoTime(e) {
this.isSync = true
this.$emit('videoSyncTime', this.ppts[this.currentIndex].ppt_point)
},
// 全屏
fullscreen() {
this.isFullscreen = !this.isFullscreen
this.$emit('fullscreen', this.isFullscreen)
}
}
}
</script>
<style lang="scss" scoped>
.ppt-player {
position: relative;
width: 100%;
height: 100%;
background-color: #000;
}
.ppt-player-preview {
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.ppt-player-controls {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 44px;
line-height: 44px;
padding: 0 14px;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
}
.ppt-player-controls__page {
width: 90px;
color: #fff;
i {
padding: 0 10px;
font-size: 18px;
cursor: pointer;
}
}
.ppt-player-controls__pages {
flex: 1;
color: #fff;
text-align: center;
}
.ppt-player-controls__pages .is-active {
color: #d29f29;
}
.ppt-player-controls__tools {
float: right;
}
.ppt-player-controls__tools i {
color: #fff;
margin: 0 10px;
cursor: pointer;
}
.ppt-player-controls__tools i.active,
.ppt-player-controls__tools i:hover {
color: #d29f29;
}
.ppt-player-controls__tools .icon-rotate {
font-size: 1.125em;
}
</style>
<script setup lang="ts">
import type { CourseResourceType } from '../types'
import CoursePlayerResourceListItem from './CoursePlayerResourceListItem.vue'
defineProps<{
list: CourseResourceType[]
}>()
</script>
<template>
<div class="list">
<template v-if="list.length">
<CoursePlayerResourceListItem v-for="item in list" :data="item" :key="item.id" />
</template>
<el-empty description="暂无数据" v-else />
</div>
</template>
...@@ -20,7 +20,7 @@ const props = defineProps<Props>() ...@@ -20,7 +20,7 @@ const props = defineProps<Props>()
// 跳转链接 // 跳转链接
const targetUrl = computed(() => { const targetUrl = computed(() => {
const info = props.data.info const info = props.data.info || {}
if (props.data.resource_type === 3 || props.data.resource_type === 9) { if (props.data.resource_type === 3 || props.data.resource_type === 9) {
return `/course/exam?course_id=${courseId}&semester_id=${semesterId}&paper_id=${props.data.resource_id}&type=2&paper_title=${props.data.name}` return `/course/exam?course_id=${courseId}&semester_id=${semesterId}&paper_id=${props.data.resource_id}&type=2&paper_title=${props.data.name}`
} else if (props.data.resource_type === 6) { } else if (props.data.resource_type === 6) {
...@@ -71,7 +71,7 @@ function downloadFile(data: CourseResourceType) { ...@@ -71,7 +71,7 @@ function downloadFile(data: CourseResourceType) {
<div class="course-resource-item"> <div class="course-resource-item">
<p> <p>
<a :href="targetUrl" target="_blank"> <a :href="targetUrl" target="_blank">
<ResourceIcon :resourceType="data.resource_type" /> <ResourceIcon :resourceType="data.resource_type" :info="data.info" />
{{ data.name }} {{ data.name }}
</a> </a>
</p> </p>
...@@ -83,7 +83,7 @@ function downloadFile(data: CourseResourceType) { ...@@ -83,7 +83,7 @@ function downloadFile(data: CourseResourceType) {
> >
<el-button round size="small">查看报告</el-button> <el-button round size="small">查看报告</el-button>
</router-link> </router-link>
<template v-if="data.resource_type === 6"> <template v-if="data.resource_type === 6 && data.info">
<span>{{ formatLiveDate(data.info.start_time) }}</span> <span>{{ formatLiveDate(data.info.start_time) }}</span>
<span style="margin-left: 10px">{{ formatLiveStatus(data.info.status) }}</span> <span style="margin-left: 10px">{{ formatLiveStatus(data.info.status) }}</span>
</template> </template>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import type { CourseChapterType, CourseResourceType, VideoRecordType, PlayItemType } from '../types' import type { CourseChapterType, CourseResourceType, VideoRecordType, PlayItemType } from '../types'
import type { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js' import type { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js'
import { throttle } from 'lodash' import { throttle } from 'lodash'
import { useStorage } from '@vueuse/core' import { useStorage, useElementVisibility } from '@vueuse/core'
import { Swiper, SwiperSlide } from 'swiper/vue' import { Swiper, SwiperSlide } from 'swiper/vue'
import 'swiper/css' import 'swiper/css'
import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue' import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue'
...@@ -13,6 +13,9 @@ interface Props { ...@@ -13,6 +13,9 @@ interface Props {
} }
const props = defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'updateResource', resource: CourseResourceType | undefined): void
}>()
const options = $ref<VideoJsPlayerOptions>() const options = $ref<VideoJsPlayerOptions>()
...@@ -30,39 +33,31 @@ watchEffect(() => { ...@@ -30,39 +33,31 @@ watchEffect(() => {
semesterId = route.query.semester_id as string semesterId = route.query.semester_id as string
}) })
const skipTime = $ref<number>(6) const playerWrapperRef = ref(null)
const playerIsVisible = useElementVisibility(playerWrapperRef)
/** /**
* 视频播放器相关 * 视频播放器相关
*/ */
let src = $ref<{ src: string; type: string }>() let src = $ref<{ src: string; type: string }>()
function changeSrc(data: PlayItemType) {
// src = { src: data.PlayURL, type: 'application/x-mpegURL' }
src = { src: data.PlayURL, type: 'video/mp4' }
}
// 跳过片头 // 跳过片头
const isSkip = $ref(useStorage('isSkip', false)) const isSkip = $ref(useStorage('isSkip', false))
const skipTime = $ref<number>(6)
// 连续播放 // 连续播放
const isAutoPlayNext = $ref(useStorage('isAutoPlayNext', false)) const isAutoPlayNext = $ref(useStorage('isAutoPlayNext', false))
// 播放器ready // 播放器ready
let isReady = $ref<boolean>(false) let isReady = $ref<boolean>(false)
let videoJsPlayer = $ref<VideoJsPlayer | null>() let videoJsPlayer = $ref<VideoJsPlayer | null>(inject('videoJsPlayer'))
const onReady = (player: VideoJsPlayer) => { const onReady = (player: VideoJsPlayer) => {
isReady = true isReady = true
videoJsPlayer = player videoJsPlayer = player
} }
function changeSrc(data: PlayItemType) {
// src = { src: data.PlayURL, type: 'application/x-mpegURL' }
src = { src: data.PlayURL, type: 'video/mp4' }
}
// 切换视频清晰度
function changeDefinition(data: PlayItemType) {
changeSrc(data)
}
// 切换视频资源
function changeResource(data: CourseResourceType) {
throttledFn && throttledFn.flush()
resourceId = data.resource_id
videoJsPlayer && videoJsPlayer.pause()
}
// 当前章 // 当前章
const chapter = $computed(() => { const chapter = $computed(() => {
return props.chapterList.find(item => item.id === chapterId) return props.chapterList.find(item => item.id === chapterId)
...@@ -72,23 +67,34 @@ const section = $computed(() => { ...@@ -72,23 +67,34 @@ const section = $computed(() => {
return chapter?.sections.find(item => item.id === sectionId) return chapter?.sections.find(item => item.id === sectionId)
}) })
// 当前节 // 当前节下的视频列表
const resource = $computed(() => { const sectionVideoList = $computed<CourseResourceType[]>(() => {
return section?.resources.find(item => item.resource_id === resourceId && item.resource_type === 2)
})
// 资源视频列表
const resourceVideoList = $computed<CourseResourceType[]>(() => {
const list = section?.resources ?? [] const list = section?.resources ?? []
return list.filter(item => item.resource_type === 2) return list.filter(item => item.resource_type === 2)
}) })
watchEffect(() => { watchEffect(() => {
if (resourceVideoList.length) { if (sectionVideoList.length) {
const found = resourceVideoList.find(item => item.resource_id === resourceId) const found = sectionVideoList.find(item => item.resource_id === resourceId)
resourceId = found ? resourceId : resourceVideoList[0]?.resource_id resourceId = found ? resourceId : sectionVideoList[0]?.resource_id
} }
}) })
// 当前资源
const resource = $computed(() => {
return section?.resources.find(item => item.resource_id === resourceId && item.resource_type === 2)
})
watchEffect(() => {
emit('updateResource', resource)
})
// 切换视频资源
function changeResource(data: CourseResourceType) {
throttledFn && throttledFn.flush()
resourceId = data.resource_id
videoJsPlayer?.pause()
}
// 进度信息
const progress = reactive<VideoRecordType>({ const progress = reactive<VideoRecordType>({
cumulative_playing_time: '', cumulative_playing_time: '',
current_playing_time: 0, current_playing_time: 0,
...@@ -141,6 +147,10 @@ watchEffect(async () => { ...@@ -141,6 +147,10 @@ watchEffect(async () => {
function setVideoInfo() { function setVideoInfo() {
if (!videoJsPlayer) return if (!videoJsPlayer) return
// 上次播放结束,设置进度为0
if (videoJsPlayer.duration() - progress.current_playing_time < 1) {
progress.current_playing_time = 0
}
// 统计,增加默认时间10秒 // 统计,增加默认时间10秒
progress.valid_playing_time = progress.valid_playing_time || 10 progress.valid_playing_time = progress.valid_playing_time || 10
// 跳过片头 // 跳过片头
...@@ -170,7 +180,7 @@ const throttledFn = throttle( ...@@ -170,7 +180,7 @@ const throttledFn = throttle(
valid_playing_time: progress.valid_playing_time, valid_playing_time: progress.valid_playing_time,
current_playing_time: progress.current_playing_time, current_playing_time: progress.current_playing_time,
max_playing_time: progress.max_playing_time, max_playing_time: progress.max_playing_time,
cumulative_playing_time: progress.watchedTimePoint.join(',') cumulative_playing_time: progress.watchedTimePoint.join(',') || progress.current_playing_time.toString()
}) })
// 清空已经上传过的观看时间点 // 清空已经上传过的观看时间点
progress.watchedTimePoint = [] progress.watchedTimePoint = []
...@@ -206,30 +216,42 @@ function onTimeUpdate() { ...@@ -206,30 +216,42 @@ function onTimeUpdate() {
// 播放结束 // 播放结束
function onEnded() { function onEnded() {
console.log('onEnd') console.log('onEnd')
throttledFn && throttledFn.flush()
// 自动播放下一个视频 // 自动播放下一个视频
if (isAutoPlayNext) { if (isAutoPlayNext) {
const currentIndex = resourceVideoList.findIndex(item => item.resource_id === resourceId) const currentIndex = sectionVideoList.findIndex(item => item.resource_id === resourceId)
const next = resourceVideoList[currentIndex + 1] const next = sectionVideoList[currentIndex + 1]
next && changeResource(next) next && changeResource(next)
} }
} }
function onSeeked() {
console.log('onSeeked')
throttledFn && throttledFn.flush()
}
onUnmounted(() => {
throttledFn && throttledFn.cancel()
})
</script> </script>
<template> <template>
<AppVideoPlayer <div ref="playerWrapperRef" :style="resource ? `height: 510px` : ''">
:options="options" <div class="player-box" :class="{ 'is-pinned': !playerIsVisible }">
:src="src" <AppVideoPlayer
@ready="onReady" :options="options"
@timeupdate="onTimeUpdate" :src="src"
@ended="onEnded" @ready="onReady"
@loadeddata="onLoadedData" @loadeddata="onLoadedData"
height="510" @timeupdate="onTimeUpdate"
style="width: 100%" @seeked="onSeeked"
v-if="src" @ended="onEnded"
></AppVideoPlayer> style="width: 100%; height: 100%"
v-if="src"
></AppVideoPlayer>
</div>
</div>
<!-- 设置 --> <!-- 设置 -->
<teleport to=".vjs-control-bar" v-if="isReady"> <teleport to=".vjs-control-bar" v-if="isReady">
<el-popover trigger="hover" effect="dark" placement="top" :teleported="false" width="40px"> <el-popover trigger="hover" effect="dark" placement="top" :teleported="false" :show-arrow="false" width="40px">
<template #reference> <template #reference>
<button class="vjs-hd-control vjs-control vjs-button" type="button"> <button class="vjs-hd-control vjs-control vjs-button" type="button">
<span class="vjs-icon-hd"></span> <span class="vjs-icon-hd"></span>
...@@ -240,13 +262,13 @@ function onEnded() { ...@@ -240,13 +262,13 @@ function onEnded() {
v-for="(item, index) in currentPlayList" v-for="(item, index) in currentPlayList"
:key="index" :key="index"
:class="{ 'is-active': item.PlayURL === src.src }" :class="{ 'is-active': item.PlayURL === src.src }"
@click="changeDefinition(item)" @click="changeSrc(item)"
> >
{{ item.DefinitionName }} {{ item.DefinitionName }}
</li> </li>
</ul> </ul>
</el-popover> </el-popover>
<el-popover trigger="hover" effect="dark" placement="top" :teleported="false" width="140px"> <el-popover trigger="hover" effect="dark" placement="top" :teleported="false" :show-arrow="false" width="140px">
<template #reference> <template #reference>
<button class="vjs-cog-control vjs-control vjs-button" type="button"> <button class="vjs-cog-control vjs-control vjs-button" type="button">
<span class="vjs-icon-cog"></span> <span class="vjs-icon-cog"></span>
...@@ -260,7 +282,7 @@ function onEnded() { ...@@ -260,7 +282,7 @@ function onEnded() {
</teleport> </teleport>
<swiper :slidesPerView="'auto'" :spaceBetween="30"> <swiper :slidesPerView="'auto'" :spaceBetween="30">
<swiper-slide <swiper-slide
v-for="item in resourceVideoList" v-for="item in sectionVideoList"
:key="item.id" :key="item.id"
class="video-item" class="video-item"
:class="{ 'is-active': item.resource_id === resourceId }" :class="{ 'is-active': item.resource_id === resourceId }"
...@@ -273,10 +295,31 @@ function onEnded() { ...@@ -273,10 +295,31 @@ function onEnded() {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.player-box {
width: 100%;
height: 100%;
&.is-pinned {
position: fixed;
bottom: 40px;
right: 40px;
width: 400px;
height: 220px;
z-index: 9999;
.vjs-hd-control,
.vjs-cog-control,
:deep(.vjs-fullscreen-control) {
display: none;
}
}
}
.vjs-icon-hd, .vjs-icon-hd,
.vjs-icon-cog { .vjs-icon-cog {
font-size: 1.8em; font-size: 1.8em;
} }
:deep(.vjs-fullscreen-control) {
order: 1;
}
.video-item { .video-item {
position: relative; position: relative;
margin: 20px 0; margin: 20px 0;
...@@ -319,6 +362,9 @@ function onEnded() { ...@@ -319,6 +362,9 @@ function onEnded() {
padding: 0.2em 0; padding: 0.2em 0;
font-size: 1.2em; font-size: 1.2em;
line-height: 1.4em; line-height: 1.4em;
&:hover {
background: rgba(115, 133, 159, 0.5);
}
&.is-active { &.is-active {
color: #2b333f; color: #2b333f;
background-color: #fff; background-color: #fff;
...@@ -343,8 +389,5 @@ function onEnded() { ...@@ -343,8 +389,5 @@ function onEnded() {
border-radius: 0; border-radius: 0;
padding: 0; padding: 0;
margin-bottom: -12px !important; margin-bottom: -12px !important;
.el-popper__arrow {
display: none;
}
} }
</style> </style>
<!-- 论坛 --> <!-- 论坛 -->
<script setup lang="ts"></script> <script setup lang="ts"></script>
<template>论坛</template> <template>
<el-empty description="暂无数据" />
</template>
...@@ -5,6 +5,8 @@ import ResourceIcon from '@/components/ResourceIcon.vue' ...@@ -5,6 +5,8 @@ import ResourceIcon from '@/components/ResourceIcon.vue'
import { Download } from '@element-plus/icons-vue' import { Download } from '@element-plus/icons-vue'
import { saveAs } from 'file-saver' import { saveAs } from 'file-saver'
import format from 'format-duration' import format from 'format-duration'
import { formatLiveDate, formatLiveStatus } from '@/utils/index'
import { getChapterTreeList, collectionResource } from '../api' import { getChapterTreeList, collectionResource } from '../api'
let chapterList = $ref<CourseChapterType[]>([]) let chapterList = $ref<CourseChapterType[]>([])
...@@ -87,7 +89,7 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha ...@@ -87,7 +89,7 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha
} }
</script> </script>
<template> <template>
<el-collapse class="course-chapters" v-model="chapterIds"> <el-collapse class="course-chapters" v-model="chapterIds" v-if="chapterList.length">
<el-collapse-item :name="item.id" v-for="item in chapterList" :key="item.id"> <el-collapse-item :name="item.id" v-for="item in chapterList" :key="item.id">
<template #title><i class="icon-chapter"></i>{{ item.name }}</template> <template #title><i class="icon-chapter"></i>{{ item.name }}</template>
<el-collapse class="course-sections" v-model="sectionIds"> <el-collapse class="course-sections" v-model="sectionIds">
...@@ -97,7 +99,7 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha ...@@ -97,7 +99,7 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha
<li class="course-resource-item" v-for="resource in section.resources" :key="resource.id"> <li class="course-resource-item" v-for="resource in section.resources" :key="resource.id">
<p> <p>
<router-link :to="targetUrl(resource, section, item)" target="_blank"> <router-link :to="targetUrl(resource, section, item)" target="_blank">
<ResourceIcon :resourceType="resource.resource_type" /> <ResourceIcon :resourceType="resource.resource_type" :info="resource.info" />
{{ resource.name }} {{ resource.name }}
</router-link> </router-link>
</p> </p>
...@@ -113,6 +115,11 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha ...@@ -113,6 +115,11 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha
<div class="video-duration" v-if="resource.resource_type === 2"> <div class="video-duration" v-if="resource.resource_type === 2">
{{ formatDuration(resource.info.length) }} {{ formatDuration(resource.info.length) }}
</div> </div>
<template v-if="resource.resource_type === 6 && resource.info">
<span>{{ formatLiveDate(resource.info.start_time) }}</span>
<span style="margin-left: 10px">{{ formatLiveStatus(resource.info.status) }}</span>
</template>
<div class="actions"> <div class="actions">
<i <i
class="icon-star" class="icon-star"
...@@ -130,6 +137,7 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha ...@@ -130,6 +137,7 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha
</el-collapse> </el-collapse>
</el-collapse-item> </el-collapse-item>
</el-collapse> </el-collapse>
<el-empty description="暂无数据" v-else />
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -142,6 +150,9 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha ...@@ -142,6 +150,9 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha
border-bottom: 1px solid #e6e6e6; border-bottom: 1px solid #e6e6e6;
p { p {
flex: 1; flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
} }
a { a {
&:hover { &:hover {
...@@ -153,11 +164,14 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha ...@@ -153,11 +164,14 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha
} }
.actions { .actions {
width: 60px; min-width: 60px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
line-height: 1; line-height: 1;
&:empty {
min-width: auto;
}
} }
} }
.icon-star { .icon-star {
......
...@@ -18,6 +18,9 @@ onMounted(() => { ...@@ -18,6 +18,9 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="course-exam"> <div class="course-exam">
<CourseViewExamItem v-for="item in list" :data="item" :key="item.id"></CourseViewExamItem> <template v-if="list.length">
<CourseViewExamItem v-for="item in list" :data="item" :key="item.id"></CourseViewExamItem>
</template>
<el-empty description="暂无数据" v-else />
</div> </div>
</template> </template>
...@@ -19,6 +19,9 @@ onMounted(() => { ...@@ -19,6 +19,9 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="course-live"> <div class="course-live">
<CourseViewLiveItem v-for="item in list" :data="item" :key="item.id"></CourseViewLiveItem> <template v-if="list.length">
<CourseViewLiveItem v-for="item in list" :data="item" :key="item.id"></CourseViewLiveItem>
</template>
<el-empty description="暂无数据" v-else />
</div> </div>
</template> </template>
...@@ -83,4 +83,4 @@ export interface PlayItemType { ...@@ -83,4 +83,4 @@ export interface PlayItemType {
StreamType: string StreamType: string
Width: number Width: number
DefinitionName: string DefinitionName: string
} }
\ No newline at end of file
<script setup lang="ts"> <script setup lang="ts">
import type { CourseResourceType, CourseChapterType } from '../types' import type { CourseResourceType, CourseChapterType } from '../types'
import { getCourseSection, getChapterTreeList } from '../api' import { getCourseSection, getChapterTreeList } from '../api'
import CoursePlayerResourceItem from '../components/CoursePlayerResourceItem.vue' import CoursePlayerResourceList from '../components/CoursePlayerResourceList.vue'
import { ArrowLeftBold, ArrowRightBold } from '@element-plus/icons-vue'
import type { VideoJsPlayer } from 'video.js'
const CoursePlayerVideo = defineAsyncComponent(() => import('../components/CoursePlayerVideo.vue')) const CoursePlayerVideo = defineAsyncComponent(() => import('../components/CoursePlayerVideo.vue'))
const CoursePlayerChapter = defineAsyncComponent(() => import('../components/CoursePlayerChapter.vue')) const CoursePlayerChapter = defineAsyncComponent(() => import('../components/CoursePlayerChapter.vue'))
...@@ -49,6 +51,21 @@ function fetchList() { ...@@ -49,6 +51,21 @@ function fetchList() {
onMounted(() => { onMounted(() => {
fetchList() fetchList()
}) })
const videoJsPlayer = $ref<VideoJsPlayer | null>()
provide('videoJsPlayer', $$(videoJsPlayer))
let currentVideoResource = $ref<CourseResourceType | undefined>()
const pptList = $computed(() => {
return currentVideoResource?.info?.ppt_arr ?? []
})
function onUpdateResource(resource: CourseResourceType | undefined) {
currentVideoResource = resource
}
let sidebarVisible = $ref<boolean>(true)
function toggleSidebar() {
sidebarVisible = !sidebarVisible
}
</script> </script>
<template> <template>
...@@ -58,54 +75,38 @@ onMounted(() => { ...@@ -58,54 +75,38 @@ onMounted(() => {
</div> </div>
<div class="course-player-bd"> <div class="course-player-bd">
<div class="course-player-main" v-loading="loading"> <div class="course-player-main" v-loading="loading">
<CoursePlayerVideo :chapterList="chapterList" :key="sectionId" /> <CoursePlayerVideo :chapterList="chapterList" :key="sectionId" @updateResource="onUpdateResource" />
<el-tabs> <el-tabs>
<el-tab-pane label="课件"> <el-tab-pane label="课件">
<CoursePlayerResourceItem <CoursePlayerResourceList :list="detail.coursewares" />
v-for="item in detail.coursewares"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="教案" lazy> <el-tab-pane label="教案" lazy>
<CoursePlayerResourceItem <CoursePlayerResourceList :list="detail.lesson_plans" />
v-for="item in detail.lesson_plans"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="作业" lazy> <el-tab-pane label="作业" lazy>
<CoursePlayerResourceItem <CoursePlayerResourceList :list="detail.jobs" />
v-for="item in detail.jobs"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="资料" lazy> <el-tab-pane label="资料" lazy>
<CoursePlayerResourceItem <CoursePlayerResourceList :list="detail.other_infos" />
v-for="item in detail.other_infos"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="考试/测验" lazy> <el-tab-pane label="考试/测验" lazy>
<CoursePlayerResourceItem <CoursePlayerResourceList :list="detail.exams" />
v-for="item in detail.exams"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="直播" lazy> <el-tab-pane label="直播" lazy>
<CoursePlayerResourceItem <CoursePlayerResourceList :list="detail.meetings" />
v-for="item in detail.meetings"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
<div class="course-player-aside"> <div class="course-player-aside" :class="{ 'is-hidden': !sidebarVisible }">
<CoursePlayerChapter :chapterList="chapterList" /> <div class="course-player-aside__inner">
<CoursePlayerChapter :chapterList="chapterList" :pptList="pptList" />
</div>
<div class="toggle-button" :class="{ 'is-hidden': !sidebarVisible }" @click="toggleSidebar">
<el-icon>
<ArrowRightBold v-if="sidebarVisible" />
<ArrowLeftBold v-else />
</el-icon>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -136,4 +137,43 @@ onMounted(() => { ...@@ -136,4 +137,43 @@ onMounted(() => {
line-height: 30px; line-height: 30px;
color: #333333; color: #333333;
} }
.course-player-aside {
position: relative;
width: 258px;
background-color: #1f1e24;
transition: width 0.3s ease-in-out;
&.is-hidden {
width: 0;
}
}
.course-player-aside__inner {
width: 100%;
height: 100%;
overflow: hidden;
}
.toggle-button {
position: absolute;
top: 238px;
left: -17px;
width: 34px;
height: 34px;
font-size: 14px;
border-radius: 50%;
background: #303133;
font-size: 14px;
color: #fff;
padding: 5px;
display: flex;
align-items: center;
justify-content: flex-end;
// 右半圆
clip: rect(0px 34px 34px 17px);
box-sizing: border-box;
cursor: pointer;
&.is-hidden {
// 左半圆
clip: rect(0px 17px 34px 0px);
justify-content: flex-start;
}
}
</style> </style>
...@@ -11,6 +11,7 @@ export interface CollectionType { ...@@ -11,6 +11,7 @@ export interface CollectionType {
source_id: string source_id: string
status: number status: number
type: number type: number
resource_type: number
} }
export interface CollectionSemesterType { export interface CollectionSemesterType {
......
...@@ -103,7 +103,7 @@ function targetUrl(item: CollectionType) { ...@@ -103,7 +103,7 @@ function targetUrl(item: CollectionType) {
<li class="collection-item" v-for="item in list" :key="item.id"> <li class="collection-item" v-for="item in list" :key="item.id">
<p> <p>
<router-link :to="targetUrl(item)" target="_blank"> <router-link :to="targetUrl(item)" target="_blank">
<ResourceIcon :resourceType="item.type" /> <ResourceIcon :resourceType="item.resource_type" :info="item.info" />
{{ item.info.name || item.info.paper_title }} {{ item.info.name || item.info.paper_title }}
</router-link> </router-link>
</p> </p>
......
...@@ -26,12 +26,20 @@ function handleUpdate() { ...@@ -26,12 +26,20 @@ function handleUpdate() {
</script> </script>
<template> <template>
<div>
<el-button
round
type="warning"
@click="dialogVisible = true"
style="width: 80px; margin-bottom: 20px; background-color: #d38846"
>创建</el-button
>
</div>
<el-tabs v-model="params.status"> <el-tabs v-model="params.status">
<el-tab-pane label="全部" name=""> </el-tab-pane> <el-tab-pane label="全部" name=""> </el-tab-pane>
<el-tab-pane label="待处理" name="1" lazy></el-tab-pane> <el-tab-pane label="待处理" name="1" lazy></el-tab-pane>
<el-tab-pane label="已处理" name="2" lazy></el-tab-pane> <el-tab-pane label="已处理" name="2" lazy></el-tab-pane>
</el-tabs> </el-tabs>
<el-button round type="warning" @click="dialogVisible = true" style="margin-bottom: 20px">创建</el-button>
<el-collapse v-if="dataset.list.length"> <el-collapse v-if="dataset.list.length">
<el-collapse-item v-for="item in dataset.list" :name="item.id" :key="item.id"> <el-collapse-item v-for="item in dataset.list" :name="item.id" :key="item.id">
<template #title> <template #title>
......
...@@ -25,7 +25,7 @@ function handleSubmit() { ...@@ -25,7 +25,7 @@ function handleSubmit() {
} }
// 修改 // 修改
const update = () => { const update = () => {
const params = Object.assign({}, form, { files: JSON.stringify(form.files) }) const params = Object.assign({}, form, { files: form.files.length ? JSON.stringify(form.files) : '' })
submitSuggestion(params).then(() => { submitSuggestion(params).then(() => {
ElMessage({ message: '提交成功', type: 'success' }) ElMessage({ message: '提交成功', type: 'success' })
emit('update') emit('update')
...@@ -41,7 +41,7 @@ const update = () => { ...@@ -41,7 +41,7 @@ const update = () => {
<el-input v-model="form.title" /> <el-input v-model="form.title" />
</el-form-item> </el-form-item>
<el-form-item label="问题详情" prop="content"> <el-form-item label="问题详情" prop="content">
<AppEditor v-model="form.content" /> <AppEditor v-model="form.content" :height="300" />
</el-form-item> </el-form-item>
<el-form-item label="上传附件" prop="files"> <el-form-item label="上传附件" prop="files">
<AppUpload v-model="form.files" :limit="1"> <AppUpload v-model="form.files" :limit="1">
......
...@@ -51,7 +51,7 @@ const update = () => { ...@@ -51,7 +51,7 @@ const update = () => {
</AppUpload> </AppUpload>
</el-form-item> </el-form-item>
<el-form-item label="性别" prop="gender"> <el-form-item label="性别" prop="gender">
<el-radio-group v-model="form.gender" class="ml-4"> <el-radio-group v-model="form.gender" disabled>
<el-radio :label="1" size="large"></el-radio> <el-radio :label="1" size="large"></el-radio>
<el-radio :label="2" size="large"></el-radio> <el-radio :label="2" size="large"></el-radio>
</el-radio-group> </el-radio-group>
......
...@@ -83,7 +83,17 @@ export interface FileType { ...@@ -83,7 +83,17 @@ export interface FileType {
length?: number length?: number
size: number size: number
source_id: string source_id: string
ppt_arr?: PptType[]
} }
export interface PptType {
id: string
name: string
point: number
url: string
video_id: string
}
// 直播类型 // 直播类型
export interface LiveType { export interface LiveType {
end_time: number end_time: number
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论