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

chore: update

上级 ab2d3548
......@@ -10,11 +10,11 @@
"dependencies": {
"@element-plus/icons-vue": "^2.0.6",
"@tinymce/tinymce-vue": "^5.0.0",
"@vueuse/core": "^9.0.0",
"@vueuse/core": "^9.0.2",
"axios": "^0.27.2",
"blueimp-md5": "^2.19.0",
"dayjs": "^1.11.4",
"element-plus": "^2.2.11",
"element-plus": "^2.2.12",
"file-saver": "^2.0.5",
"format-duration": "^2.0.0",
"lodash-es": "^4.17.21",
......@@ -733,13 +733,13 @@
}
},
"node_modules/@vueuse/core": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.0.0.tgz",
"integrity": "sha512-hMMc2ajuVknkL7Z39JdP9gFFND2OgnDTSS5mmuinWGAE1Vxy1AwDvTHm3+juyk+GzJjYRAktnBIPy7Fq53iOnw==",
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.0.2.tgz",
"integrity": "sha512-kOIqaQPSs7OSByWg1ulEKRUJbsq3FmbJiUr0RhEKpt3O1Uhl4DrDj85DUbQBABVYgPvSaY6AE/fP3/FOcRIOoQ==",
"dependencies": {
"@types/web-bluetooth": "^0.0.15",
"@vueuse/metadata": "9.0.0",
"@vueuse/shared": "9.0.0",
"@vueuse/metadata": "9.0.2",
"@vueuse/shared": "9.0.2",
"vue-demi": "*"
},
"funding": {
......@@ -772,17 +772,17 @@
}
},
"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==",
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.0.2.tgz",
"integrity": "sha512-TRh+TNUYXiodatSAxd0xZc7sh4RfktVVgNFIN7TCQXKyancbCAcWfHvKfgdlX8LcqSBxKoHVa90n0XdUbboTkw==",
"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==",
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.0.2.tgz",
"integrity": "sha512-KwBDefK2ljLESpt0ffe2w8EGUCb3IaMfTzeytB/uHHjHOGOEIHLHHyn8W2C48uGQEvoe5iwaW4Bfp8cRUM6IFA==",
"dependencies": {
"vue-demi": "*"
},
......@@ -791,9 +791,9 @@
}
},
"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==",
"version": "0.13.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.6.tgz",
"integrity": "sha512-02NYpxgyGE2kKGegRPYlNQSL1UWfA/+JqvzhGCOYjhfbLWXU5QQX0+9pAm/R2sCOPKr5NBxVIab7fvFU0B1RxQ==",
"hasInstallScript": true,
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
......@@ -1458,9 +1458,9 @@
"dev": true
},
"node_modules/element-plus": {
"version": "2.2.11",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.2.11.tgz",
"integrity": "sha512-JjOvz5DLBc4Jp9OHKXNcK/Cys4NX5/vxpZ+gYmH2V+pLkwJnyIOrNZ3QxfdyG6yE4+NkpoA6koEgUB7T+0Z5vQ==",
"version": "2.2.12",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.2.12.tgz",
"integrity": "sha512-g/hIHj3b+dND2R3YRvyvCJtJhQvR7lWvXqhJaoxaQmajjNWedoe4rttxG26fOSv9YCC2wN4iFDcJHs70YFNgrA==",
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.0.6",
......@@ -5343,13 +5343,13 @@
"requires": {}
},
"@vueuse/core": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.0.0.tgz",
"integrity": "sha512-hMMc2ajuVknkL7Z39JdP9gFFND2OgnDTSS5mmuinWGAE1Vxy1AwDvTHm3+juyk+GzJjYRAktnBIPy7Fq53iOnw==",
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.0.2.tgz",
"integrity": "sha512-kOIqaQPSs7OSByWg1ulEKRUJbsq3FmbJiUr0RhEKpt3O1Uhl4DrDj85DUbQBABVYgPvSaY6AE/fP3/FOcRIOoQ==",
"requires": {
"@types/web-bluetooth": "^0.0.15",
"@vueuse/metadata": "9.0.0",
"@vueuse/shared": "9.0.0",
"@vueuse/metadata": "9.0.2",
"@vueuse/shared": "9.0.2",
"vue-demi": "*"
},
"dependencies": {
......@@ -5362,22 +5362,22 @@
}
},
"@vueuse/metadata": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.0.0.tgz",
"integrity": "sha512-79YVIsAP1bbWm5GdQuG7jDVF/9uuExzhkO0Sd4/TLuSfzH2uZOrHvGwy+ZNJHjbyRn3uf56rKINWLJdBuTLSqQ=="
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.0.2.tgz",
"integrity": "sha512-TRh+TNUYXiodatSAxd0xZc7sh4RfktVVgNFIN7TCQXKyancbCAcWfHvKfgdlX8LcqSBxKoHVa90n0XdUbboTkw=="
},
"@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==",
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.0.2.tgz",
"integrity": "sha512-KwBDefK2ljLESpt0ffe2w8EGUCb3IaMfTzeytB/uHHjHOGOEIHLHHyn8W2C48uGQEvoe5iwaW4Bfp8cRUM6IFA==",
"requires": {
"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==",
"version": "0.13.6",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.6.tgz",
"integrity": "sha512-02NYpxgyGE2kKGegRPYlNQSL1UWfA/+JqvzhGCOYjhfbLWXU5QQX0+9pAm/R2sCOPKr5NBxVIab7fvFU0B1RxQ==",
"requires": {}
}
}
......@@ -5890,9 +5890,9 @@
"dev": true
},
"element-plus": {
"version": "2.2.11",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.2.11.tgz",
"integrity": "sha512-JjOvz5DLBc4Jp9OHKXNcK/Cys4NX5/vxpZ+gYmH2V+pLkwJnyIOrNZ3QxfdyG6yE4+NkpoA6koEgUB7T+0Z5vQ==",
"version": "2.2.12",
"resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.2.12.tgz",
"integrity": "sha512-g/hIHj3b+dND2R3YRvyvCJtJhQvR7lWvXqhJaoxaQmajjNWedoe4rttxG26fOSv9YCC2wN4iFDcJHs70YFNgrA==",
"requires": {
"@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.0.6",
......
......@@ -15,11 +15,11 @@
"dependencies": {
"@element-plus/icons-vue": "^2.0.6",
"@tinymce/tinymce-vue": "^5.0.0",
"@vueuse/core": "^9.0.0",
"@vueuse/core": "^9.0.2",
"axios": "^0.27.2",
"blueimp-md5": "^2.19.0",
"dayjs": "^1.11.4",
"element-plus": "^2.2.11",
"element-plus": "^2.2.12",
"file-saver": "^2.0.5",
"format-duration": "^2.0.0",
"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">
interface Props {
resourceType: number
resourceType?: number
info?: any
}
const props = defineProps<Props>()
function show(fileType: string) {
return props.info?.type === fileType
}
defineProps<Props>()
</script>
<template>
<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_live.png" v-else-if="resourceType === 6" />
<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_ppt.png" v-else-if="resourceType === 10" />
<img src="@/assets/images/icon_rar.png" v-else-if="resourceType === 4" /> -->
</div>
</template>
......
......@@ -6,7 +6,7 @@ import { getSignature, uploadFile } from '@/api/base'
const props = defineProps({
height: {
type: Number,
default: 600
default: 400
}
})
......
......@@ -2,6 +2,9 @@
const DEFAULT_OPTIONS = {
controls: true,
autoplay: false,
controlBar: {
pictureInPictureToggle: false
},
// fluid: true,
responsive: true,
playbackRates: [0.5, 1, 1.5, 2]
......
......@@ -86,7 +86,9 @@ function handleClick(index: number) {
}
.question-num {
flex: 1;
padding-top: 20px;
margin: 20px 0;
overflow-y: auto;
overflow-x: hidden;
.tit {
font-size: 12px;
color: #999999;
......@@ -94,33 +96,26 @@ function handleClick(index: number) {
margin-bottom: 10px;
}
ul {
display: flex;
list-style: none;
padding: 0;
margin: 0;
flex-wrap: wrap;
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
}
}
.question-num-item {
cursor: pointer;
position: relative;
border-radius: 50px;
margin: 0 auto;
width: 24px;
height: 24px;
font-size: 14px;
line-height: 24px;
margin-right: 20px;
margin-bottom: 10px;
color: #666;
text-align: center;
border: 2px solid #ccc;
color: #666;
&:nth-child(5n) {
margin-right: 0;
}
border-radius: 50px;
cursor: pointer;
}
.question-num-tips {
padding-bottom: 20px;
padding: 10px 0;
display: flex;
align-items: center;
justify-content: space-between;
......
......@@ -78,7 +78,9 @@ function handleClick(index: number) {
}
.question-num {
flex: 1;
padding-top: 20px;
margin: 20px 0;
overflow-y: auto;
overflow-x: hidden;
.tit {
font-size: 12px;
color: #999999;
......@@ -86,30 +88,23 @@ function handleClick(index: number) {
margin-bottom: 10px;
}
ul {
display: flex;
list-style: none;
padding: 0;
margin: 0;
flex-wrap: wrap;
display: grid;
grid-template-columns: repeat(10, 1fr);
gap: 10px;
}
}
.question-num-item {
cursor: pointer;
position: relative;
border-radius: 50px;
margin: 0 auto;
width: 24px;
height: 24px;
font-size: 14px;
line-height: 24px;
margin-right: 20px;
margin-bottom: 10px;
color: #666;
text-align: center;
border: 2px solid #ccc;
color: #666;
&:nth-child(10n) {
margin-right: 0;
}
border-radius: 50px;
cursor: pointer;
}
.question-num-tips {
padding: 0 20px 20px;
......@@ -147,7 +142,7 @@ function handleClick(index: number) {
}
.is-review {
color: #fff;
color: #fff !important;
background-color: blue;
border: 2px solid blue;
}
......
......@@ -188,6 +188,10 @@ const filterList = computed(() => {
.el-checkbox-group {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.el-checkbox-button {
margin-bottom: 10px;
}
.el-checkbox-button__inner {
padding: 10px 20px;
......
<!-- 学习 -->
<script setup lang="ts">
import format from 'format-duration'
import type { PptType } from '@/types'
import type { CourseChapterType } from '../types'
interface Props {
chapterList: CourseChapterType[]
pptList: PptType[]
}
defineProps<Props>()
const props = defineProps<Props>()
const route = useRoute()
let courseId = $ref<string>('')
......@@ -15,12 +19,25 @@ watchEffect(() => {
sectionId = route.query.section_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>
<template>
<div class="course-player-chapter">
<el-tabs>
<el-tab-pane label="章节">
<el-tabs v-model="activeTab">
<el-tab-pane label="章节" name="chapter">
<dl v-for="(item, index) in chapterList" :key="item.id">
<dt>
<span>{{ index + 1 }}</span>
......@@ -35,14 +52,20 @@ watchEffect(() => {
</dd>
</dl>
</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>
</div>
</template>
<style lang="scss" scoped>
.course-player-chapter {
width: 258px;
height: 100%;
padding: 20px;
background-color: #1f1e24;
......@@ -84,6 +107,7 @@ watchEffect(() => {
border-radius: 50%;
}
p {
flex: 1;
margin-left: 8px;
font-size: 16px;
font-weight: 500;
......@@ -93,7 +117,7 @@ watchEffect(() => {
}
dd {
margin-left: 34px;
padding: 5px 0;
padding: 5px 0 5px 1em;
font-size: 14px;
font-weight: 400;
line-height: 24px;
......@@ -102,5 +126,29 @@ watchEffect(() => {
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>
<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>()
// 跳转链接
const targetUrl = computed(() => {
const info = props.data.info
const info = props.data.info || {}
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}`
} else if (props.data.resource_type === 6) {
......@@ -71,7 +71,7 @@ function downloadFile(data: CourseResourceType) {
<div class="course-resource-item">
<p>
<a :href="targetUrl" target="_blank">
<ResourceIcon :resourceType="data.resource_type" />
<ResourceIcon :resourceType="data.resource_type" :info="data.info" />
{{ data.name }}
</a>
</p>
......@@ -83,7 +83,7 @@ function downloadFile(data: CourseResourceType) {
>
<el-button round size="small">查看报告</el-button>
</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 style="margin-left: 10px">{{ formatLiveStatus(data.info.status) }}</span>
</template>
......
......@@ -2,7 +2,7 @@
import type { CourseChapterType, CourseResourceType, VideoRecordType, PlayItemType } from '../types'
import type { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js'
import { throttle } from 'lodash'
import { useStorage } from '@vueuse/core'
import { useStorage, useElementVisibility } from '@vueuse/core'
import { Swiper, SwiperSlide } from 'swiper/vue'
import 'swiper/css'
import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue'
......@@ -13,6 +13,9 @@ interface Props {
}
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'updateResource', resource: CourseResourceType | undefined): void
}>()
const options = $ref<VideoJsPlayerOptions>()
......@@ -30,39 +33,31 @@ watchEffect(() => {
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 }>()
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 skipTime = $ref<number>(6)
// 连续播放
const isAutoPlayNext = $ref(useStorage('isAutoPlayNext', false))
// 播放器ready
let isReady = $ref<boolean>(false)
let videoJsPlayer = $ref<VideoJsPlayer | null>()
let videoJsPlayer = $ref<VideoJsPlayer | null>(inject('videoJsPlayer'))
const onReady = (player: VideoJsPlayer) => {
isReady = true
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(() => {
return props.chapterList.find(item => item.id === chapterId)
......@@ -72,23 +67,34 @@ const section = $computed(() => {
return chapter?.sections.find(item => item.id === sectionId)
})
// 当前节
const resource = $computed(() => {
return section?.resources.find(item => item.resource_id === resourceId && item.resource_type === 2)
})
// 资源视频列表
const resourceVideoList = $computed<CourseResourceType[]>(() => {
// 当前节下的视频列表
const sectionVideoList = $computed<CourseResourceType[]>(() => {
const list = section?.resources ?? []
return list.filter(item => item.resource_type === 2)
})
watchEffect(() => {
if (resourceVideoList.length) {
const found = resourceVideoList.find(item => item.resource_id === resourceId)
resourceId = found ? resourceId : resourceVideoList[0]?.resource_id
if (sectionVideoList.length) {
const found = sectionVideoList.find(item => item.resource_id === resourceId)
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>({
cumulative_playing_time: '',
current_playing_time: 0,
......@@ -141,6 +147,10 @@ watchEffect(async () => {
function setVideoInfo() {
if (!videoJsPlayer) return
// 上次播放结束,设置进度为0
if (videoJsPlayer.duration() - progress.current_playing_time < 1) {
progress.current_playing_time = 0
}
// 统计,增加默认时间10秒
progress.valid_playing_time = progress.valid_playing_time || 10
// 跳过片头
......@@ -170,7 +180,7 @@ const throttledFn = throttle(
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(',')
cumulative_playing_time: progress.watchedTimePoint.join(',') || progress.current_playing_time.toString()
})
// 清空已经上传过的观看时间点
progress.watchedTimePoint = []
......@@ -206,30 +216,42 @@ function onTimeUpdate() {
// 播放结束
function onEnded() {
console.log('onEnd')
throttledFn && throttledFn.flush()
// 自动播放下一个视频
if (isAutoPlayNext) {
const currentIndex = resourceVideoList.findIndex(item => item.resource_id === resourceId)
const next = resourceVideoList[currentIndex + 1]
const currentIndex = sectionVideoList.findIndex(item => item.resource_id === resourceId)
const next = sectionVideoList[currentIndex + 1]
next && changeResource(next)
}
}
function onSeeked() {
console.log('onSeeked')
throttledFn && throttledFn.flush()
}
onUnmounted(() => {
throttledFn && throttledFn.cancel()
})
</script>
<template>
<AppVideoPlayer
:options="options"
:src="src"
@ready="onReady"
@timeupdate="onTimeUpdate"
@ended="onEnded"
@loadeddata="onLoadedData"
height="510"
style="width: 100%"
v-if="src"
></AppVideoPlayer>
<div ref="playerWrapperRef" :style="resource ? `height: 510px` : ''">
<div class="player-box" :class="{ 'is-pinned': !playerIsVisible }">
<AppVideoPlayer
:options="options"
:src="src"
@ready="onReady"
@loadeddata="onLoadedData"
@timeupdate="onTimeUpdate"
@seeked="onSeeked"
@ended="onEnded"
style="width: 100%; height: 100%"
v-if="src"
></AppVideoPlayer>
</div>
</div>
<!-- 设置 -->
<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>
<button class="vjs-hd-control vjs-control vjs-button" type="button">
<span class="vjs-icon-hd"></span>
......@@ -240,13 +262,13 @@ function onEnded() {
v-for="(item, index) in currentPlayList"
:key="index"
:class="{ 'is-active': item.PlayURL === src.src }"
@click="changeDefinition(item)"
@click="changeSrc(item)"
>
{{ item.DefinitionName }}
</li>
</ul>
</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>
<button class="vjs-cog-control vjs-control vjs-button" type="button">
<span class="vjs-icon-cog"></span>
......@@ -260,7 +282,7 @@ function onEnded() {
</teleport>
<swiper :slidesPerView="'auto'" :spaceBetween="30">
<swiper-slide
v-for="item in resourceVideoList"
v-for="item in sectionVideoList"
:key="item.id"
class="video-item"
:class="{ 'is-active': item.resource_id === resourceId }"
......@@ -273,10 +295,31 @@ function onEnded() {
</template>
<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-cog {
font-size: 1.8em;
}
:deep(.vjs-fullscreen-control) {
order: 1;
}
.video-item {
position: relative;
margin: 20px 0;
......@@ -319,6 +362,9 @@ function onEnded() {
padding: 0.2em 0;
font-size: 1.2em;
line-height: 1.4em;
&:hover {
background: rgba(115, 133, 159, 0.5);
}
&.is-active {
color: #2b333f;
background-color: #fff;
......@@ -343,8 +389,5 @@ function onEnded() {
border-radius: 0;
padding: 0;
margin-bottom: -12px !important;
.el-popper__arrow {
display: none;
}
}
</style>
<!-- 论坛 -->
<script setup lang="ts"></script>
<template>论坛</template>
<template>
<el-empty description="暂无数据" />
</template>
......@@ -5,6 +5,8 @@ import ResourceIcon from '@/components/ResourceIcon.vue'
import { Download } from '@element-plus/icons-vue'
import { saveAs } from 'file-saver'
import format from 'format-duration'
import { formatLiveDate, formatLiveStatus } from '@/utils/index'
import { getChapterTreeList, collectionResource } from '../api'
let chapterList = $ref<CourseChapterType[]>([])
......@@ -87,7 +89,7 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha
}
</script>
<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">
<template #title><i class="icon-chapter"></i>{{ item.name }}</template>
<el-collapse class="course-sections" v-model="sectionIds">
......@@ -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">
<p>
<router-link :to="targetUrl(resource, section, item)" target="_blank">
<ResourceIcon :resourceType="resource.resource_type" />
<ResourceIcon :resourceType="resource.resource_type" :info="resource.info" />
{{ resource.name }}
</router-link>
</p>
......@@ -113,6 +115,11 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha
<div class="video-duration" v-if="resource.resource_type === 2">
{{ formatDuration(resource.info.length) }}
</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">
<i
class="icon-star"
......@@ -130,6 +137,7 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha
</el-collapse>
</el-collapse-item>
</el-collapse>
<el-empty description="暂无数据" v-else />
</template>
<style lang="scss" scoped>
......@@ -142,6 +150,9 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha
border-bottom: 1px solid #e6e6e6;
p {
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
a {
&:hover {
......@@ -153,11 +164,14 @@ function targetUrl(resource: CourseResourceType, section: CourseSectionType, cha
}
.actions {
width: 60px;
min-width: 60px;
display: flex;
align-items: center;
justify-content: flex-end;
line-height: 1;
&:empty {
min-width: auto;
}
}
}
.icon-star {
......
......@@ -18,6 +18,9 @@ onMounted(() => {
</script>
<template>
<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>
</template>
......@@ -19,6 +19,9 @@ onMounted(() => {
</script>
<template>
<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>
</template>
......@@ -83,4 +83,4 @@ export interface PlayItemType {
StreamType: string
Width: number
DefinitionName: string
}
}
\ No newline at end of file
<script setup lang="ts">
import type { CourseResourceType, CourseChapterType } from '../types'
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 CoursePlayerChapter = defineAsyncComponent(() => import('../components/CoursePlayerChapter.vue'))
......@@ -49,6 +51,21 @@ function fetchList() {
onMounted(() => {
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>
<template>
......@@ -58,54 +75,38 @@ onMounted(() => {
</div>
<div class="course-player-bd">
<div class="course-player-main" v-loading="loading">
<CoursePlayerVideo :chapterList="chapterList" :key="sectionId" />
<CoursePlayerVideo :chapterList="chapterList" :key="sectionId" @updateResource="onUpdateResource" />
<el-tabs>
<el-tab-pane label="课件">
<CoursePlayerResourceItem
v-for="item in detail.coursewares"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
<CoursePlayerResourceList :list="detail.coursewares" />
</el-tab-pane>
<el-tab-pane label="教案" lazy>
<CoursePlayerResourceItem
v-for="item in detail.lesson_plans"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
<CoursePlayerResourceList :list="detail.lesson_plans" />
</el-tab-pane>
<el-tab-pane label="作业" lazy>
<CoursePlayerResourceItem
v-for="item in detail.jobs"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
<CoursePlayerResourceList :list="detail.jobs" />
</el-tab-pane>
<el-tab-pane label="资料" lazy>
<CoursePlayerResourceItem
v-for="item in detail.other_infos"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
<CoursePlayerResourceList :list="detail.other_infos" />
</el-tab-pane>
<el-tab-pane label="考试/测验" lazy>
<CoursePlayerResourceItem
v-for="item in detail.exams"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
<CoursePlayerResourceList :list="detail.exams" />
</el-tab-pane>
<el-tab-pane label="直播" lazy>
<CoursePlayerResourceItem
v-for="item in detail.meetings"
:data="item"
:key="item.id"
></CoursePlayerResourceItem>
<CoursePlayerResourceList :list="detail.meetings" />
</el-tab-pane>
</el-tabs>
</div>
<div class="course-player-aside">
<CoursePlayerChapter :chapterList="chapterList" />
<div class="course-player-aside" :class="{ 'is-hidden': !sidebarVisible }">
<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>
......@@ -136,4 +137,43 @@ onMounted(() => {
line-height: 30px;
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>
......@@ -11,6 +11,7 @@ export interface CollectionType {
source_id: string
status: number
type: number
resource_type: number
}
export interface CollectionSemesterType {
......
......@@ -103,7 +103,7 @@ function targetUrl(item: CollectionType) {
<li class="collection-item" v-for="item in list" :key="item.id">
<p>
<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 }}
</router-link>
</p>
......
......@@ -26,12 +26,20 @@ function handleUpdate() {
</script>
<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-tab-pane label="全部" name=""> </el-tab-pane>
<el-tab-pane label="待处理" name="1" lazy></el-tab-pane>
<el-tab-pane label="已处理" name="2" lazy></el-tab-pane>
</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-item v-for="item in dataset.list" :name="item.id" :key="item.id">
<template #title>
......
......@@ -25,7 +25,7 @@ function handleSubmit() {
}
// 修改
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(() => {
ElMessage({ message: '提交成功', type: 'success' })
emit('update')
......@@ -41,7 +41,7 @@ const update = () => {
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="问题详情" prop="content">
<AppEditor v-model="form.content" />
<AppEditor v-model="form.content" :height="300" />
</el-form-item>
<el-form-item label="上传附件" prop="files">
<AppUpload v-model="form.files" :limit="1">
......
......@@ -51,7 +51,7 @@ const update = () => {
</AppUpload>
</el-form-item>
<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="2" size="large"></el-radio>
</el-radio-group>
......
......@@ -83,7 +83,17 @@ export interface FileType {
length?: number
size: number
source_id: string
ppt_arr?: PptType[]
}
export interface PptType {
id: string
name: string
point: number
url: string
video_id: string
}
// 直播类型
export interface LiveType {
end_time: number
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论