Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
L
learn-online-pc
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
EzijingWeb
learn-online-pc
Commits
506bd6c8
提交
506bd6c8
authored
5月 08, 2020
作者:
王鹏飞
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
update viewer module
上级
fbf2f920
隐藏空白字符变更
内嵌
并排
正在显示
20 个修改的文件
包含
1457 行增加
和
143 行删除
+1457
-143
index.js
client/src/modules/viewer/api/index.js
+58
-3
chapter.vue
client/src/modules/viewer/components/aside/chapter.vue
+41
-16
index.vue
client/src/modules/viewer/components/aside/index.vue
+11
-17
lecture.vue
client/src/modules/viewer/components/aside/lecture.vue
+18
-11
filePanel.vue
client/src/modules/viewer/components/file/filePanel.vue
+68
-0
index.vue
client/src/modules/viewer/components/file/index.vue
+35
-0
layout.vue
client/src/modules/viewer/components/layout.vue
+37
-0
index.vue
client/src/modules/viewer/components/player/index.vue
+76
-16
videoPlayer.vue
client/src/modules/viewer/components/player/videoPlayer.vue
+8
-6
chapterExam.vue
client/src/modules/viewer/components/work/chapterExam.vue
+232
-0
chapterExamItem.vue
...nt/src/modules/viewer/components/work/chapterExamItem.vue
+228
-0
chapterWork.vue
client/src/modules/viewer/components/work/chapterWork.vue
+12
-0
courseWork.vue
client/src/modules/viewer/components/work/courseWork.vue
+223
-0
editor.vue
client/src/modules/viewer/components/work/editor.vue
+135
-0
index.vue
client/src/modules/viewer/components/work/index.vue
+36
-0
upload.vue
client/src/modules/viewer/components/work/upload.vue
+80
-0
index.vue
client/src/modules/viewer/index.vue
+142
-52
routes.js
client/src/modules/viewer/routes.js
+13
-0
routes.js
client/src/router/routes.js
+4
-1
package-lock.json
package-lock.json
+0
-21
没有找到文件。
client/src/modules/viewer/api/index.js
浏览文件 @
506bd6c8
import
BaseAPI
from
'@/api/base_api'
const
httpRequest
=
new
BaseAPI
(
webConf
)
/**
* 获取课程详情
* @param {string} courseId 课程ID
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/
export
function
getCourse
(
courseId
,
semester
Id
)
{
return
httpRequest
.
get
(
`/v2/education/courses/
${
courseId
}
/
${
semester
Id
}
`
)
export
function
getCourse
(
semesterId
,
course
Id
)
{
return
httpRequest
.
get
(
`/v2/education/courses/
${
semesterId
}
/
${
course
Id
}
`
)
}
/**
...
...
@@ -33,3 +34,57 @@ export function getChapterVideoAliyun(vid) {
{
headers
:
{
'Content-Type'
:
'application/json'
}
}
)
}
/**
* 获取答题信息
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
* @param {string} resourseId 章节的资源ID
*/
export
function
getChapterExam
(
semesterId
,
courseId
,
resourseId
)
{
return
httpRequest
.
get
(
`/v2/education/homeworks/
${
semesterId
}
/
${
courseId
}
/
${
resourseId
}
`
)
}
/**
* 提交考试
*/
export
function
sbumitChapterExam
(
params
)
{
return
httpRequest
.
post
(
'/v2/education/homeworks'
,
params
,
{
headers
:
{
'Content-Type'
:
'application/json'
}
})
}
/**
* 上传文件
*/
export
function
uploadFile
(
data
)
{
return
httpRequest
.
post
(
'/util/upload-file'
,
data
,
{
headers
:
{
'Content-Type'
:
'multipart/form-data'
}
})
}
/**
* 获取课程大作业详情
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/
export
function
getCourseWork
(
semesterId
,
courseId
)
{
return
httpRequest
.
get
(
`/v2/education/courses/
${
semesterId
}
/
${
courseId
}
/essay`
)
}
/**
* 提交课程大作业
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/
export
function
updateCourseWork
(
semesterId
,
courseId
,
data
)
{
return
httpRequest
.
post
(
`/v2/education/courses/
${
semesterId
}
/
${
courseId
}
/essay`
,
data
,
{
headers
:
{
'Content-Type'
:
'multipart/form-data'
}
}
)
}
client/src/modules/viewer/components/aside/
asideC
hapter.vue
→
client/src/modules/viewer/components/aside/
c
hapter.vue
浏览文件 @
506bd6c8
<
template
>
<ul
class=
"chapter-list"
>
<li
class=
"chapter-item"
v-for=
"item in
list
"
:key=
"item.id"
>
<li
class=
"chapter-item"
v-for=
"item in
data
"
:key=
"item.id"
>
<h4>
{{
item
.
name
}}
</h4>
<ul
class=
"knot-list"
>
<li
v-for=
"subItem in item.children"
:key=
"subItem.id"
@
click=
"onClick(subItem)"
>
<span
class=
"knot-name"
>
{{
subItem
.
name
|
showName
(
subItem
.
type
)
}}
</span>
<ul
class=
"chapter-item-list"
>
<li
v-for=
"subItem in item.children"
:key=
"subItem.id"
@
click=
"onClick(subItem)"
:class=
"
{'is-active': subItem.id === (active ? active.id : '')}"
>
<span
class=
"chapter-item-list__name"
>
{{
subItem
.
name
|
showName
(
subItem
.
type
)
}}
</span>
<i
class=
"el-icon"
:class=
"genIconClass(subItem.type)"
></i>
</li>
</ul>
</li>
...
...
@@ -14,12 +20,9 @@
<
script
>
export
default
{
props
:
{
data
:
{
type
:
Array
,
default
()
{
return
[]
}
}
data
:
{
type
:
Array
,
default
:
()
=>
[]
},
// 当前选中的章节
active
:
{
type
:
Object
,
default
:
()
=>
{}
}
},
data
()
{
return
{
...
...
@@ -46,8 +49,29 @@ export default {
}
},
methods
:
{
genIconClass
(
type
)
{
const
map
=
{
2
:
'el-icon-self-iconset0481'
,
3
:
'el-icon-edit-outline'
,
4
:
'el-icon-self-cc-book'
}
return
map
[
type
]
||
'el-icon-self-cc-book'
},
onClick
(
data
)
{
console
.
log
(
data
)
// 课程大作业
// if (data.id === 'course_work') {
// this.$router.push({ name: 'viewerCourseWork' })
// return
// }
// 课程资料
// if (data.id === 'course_info') {
// this.$router.push({ name: 'viewerCourseFile' })
// return
// }
this
.
$router
.
push
({
name
:
'viewerCourseChapter'
,
params
:
{
id
:
data
.
id
}
})
}
}
}
...
...
@@ -62,23 +86,23 @@ export default {
overflow
:
hidden
;
.chapter-item
{
h4
{
padding
:
10px
3
2px
;
padding
:
10px
2
2px
;
margin
:
0
;
font-size
:
15px
;
color
:
#b0b0b0
;
background-color
:
#2f2f2f
;
}
/* 节列表样式 */
.
knot
-list
{
.
chapter-item
-list
{
margin
:
0
;
padding
:
0
;
line-height
:
1
.6
;
overflow
:
hidden
;
li
{
position
:
relative
;
&
.
on
{
&
.
is-active
{
background
:
#3c3c3c
;
a
{
.chapter-item-list__name
{
color
:
#b49441
;
}
}
...
...
@@ -110,7 +134,7 @@ export default {
background
:
#616161
;
}
}
.
knot-
name
{
.
chapter-item-list__
name
{
display
:
block
;
padding
:
15px
35px
15px
40px
;
font-size
:
14px
;
...
...
@@ -126,6 +150,7 @@ export default {
right
:
10px
;
top
:
50%
;
transform
:
translateY
(
-50%
);
color
:
#a0a0a0
;
}
}
}
...
...
client/src/modules/viewer/components/aside/
aside
.vue
→
client/src/modules/viewer/components/aside/
index
.vue
浏览文件 @
506bd6c8
...
...
@@ -3,12 +3,12 @@
<el-tabs
v-model=
"activeName"
>
<el-tab-pane
label=
"章节"
name=
"0"
>
<div
class=
"tab-pane"
>
<aside-chapter
:data=
"chapters"
></aside-chapter>
<aside-chapter
:data=
"chapters"
:active=
"active"
></aside-chapter>
</div>
</el-tab-pane>
<el-tab-pane
label=
"讲义"
name=
"1"
>
<el-tab-pane
label=
"讲义"
name=
"1"
v-if=
"active && active.type === 2"
>
<div
class=
"tab-pane"
>
<aside-lecture
:data=
"ppts"
></aside-lecture>
<aside-lecture
:data=
"ppts"
:pptIndex=
"pptIndex"
v-on=
"$listeners"
></aside-lecture>
</div>
</el-tab-pane>
</el-tabs>
...
...
@@ -16,25 +16,19 @@
</
template
>
<
script
>
import
AsideChapter
from
'./
asideC
hapter.vue'
import
AsideLecture
from
'./
asideL
ecture.vue'
import
AsideChapter
from
'./
c
hapter.vue'
import
AsideLecture
from
'./
l
ecture.vue'
export
default
{
props
:
{
// 章节
chapters
:
{
type
:
Array
,
default
()
{
return
[]
}
},
chapters
:
{
type
:
Array
,
default
:
()
=>
[]
},
// 讲义
ppts
:
{
type
:
Array
,
default
()
{
return
[]
}
}
ppts
:
{
type
:
Array
,
default
:
()
=>
[]
},
// 当前选中的章节
active
:
{
type
:
Object
,
default
:
()
=>
{}
},
// 当前选择的PPT
pptIndex
:
{
type
:
Number
,
default
:
0
}
},
components
:
{
AsideChapter
,
AsideLecture
},
data
()
{
...
...
client/src/modules/viewer/components/aside/
asideL
ecture.vue
→
client/src/modules/viewer/components/aside/
l
ecture.vue
浏览文件 @
506bd6c8
<
template
>
<ul
class=
"lecture-list"
>
<li
v-for=
"item in data"
:key=
"item.id"
@
click=
"onClick(item)"
>
<li
v-for=
"(item, index) in data"
:key=
"item.id"
@
click=
"onClick(index)"
:class=
"
{'is-active': index === activeIndex}"
>
<img
:src=
"item.ppt_url"
/>
</li>
</ul>
...
...
@@ -9,23 +14,25 @@
<
script
>
export
default
{
props
:
{
data
:
{
type
:
Array
,
default
()
{
return
[]
}
}
// 当前选择的PPT
pptIndex
:
{
type
:
Number
,
default
:
0
},
data
:
{
type
:
Array
,
default
:
()
=>
[]
}
},
data
()
{
return
{
activeIndex
:
0
activeIndex
:
this
.
pptIndex
}
},
watch
:
{
pptIndex
(
index
)
{
this
.
activeIndex
=
index
}
},
methods
:
{
// 点击PPT
onClick
(
data
)
{
this
.
activeIndex
=
data
.
id
this
.
$emit
(
'c
lickPPT'
,
data
)
onClick
(
index
)
{
this
.
activeIndex
=
index
this
.
$emit
(
'c
hange-ppt'
,
index
)
}
}
}
...
...
client/src/modules/viewer/components/file/filePanel.vue
0 → 100644
浏览文件 @
506bd6c8
<
template
>
<div
class=
"course-viewer-content"
>
<div
class=
"course-viewer-content-hd"
>
<h3
class=
"course-viewer-content-hd__title"
>
{{
title
}}
</h3>
</div>
<div
class=
"course-viewer-content-bd"
>
<ul
class=
"file-list"
v-if=
"files.length"
>
<li
class=
"file-list-item"
v-for=
"file in files"
:key=
"file.id"
>
<a
:href=
"file.file_url"
target=
"_blank"
>
<i
class=
"el-icon-document"
></i>
{{
file
.
file_name
}}
</a>
<span
v-if=
"file.file_size"
>
{{
file
.
file_size
}}
</span>
<a
:href=
"file.file_url"
:download=
"file.file_name"
target=
"_blank"
>
<el-tooltip
effect=
"dark"
content=
"下载"
>
<i
class=
"el-icon-download"
></i>
</el-tooltip>
</a>
</li>
</ul>
<div
class=
"empty"
v-else
>
<slot
name=
"empty"
>
暂无课程资料
</slot>
</div>
</div>
</div>
</
template
>
<
script
>
export
default
{
name
:
'FilePanel'
,
props
:
{
// 标题
title
:
{
type
:
String
,
default
:
'课程资料'
},
// 文件列表
files
:
{
type
:
Array
,
default
:
()
=>
[]
}
}
}
</
script
>
<
style
lang=
"scss"
scoped
>
.file-list
{
padding
:
0
;
}
.file-list-item
{
display
:
flex
;
font-size
:
16px
;
padding
:
20px
30px
;
margin-bottom
:
10px
;
background-color
:
#fff
;
list-style
:
none
;
border-radius
:
32px
;
justify-content
:
space-between
;
a
{
text-decoration
:
none
;
color
:
#333
;
&
:hover
{
color
:
#b49441
;
}
}
}
.empty
{
font-size
:
18px
;
line-height
:
80px
;
background-color
:
#fff
;
text-align
:
center
;
border-radius
:
40px
;
}
</
style
>
client/src/modules/viewer/components/file/index.vue
0 → 100644
浏览文件 @
506bd6c8
<
template
>
<file-panel
:title=
"chapter.name"
:files=
"files"
></file-panel>
</
template
>
<
script
>
import
FilePanel
from
'./filePanel.vue'
export
default
{
name
:
'ViewerFile'
,
components
:
{
FilePanel
},
props
:
{
// 当前选中的
chapter
:
{
type
:
Object
,
default
:
()
=>
{}
},
// 课程详情接口返回的数据
data
:
{
type
:
Object
,
default
:
()
=>
{}
}
},
computed
:
{
files
()
{
// 课程资料
if
(
this
.
chapter
.
id
===
'course_info'
)
{
return
this
.
data
.
files
||
[]
}
// 章节资料
if
(
this
.
chapter
.
reading
)
{
const
reading
=
this
.
chapter
.
reading
const
file
=
{
file_name
:
reading
.
reading_content
,
file_url
:
reading
.
reading_attachment
}
return
[
file
]
}
return
[]
}
}
}
</
script
>
client/src/modules/viewer/components/layout.vue
0 → 100644
浏览文件 @
506bd6c8
<
template
>
<component
:is=
"currentCompoent"
:chapter=
"chapter"
v-bind=
"$attrs"
v-on=
"$listeners"
v-if=
"chapter"
:key=
"pid"
/>
</
template
>
<
script
>
import
VPlayer
from
'./player/index.vue'
import
VWork
from
'./work/index.vue'
import
VFile
from
'./file/index.vue'
export
default
{
name
:
'ViewerLayout'
,
components
:
{
VPlayer
,
VWork
,
VFile
},
props
:
{
chapter
:
{
type
:
Object
,
default
:
()
=>
{}
}
},
computed
:
{
currentCompoent
()
{
const
componentNames
=
{
2
:
'VPlayer'
,
// 视频
3
:
'VWork'
,
// 作业
4
:
'VFile'
// 资料
}
return
this
.
chapter
?
componentNames
[
this
.
chapter
.
type
]
||
''
:
''
},
pid
()
{
return
this
.
$route
.
params
.
id
}
}
}
</
script
>
client/src/modules/viewer/components/player/
player
.vue
→
client/src/modules/viewer/components/player/
index
.vue
浏览文件 @
506bd6c8
<
template
>
<div
class=
"player"
>
<div
class=
"player"
v-if=
"chatperResources"
>
<div
class=
"player-main"
>
<div
class=
"player-column"
v-show=
"videoVisible"
>
<!-- 视频 -->
<video-player
:video=
"video"
></video-player>
<video-player
:isSkip=
"isSkip"
:video=
"chatperResources.video"
@
timeupdate=
"onTimeupdate"
ref=
"videoPlayer"
></video-player>
</div>
<div
class=
"player-column"
v-if=
"pptVisible"
>
<!-- ppt -->
<ppt-player
:ppts=
"ppts"
@
close=
"pptVisible = false"
@
fullscreen=
"onPPTFullscreen"
></ppt-player>
<ppt-player
:ppts=
"chatperResources.ppts"
@
close=
"pptVisible = false"
@
fullscreen=
"onPPTFullscreen"
></ppt-player>
</div>
</div>
<div
class=
"player-footer"
>
<em
class=
"player-button player-button-download"
v-if=
"pdf"
>
<a
:href=
"
pdf"
target=
"_blank"
>
下载PPT
</a>
<em
class=
"player-button player-button-download"
v-if=
"
chapter.
pdf"
>
<a
:href=
"
chapter.pdf"
download
target=
"_blank"
>
下载PPT
</a>
</em>
<em
:class=
"pptClass"
@
click=
"togglePPTVisible"
v-if=
"ppts.length"
>
同步显示PPT
</em>
<em
:class=
"pptClass"
@
click=
"togglePPTVisible"
v-if=
"
chatperResources.
ppts.length"
>
同步显示PPT
</em>
<em
:class=
"skipClass"
@
click=
"toggleSkip"
>
始终跳过片头
</em>
</div>
</div>
</
template
>
<
script
>
// api
import
*
as
api
from
'../../api/index'
// components
import
videoPlayer
from
'./videoPlayer.vue'
import
pptPlayer
from
'./pptPlayer.vue'
export
default
{
name
:
'Player'
,
name
:
'
Viewer
Player'
,
components
:
{
videoPlayer
,
pptPlayer
},
props
:
{
video
:
{
type
:
Object
},
pdf
:
{
type
:
String
},
ppts
:
{
type
:
Array
,
default
()
{
return
[]
}
}
// 当前章节
chapter
:
{
type
:
Object
},
// PPT当前选中的索引
pptIndex
:
{
type
:
Number
,
default
:
0
}
},
data
()
{
return
{
videoVisible
:
true
,
pptVisible
:
false
,
isSkip
:
false
isSkip
:
false
,
chatperResources
:
null
}
},
watch
:
{
pptIndex
(
index
)
{
this
.
updateVideoCurrentTime
(
index
)
}
},
computed
:
{
// 视频资源ID
resourceId
()
{
return
this
.
chapter
.
resource_id
},
/**
* 视频提供者
* @return 1是CC加密; 2是非加密; 3是阿里云
*/
videoProvider
()
{
const
video
=
this
.
chapter
.
video
||
{}
return
video
.
video_provider
||
3
},
pptClass
()
{
return
{
'player-button'
:
true
,
...
...
@@ -72,7 +98,40 @@ export default {
// PPT全屏
onPPTFullscreen
(
value
)
{
this
.
videoVisible
=
!
value
},
// 当前播放时间更新
onTimeupdate
(
time
)
{
const
ppts
=
this
.
chatperResources
.
ppts
||
[]
let
index
=
this
.
chatperResources
.
ppts
.
findIndex
(
item
=>
item
.
ppt_point
>
time
)
index
=
index
!==
-
1
?
index
-
1
:
ppts
.
length
-
1
this
.
$emit
(
'change-ppt'
,
index
)
},
// 更新视频当前播放时间
updateVideoCurrentTime
()
{
const
player
=
this
.
$refs
.
videoPlayer
.
player
const
ppt
=
this
.
chatperResources
.
ppts
[
this
.
pptIndex
]
ppt
&&
player
.
seek
(
ppt
.
ppt_point
)
// 增加2秒
},
// 获取章节视频详情
getChapterVideo
()
{
// 视频播放类型 1是CC加密; 2是非加密; 3是阿里云
if
(
this
.
videoProvider
===
3
)
{
api
.
getChapterVideoAliyun
(
this
.
resourceId
).
then
(
response
=>
{
this
.
chatperResources
=
response
Array
.
isArray
(
response
.
ppts
)
&&
this
.
$emit
(
'pptupdate'
,
response
.
ppts
)
})
}
else
{
api
.
getChapterVideo
(
this
.
resourceId
).
then
(
response
=>
{
this
.
chatperResources
=
response
Array
.
isArray
(
response
.
ppts
)
&&
this
.
$emit
(
'pptupdate'
,
response
.
ppts
)
})
}
}
},
beforeMount
()
{
this
.
getChapterVideo
()
}
}
</
script
>
...
...
@@ -83,6 +142,7 @@ export default {
flex-direction
:
column
;
width
:
100%
;
height
:
100%
;
background-color
:
#3f3f3f
;
}
.player-main
{
display
:
flex
;
...
...
client/src/modules/viewer/components/player/videoPlayer.vue
浏览文件 @
506bd6c8
...
...
@@ -5,12 +5,13 @@
<
script
>
export
default
{
name
:
'VideoPlayer'
,
props
:
{
video
:
Object
},
props
:
{
isSkip
:
Boolean
,
video
:
Object
},
data
()
{
return
{
player
:
null
}
},
methods
:
{
createPlayer
()
{
const
_this
=
this
const
{
FD
,
LD
,
SD
}
=
this
.
video
this
.
player
=
new
Aliplayer
(
{
...
...
@@ -29,16 +30,17 @@ export default {
]
},
function
(
player
)
{
console
.
log
(
'The player is created'
)
/* Register the sourceloaded of the player, query the resolution of the video, invoke the resolution component, and call the setCurrentQuality method to set the resolution. */
player
.
on
(
'sourceloaded'
,
function
(
params
)
{
var
paramData
=
params
.
paramData
var
desc
=
paramData
.
desc
var
definition
=
paramData
.
definition
const
paramData
=
params
.
paramData
const
desc
=
paramData
.
desc
const
definition
=
paramData
.
definition
player
.
getComponent
(
'QualityComponent'
)
.
setCurrentQuality
(
desc
,
definition
)
})
player
.
on
(
'timeupdate'
,
function
(
event
)
{
_this
.
$emit
(
'timeupdate'
,
player
.
getCurrentTime
())
})
}
)
}
...
...
client/src/modules/viewer/components/work/chapterExam.vue
0 → 100644
浏览文件 @
506bd6c8
<
template
>
<div
class=
"course-viewer-content"
v-loading=
"loading"
>
<div
class=
"course-viewer-content-hd"
>
<h3
class=
"course-viewer-content-hd__title"
>
{{
chapter
.
name
}}
</h3>
<div
class=
"course-viewer-content-hd__aside"
v-if=
"isSubmited"
>
正确率:
{{
detail
.
score
}}
%
</div>
</div>
<div
class=
"course-viewer-content-bd"
>
<div
class=
"exam"
>
<div
class=
"exam-form"
>
<el-form
:disabled=
"isSubmited"
size=
"medium"
>
<chapter-exam-item
v-for=
"(item, index) in unorderedQuestions"
:disabled=
"isSubmited"
:data=
"item"
:value=
"findAnswerById(item.id)"
:index=
"index"
:key=
"item.id"
@
change=
"onChange(item.id, ...arguments)"
></chapter-exam-item>
<div
class=
"exam-buttons"
>
<el-tooltip
effect=
"dark"
content=
"提交之后就不能修改了哦"
placement=
"right"
>
<el-button
type=
"primary"
@
click=
"onSubmit"
>
{{
submitText
}}
</el-button>
</el-tooltip>
</div>
</el-form>
</div>
</div>
</div>
</div>
</
template
>
<
script
>
// libs
import
{
shuffle
}
from
'lodash'
// components
import
ChapterExamItem
from
'./chapterExamItem.vue'
// api
import
*
as
api
from
'../../api/index'
export
default
{
name
:
'ChapterExam'
,
components
:
{
ChapterExamItem
},
props
:
{
// 当前选中的章节
chapter
:
{
type
:
Object
,
default
:
()
=>
{}
}
},
data
()
{
return
{
loading
:
false
,
detail
:
null
,
values
:
[],
// 提交的答案
startTime
:
new
Date
().
getTime
(),
// 进入时间
messageInstance
:
null
}
},
computed
:
{
// 学期ID
sid
()
{
return
'6552021107166150656'
},
// 课程ID
cid
()
{
return
'6568035374902280192'
},
// 当前页面的ID
pid
()
{
return
this
.
$route
.
params
.
id
},
// 资源ID
resourceId
()
{
return
this
.
chapter
.
resource_id
},
// 问题列表
questions
()
{
const
homework
=
this
.
chapter
.
homework
return
homework
?
homework
.
questions
:
[]
},
// 打乱顺序的问题列表
unorderedQuestions
()
{
const
ids
=
this
.
questions
.
map
(
item
=>
item
.
id
)
const
sortIds
=
shuffle
(
ids
)
return
sortIds
.
map
(
id
=>
this
.
questions
.
find
(
item
=>
item
.
id
===
id
))
},
/**
* 解析用户提交的数据
* @return [{{ question_id: 'xxx', value: ['xxx', 'xxx'] }}]
*/
answers
()
{
const
answers
=
this
.
isSubmited
?
JSON
.
parse
(
this
.
detail
.
work_contents
)
:
[]
return
answers
.
map
(
item
=>
{
const
ids
=
item
.
options
.
reduce
((
result
,
subitem
)
=>
{
subitem
.
selected
&&
result
.
push
(
subitem
.
id
)
return
result
},
[])
return
{
question_id
:
item
.
question_id
,
value
:
ids
}
})
},
// 是否提交
isSubmited
()
{
return
this
.
detail
?
!!
this
.
detail
.
work_contents
:
false
},
// 提交按钮文本
submitText
()
{
return
this
.
isSubmited
?
'已提交'
:
'提交'
}
},
methods
:
{
// 获取测试答题详情
getChapterExam
()
{
this
.
loading
=
true
api
.
getChapterExam
(
this
.
sid
,
this
.
cid
,
this
.
resourceId
)
.
then
(
response
=>
{
this
.
detail
=
Array
.
isArray
(
response
)
?
null
:
response
})
.
finally
(()
=>
{
this
.
loading
=
false
})
},
// 通过问题ID查找答案
findAnswerById
(
id
)
{
const
found
=
this
.
answers
.
find
(
item
=>
item
.
question_id
===
id
)
return
found
?
found
.
value
:
[]
},
onChange
(
qid
,
value
)
{
const
index
=
this
.
values
.
findIndex
(
item
=>
item
.
question_id
===
qid
)
if
(
index
===
-
1
)
{
this
.
values
.
push
({
question_id
:
qid
,
value
})
}
else
{
this
.
values
.
splice
(
index
,
1
,
{
question_id
:
qid
,
value
})
}
},
// 提交校验
checkSubmit
()
{
if
(
this
.
values
.
length
!==
this
.
questions
.
length
)
{
return
false
}
const
values
=
this
.
values
for
(
let
i
=
0
;
i
<
values
.
length
;
i
++
)
{
const
options
=
values
[
i
].
value
if
(
!
options
.
length
)
{
return
false
}
}
return
true
},
// 提交
onSubmit
()
{
if
(
!
this
.
checkSubmit
())
{
this
.
messageInstance
&&
this
.
messageInstance
.
close
()
this
.
messageInstance
=
this
.
$message
.
error
(
'还有题目未做,不能提交'
)
return
}
// 计算答题时间
const
duration
=
Math
.
floor
(
(
new
Date
().
getTime
()
-
this
.
startTime
)
/
1000
)
// 答案数据
const
data
=
this
.
handleSubmitData
()
// 计算分数
const
score
=
data
.
reduce
((
result
,
item
)
=>
{
item
.
is_correct
&&
result
++
return
result
},
0
)
const
total
=
this
.
questions
.
length
const
params
=
{
semester_id
:
this
.
sid
,
course_id
:
this
.
cid
,
chapter_id
:
this
.
pid
,
work_id
:
this
.
resourceId
,
work_contents
:
JSON
.
stringify
(
data
),
duration
,
score
:
((
score
/
total
)
*
100
).
toFixed
(
1
)
}
// 请求接口
this
.
handleSubmitRequest
(
params
)
},
// 提交的答案数据
handleSubmitData
()
{
const
result
=
this
.
questions
.
map
(
item
=>
{
// 查找提交的option id
const
found
=
this
.
values
.
find
(
subitem
=>
subitem
.
question_id
===
item
.
id
)
const
ids
=
found
?
found
.
value
:
[]
// 解析
const
parseOptions
=
JSON
.
parse
(
item
.
question_options
)
// 设置提交选中状态
let
isCorrect
=
true
const
options
=
parseOptions
.
map
(
option
=>
{
option
.
selected
=
ids
.
includes
(
option
.
id
)
?
1
:
0
if
(
option
.
checked
!==
!!
option
.
selected
&&
isCorrect
)
{
isCorrect
=
false
}
return
option
})
return
{
question_id
:
item
.
id
,
is_correct
:
isCorrect
?
1
:
0
,
options
}
})
return
result
},
// 请求提交接口
handleSubmitRequest
(
params
)
{
api
.
sbumitChapterExam
(
params
).
then
(
response
=>
{
if
(
response
.
status
)
{
this
.
getChapterExam
()
}
else
{
this
.
$message
.
error
(
response
.
data
.
error
)
}
})
}
},
beforeMount
()
{
this
.
getChapterExam
()
}
}
</
script
>
<
style
lang=
"scss"
scoped
>
.exam-buttons
{
padding
:
40px
0
;
text-align
:
center
;
.el-button
{
width
:
240px
;
margin
:
40px
auto
;
}
}
</
style
>
client/src/modules/viewer/components/work/chapterExamItem.vue
0 → 100644
浏览文件 @
506bd6c8
<
template
>
<div
class=
"q-item"
>
<div
class=
"q-item-hd"
>
<div
class=
"q-item-num"
>
{{
currentIndex
}}
.
</div>
<div
class=
"q-item-title"
v-html=
"data.question_content"
></div>
<div
class=
"q-item-aside"
>
(
{{
currentTypeText
}}
)
</div>
</div>
<div
class=
"q-item-bd"
>
<!-- 单选 -->
<el-radio-group
v-model=
"radioValue"
@
change=
"onRadioChange"
v-if=
"currentType === 1"
>
<div
class=
"q-option-item"
v-for=
"item in currentOptions"
:key=
"item.id"
>
<el-radio
:class=
"genClass(item)"
:label=
"item.id"
>
{{
item
.
abc_option
}}
</el-radio>
</div>
</el-radio-group>
<!-- 多选 -->
<el-checkbox-group
v-model=
"checkboxValue"
@
change=
"onCheckboxChange"
v-if=
"currentType === 2"
>
<div
class=
"q-option-item"
v-for=
"item in currentOptions"
:key=
"item.id"
>
<el-checkbox
:class=
"genClass(item)"
:label=
"item.id"
>
{{
item
.
abc_option
}}
</el-checkbox>
</div>
</el-checkbox-group>
</div>
<div
class=
"q-item-ft"
v-if=
"disabled"
>
<p>
<span>
学生答案:
</span>
<span
:class=
"isCorrect ? 'is-success' : 'is-error'"
>
{{
submitAnswerText
}}
</span>
</p>
<p>
<span>
正确答案:
</span>
<span>
{{
correctAnswerText
}}
</span>
</p>
</div>
</div>
</
template
>
<
script
>
export
default
{
name
:
'ChapterExamItem'
,
props
:
{
// 索引
index
:
{
type
:
Number
},
// 单条数据
data
:
{
type
:
Object
,
default
:
()
=>
{}
},
// 提交的答案
value
:
{
type
:
Array
,
default
:
()
=>
[]
},
// 是否禁用,提交过的是禁用状态
disabled
:
{
type
:
Boolean
,
default
:
false
}
},
data
()
{
return
{
radioValue
:
''
,
checkboxValue
:
[]
}
},
watch
:
{
value
:
{
immediate
:
true
,
handler
(
value
)
{
if
(
this
.
currentType
===
1
)
{
this
.
radioValue
=
value
[
0
]
||
''
}
else
{
this
.
checkboxValue
=
value
}
}
}
},
computed
:
{
// 26个英文字母
A_Z
()
{
const
result
=
[]
for
(
let
i
=
0
;
i
<
26
;
i
++
)
{
result
.
push
(
String
.
fromCharCode
(
65
+
i
))
}
return
result
},
// 序号
currentIndex
()
{
return
this
.
index
+
1
},
// 当前类型
currentType
()
{
return
this
.
data
.
question_type
},
// 选项类型
currentTypeText
()
{
const
map
=
{
1
:
'单选题'
,
2
:
'多选题'
}
return
map
[
this
.
currentType
]
},
// 接口返回的options数据
options
()
{
return
this
.
data
.
question_options
?
JSON
.
parse
(
this
.
data
.
question_options
)
:
[]
},
// 处理后的options数据
currentOptions
()
{
return
this
.
options
.
map
((
item
,
index
)
=>
{
// 英文字母 + 名称
item
.
abc
=
this
.
A_Z
[
index
]
item
.
abc_option
=
`
${
this
.
A_Z
[
index
]}
.
${
item
.
option
}
`
// 提交时的选中状态
item
.
selected
=
this
.
value
.
includes
(
item
.
id
)
return
item
})
},
// 正确答案显示的英文字母
correctAnswerText
()
{
const
result
=
this
.
currentOptions
.
reduce
((
result
,
item
)
=>
{
item
.
checked
&&
result
.
push
(
item
.
abc
)
return
result
},
[])
return
result
.
join
(
'、'
)
},
// 提交答案显示的英文字母
submitAnswerText
()
{
const
result
=
this
.
currentOptions
.
reduce
((
result
,
item
)
=>
{
item
.
selected
&&
result
.
push
(
item
.
abc
)
return
result
},
[])
return
result
.
join
(
'、'
)
},
// 是否回答正确
isCorrect
()
{
const
options
=
this
.
currentOptions
for
(
let
i
=
0
;
i
<
options
.
length
;
i
++
)
{
if
(
options
[
i
].
checked
!==
!!
options
[
i
].
selected
)
{
return
false
}
}
return
true
}
},
methods
:
{
// 生成class
genClass
(
item
)
{
if
(
!
this
.
disabled
)
{
return
null
}
return
{
'is-error'
:
item
.
selected
!==
item
.
checked
,
'is-success'
:
item
.
checked
}
},
// 单选
onRadioChange
(
value
)
{
this
.
$emit
(
'change'
,
[
value
])
},
// 多选
onCheckboxChange
(
value
)
{
this
.
$emit
(
'change'
,
value
)
}
}
}
</
script
>
<
style
lang=
"scss"
scoped
>
.q-item
{
font-size
:
16px
;
padding
:
10px
0
;
border-bottom
:
1px
solid
#c9c9c9
7a
;
}
.q-item-hd
{
display
:
flex
;
padding
:
10px
0
20px
;
::v-deep
p
{
margin
:
0
;
padding
:
0
;
}
}
.q-item-num
{
width
:
20px
;
text-align
:
center
;
}
.q-item-title
{
flex
:
1
;
padding
:
0
10px
;
}
.q-item-aside
{
padding-left
:
20px
;
// align-self: flex-end;
}
.q-option-item
{
padding-left
:
30px
;
margin-bottom
:
14px
;
}
.is-success
{
color
:
#090
;
}
.is-error
{
color
:
#d80000
;
}
::v-deep
.el-radio
{
&
.is-disabled
.el-radio__label
{
color
:
#3c3c3c
;
}
&
.is-error
.el-radio__label
{
color
:
#d80000
;
}
&
.is-success
.el-radio__label
{
color
:
#090
;
}
}
::v-deep
.el-checkbox
{
&
.is-disabled
.el-checkbox__label
{
color
:
#3c3c3c
;
}
&
.is-error
.el-checkbox__label
{
color
:
#d80000
;
}
&
.is-success
.el-checkbox__label
{
color
:
#090
;
}
}
.q-item-ft
{
display
:
flex
;
justify-content
:
flex-end
;
padding
:
10px
0
;
p
{
font-size
:
14px
;
margin
:
0
;
padding-left
:
20px
;
}
}
</
style
>
client/src/modules/viewer/components/work/chapterWork.vue
0 → 100644
浏览文件 @
506bd6c8
<
template
>
<div></div>
</
template
>
<
script
>
export
default
{
name
:
'ChapterWork'
}
</
script
>
<
style
lang=
"scss"
scoped
>
</
style
>
client/src/modules/viewer/components/work/courseWork.vue
0 → 100644
浏览文件 @
506bd6c8
<
template
>
<div
class=
"course-viewer-content"
v-loading=
"loading"
>
<div
class=
"course-viewer-content-hd"
>
<h3
class=
"course-viewer-content-hd__title"
>
课程大作业
</h3>
</div>
<div
class=
"course-viewer-content-bd"
>
<el-steps
direction=
"vertical"
v-if=
"data.curriculum"
>
<el-step
title=
"阅读大作业要求"
status=
"process"
>
<template
v-slot:description
>
<div
v-html=
"data.curriculum.curriculum_essay"
></div>
<p>
截止日期:
{{
data
.
essay_date
}}
</p>
</
template
>
</el-step>
<el-step
title=
"填写作业主题、正文,上传附件(点击“提交”保存)"
status=
"process"
>
<
template
v-slot:description
>
<el-form
:model=
"ruleForm"
:rules=
"rules"
:hide-required-asterisk=
"true"
:disabled=
"isRevised"
label-position=
"top"
ref=
"ruleForm"
>
<el-form-item
label=
"主题"
prop=
"essay_name"
>
<el-input
v-model=
"ruleForm.essay_name"
placeholder=
"主题"
></el-input>
</el-form-item>
<el-form-item
label=
"正文"
prop=
"essay_description"
>
<!-- 编辑器 -->
<v-editor
:disabled=
"isRevised"
v-model=
"ruleForm.essay_description"
></v-editor>
</el-form-item>
<el-form-item
label=
"附件"
prop=
"url"
>
<!-- 文件上传 -->
<v-upload
v-model=
"ruleForm.url"
>
<template
v-slot:tip
>
只支持docx格式的文件,文件小于10M
</
template
>
</v-upload>
</el-form-item>
</el-form>
</template>
</el-step>
<el-step
title=
"截止日期前提交"
status=
"process"
>
<
template
v-slot:description
>
<div
class=
"work-bottom"
v-if=
"detail"
>
<div
class=
"info"
>
<template
v-if=
"isRevised"
>
<div
class=
"paper-check"
>
<p>
批改时间:
{{
detail
.
check_date
}}
</p>
<div
class=
"paper-check-item"
>
<b>
评分:
</b>
{{
detail
.
score
}}
</div>
<div
class=
"paper-check-item"
>
<b>
评语:
</b>
<div
class=
"edit_html"
v-html=
"detail.check_comments"
></div>
</div>
</div>
</
template
>
<
template
v-else-if=
"detail.created_time"
>
<p
class=
"help"
>
已于
{{
detail
.
created_time
}}
提交,等待老师批改中。
</p>
<template
v-if=
"detail.updated_time !== detail.created_time"
>
<p
class=
"help"
>
最近提交时间:
{{
detail
.
updated_time
}}
</p>
</
template
>
</template>
</div>
</div>
<div
class=
"buttons"
>
<el-tooltip
content=
"在获老师批改之前,可以多次提交,将以最后一次提交为准"
placement=
"right"
>
<el-button
type=
"primary"
@
click=
"onSubmit"
:disabled=
"isRevised"
>
{{submitText}}
</el-button>
</el-tooltip>
</div>
</template>
</el-step>
</el-steps>
</div>
</div>
</template>
<
script
>
// componetns
import
VEditor
from
'./editor.vue'
import
VUpload
from
'./upload.vue'
// api
import
*
as
api
from
'../../api/index'
export
default
{
name
:
'CourseWork'
,
components
:
{
VEditor
,
VUpload
},
props
:
{
// 课程详情接口返回的数据
data
:
{
type
:
Object
,
default
:
()
=>
{}
}
},
data
()
{
return
{
ruleForm
:
{
essay_name
:
''
,
essay_description
:
''
,
url
:
''
},
rules
:
{
essay_name
:
[
{
required
:
true
,
message
:
'请输入主题'
,
trigger
:
'blur'
},
{
max
:
5
,
message
:
'最多输入 50 个字符'
,
trigger
:
'blur'
}
],
essay_description
:
[
{
required
:
true
,
message
:
'请输入正文'
,
trigger
:
'blur'
}
],
url
:
[{
required
:
true
,
message
:
'请上传附件'
,
trigger
:
'change'
}]
},
detail
:
null
,
loading
:
false
,
messageInstance
:
null
}
},
computed
:
{
// 学期ID
sid
()
{
return
'6552021107166150656'
},
// 课程ID
cid
()
{
return
'6568035374902280192'
},
// 是否批改
isRevised
()
{
return
this
.
detail
?
!!
this
.
detail
.
check_date
:
false
},
// 提交按钮文本
submitText
()
{
return
this
.
isRevised
?
'已批改'
:
'提交'
}
},
methods
:
{
// 获取大作业详情
getDetail
()
{
this
.
loading
=
true
api
.
getCourseWork
(
this
.
sid
,
this
.
cid
)
.
then
(
response
=>
{
this
.
detail
=
Array
.
isArray
(
response
)
?
null
:
response
if
(
this
.
detail
)
{
this
.
ruleForm
.
essay_name
=
this
.
detail
.
essay_name
this
.
ruleForm
.
essay_description
=
this
.
detail
.
essay_description
this
.
ruleForm
.
url
=
this
.
detail
.
file_url
}
})
.
finally
(()
=>
{
this
.
loading
=
false
})
},
// 提交
onSubmit
()
{
this
.
$refs
.
ruleForm
.
validate
()
.
then
(
response
=>
{
const
params
=
Object
.
assign
(
this
.
ruleForm
,
{
semester_id
:
this
.
sid
,
course_id
:
this
.
cid
})
this
.
handleSubmitRequest
(
params
)
})
.
catch
(()
=>
{
this
.
messageInstance
&&
this
.
messageInstance
.
close
()
this
.
messageInstance
=
this
.
$message
.
error
(
'还有题目未做,不能提交'
)
})
},
// 请求提交接口
handleSubmitRequest
(
params
)
{
api
.
updateCourseWork
(
this
.
sid
,
this
.
cid
,
params
)
.
then
(
response
=>
{
if
(
response
.
status
)
{
this
.
$message
.
success
(
'提交成功,等待批改'
)
this
.
getDetail
()
}
else
{
this
.
$message
.
error
(
response
.
data
.
error
)
}
})
.
catch
(
error
=>
{
this
.
$message
.
error
(
error
.
message
)
})
}
},
beforeMount
()
{
this
.
getDetail
()
}
}
</
script
>
<
style
lang=
"scss"
scoped
>
p
{
margin
:
0
;
}
::v-deep
.el-step__title
{
border-bottom
:
1px
dashed
#cecece
;
}
::v-deep
.el-step__description
{
padding
:
20px
0
30px
;
font-size
:
14px
;
}
::v-deep
.el-form-item__label
{
font-weight
:
bold
;
line-height
:
24px
;
padding
:
0
0
5px
;
}
.work-bottom
{
.info
{
color
:
#999
;
line-height
:
28px
;
}
}
.buttons
{
padding
:
20px
0
;
::v-deep
.el-button
{
width
:
120px
;
}
}
.paper-check
{
padding
:
10px
;
color
:
#000
;
border
:
1px
solid
#dedede
;
}
.paper-check-item
{
display
:
flex
;
}
</
style
>
client/src/modules/viewer/components/work/editor.vue
0 → 100644
浏览文件 @
506bd6c8
<
template
>
<div
class=
"editor"
>
<textarea
name=
"editor"
:id=
"textareaElementId"
:disabled=
"disabled"
></textarea>
</div>
</
template
>
<
script
>
import
{
uniqueId
}
from
'lodash'
export
default
{
name
:
'VEditor'
,
props
:
{
value
:
{
type
:
String
},
disabled
:
{
type
:
Boolean
,
default
:
false
}
},
data
()
{
return
{
textareaElementId
:
uniqueId
(
'editor_'
),
ckEditor
:
null
}
},
methods
:
{
createEditor
()
{
const
editor
=
(
this
.
ckEditor
=
CKEDITOR
.
replace
(
this
.
textareaElementId
,
{
height
:
400
,
uiColor
:
'#eeeeee'
,
filebrowserImageUploadUrl
:
'/api/ckeditor/img/upload'
,
// resize_enabled: typeof this.props.resizable === 'boolean' ? this.props.resizable : true,
toolbar
:
[
// { name: 'document', items: ['Source', '-', 'Save', 'NewPage', 'Preview'] },
{
name
:
'styles'
,
items
:
[
'Styles'
,
'Format'
,
'Font'
,
'FontSize'
]
},
{
name
:
'colors'
,
items
:
[
'TextColor'
,
'BGColor'
]
},
{
name
:
'tools'
,
items
:
[
'Maximize'
,
'ShowBlocks'
]
},
// { name: 'clipboard', items: ['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo'] },
{
name
:
'editing'
,
items
:
[
'Find'
,
'Replace'
]
},
// { name: 'forms', items: ['Form', 'Checkbox', 'Radio', 'TextField', 'Textarea', 'Select', 'Button', 'ImageButton', 'HiddenField'] },
'/'
,
{
name
:
'basicstyles'
,
items
:
[
'Bold'
,
'Italic'
,
'Underline'
,
'Strike'
,
'Subscript'
,
'Superscript'
,
'-'
,
'RemoveFormat'
]
},
{
name
:
'paragraph'
,
items
:
[
'NumberedList'
,
'BulletedList'
,
'-'
,
'Outdent'
,
'Indent'
,
'-'
,
'Blockquote'
,
'CreateDiv'
,
'-'
,
'JustifyLeft'
,
'JustifyCenter'
,
'JustifyRight'
,
'JustifyBlock'
,
'-'
,
'BidiLtr'
,
'BidiRtl'
]
},
{
name
:
'links'
,
items
:
[
'Link'
,
'Unlink'
,
'Anchor'
]
},
{
name
:
'insert'
,
items
:
[
'Image'
,
'Table'
,
'HorizontalRule'
]
}
]
}))
editor
.
on
(
'instanceReady'
,
()
=>
{
const
data
=
this
.
value
editor
.
fire
(
'lockSnapshot'
)
editor
.
setData
(
data
,
{
callback
:
()
=>
{
this
.
bindEvent
()
const
newData
=
editor
.
getData
()
// Locking the snapshot prevents the 'change' event.
// Trigger it manually to update the bound data.
if
(
data
!==
newData
)
{
this
.
$once
(
'input'
,
()
=>
{
this
.
$emit
(
'ready'
,
editor
)
})
this
.
$emit
(
'input'
,
newData
)
}
else
{
this
.
$emit
(
'ready'
,
editor
)
}
editor
.
fire
(
'unlockSnapshot'
)
}
})
})
},
bindEvent
()
{
const
editor
=
this
.
ckEditor
editor
.
on
(
'change'
,
evt
=>
{
const
data
=
editor
.
getData
()
if
(
this
.
value
!==
data
)
{
this
.
$emit
(
'input'
,
data
,
evt
,
editor
)
}
})
editor
.
on
(
'focus'
,
evt
=>
{
this
.
$emit
(
'focus'
,
evt
,
editor
)
})
editor
.
on
(
'blur'
,
evt
=>
{
this
.
$emit
(
'blur'
,
evt
,
editor
)
})
}
},
mounted
()
{
this
.
createEditor
()
},
beforeDestroy
()
{
this
.
ckEditor
&&
this
.
ckEditor
.
destroy
()
this
.
ckEditor
=
null
}
}
</
script
>
<
style
lang=
"scss"
scoped
>
*
{
margin
:
0
;
padding
:
0
;
}
</
style
>
client/src/modules/viewer/components/work/index.vue
0 → 100644
浏览文件 @
506bd6c8
<
template
>
<component
:is=
"currentCompoent"
:chapter=
"chapter"
:data=
"data"
v-bind=
"$attrs"
v-on=
"$listeners"
v-if=
"chapter"
/>
</
template
>
<
script
>
import
CourseWork
from
'./courseWork.vue'
import
ChapterExam
from
'./chapterExam.vue'
export
default
{
name
:
'ViewerWork'
,
components
:
{
CourseWork
,
ChapterExam
},
props
:
{
// 当前选中的
chapter
:
{
type
:
Object
,
default
:
()
=>
{}
},
// 课程详情接口返回的数据
data
:
{
type
:
Object
,
default
:
()
=>
{}
}
},
computed
:
{
currentCompoent
()
{
const
componentNames
=
{
1
:
'ChapterExam'
,
// 考试
2
:
'CourseWork'
// 作业
}
const
homework
=
this
.
chapter
.
homework
return
homework
?
componentNames
[
homework
.
work_type
]
||
''
:
'CourseWork'
}
}
}
</
script
>
client/src/modules/viewer/components/work/upload.vue
0 → 100644
浏览文件 @
506bd6c8
<
template
>
<div
class=
"upload"
>
<el-upload
action
:show-file-list=
"false"
:http-request=
"httpRequest"
>
<el-button
size=
"small"
icon=
"el-icon-upload"
>
点击上传
</el-button>
<template
v-slot:tip
>
<div
class=
"el-upload__tips"
>
<slot
name=
"tip"
></slot>
</div>
</
template
>
</el-upload>
<div
class=
"file-list"
v-if=
"value"
>
<div
class=
"file-list-item"
>
<a
:href=
"value"
target=
"_blank"
>
<i
class=
"el-icon-document"
></i>
{{ fileName }}
</a>
<a
:href=
"value"
:download=
"fileName"
target=
"_blank"
>
<el-tooltip
effect=
"dark"
content=
"下载"
>
<i
class=
"el-icon-download"
></i>
</el-tooltip>
</a>
</div>
</div>
</div>
</template>
<
script
>
import
*
as
api
from
'../../api/index'
export
default
{
name
:
'VUpload'
,
props
:
{
value
:
{
type
:
String
}
},
data
()
{
return
{}
},
computed
:
{
fileName
()
{
return
this
.
value
?
this
.
value
.
split
(
'/'
).
pop
()
:
''
}
},
methods
:
{
httpRequest
(
xhr
)
{
api
.
uploadFile
({
file
:
xhr
.
file
})
.
then
(
response
=>
{
if
(
response
.
success
)
{
this
.
$emit
(
'input'
,
response
.
url
)
}
})
.
catch
(
error
=>
{
console
.
log
(
error
)
})
},
handleRemove
()
{
this
.
$emit
(
'input'
,
''
)
}
}
}
</
script
>
<
style
lang=
"scss"
scoped
>
.file-list-item
{
display
:
flex
;
margin-bottom
:
10px
;
padding
:
0
10px
;
justify-content
:
space-between
;
background-color
:
#fff
;
border-radius
:
4px
;
a
{
text-decoration
:
none
;
color
:
#333
;
&
:hover
{
color
:
#b49441
;
}
}
}
</
style
>
client/src/modules/viewer/index.vue
浏览文件 @
506bd6c8
<
template
>
<div
class=
"course-viewer"
>
<div
class=
"course-viewer-main"
>
<!-- 顶部区域 -->
<div
class=
"course-viewer-hd"
>
<div
class=
"course-viewer-
main-
hd"
>
<router-link
to=
"/mobile/help/student"
>
<i
class=
"el-icon-arrow-left"
></i>
</router-link>
<h1
class=
"course-viewer-
hd__title"
>
{{
detail
.
course_name
}}
</h1>
<h1
class=
"course-viewer-
main-hd__title"
>
{{
detail
.
course_name
}}
</h1>
<router-link
to=
"/app/account/feedbackCreate"
target=
"_blank"
>
<el-tooltip
effect=
"light"
content=
"意见反馈"
>
<i
class=
"el-icon-self-fankuiyijian"
></i>
...
...
@@ -20,17 +19,27 @@
</router-link>
</div>
<!-- 主体区域 -->
<div
class=
"course-viewer-bd"
>
<player
:video=
"chatperResources.video"
pdf=
"https://img1.ezijing.com/ppts/6437335122927681536/PPT_3.2%20%E6%A1%88%E4%BE%8B%E7%A0%94%E7%A9%B6%E6%96%B9%E6%B3%95%EF%BC%88%E4%B8%80%EF%BC%89.pdf"
:ppts=
"chatperResources.ppts"
v-if=
"chatperResources.video"
<div
class=
"course-viewer-main-bd"
>
<router-view
:data=
"detail"
:chapter=
"activeChapter"
:files=
"files"
:pptIndex=
"pptIndex"
:key=
"pid"
@
pptupdate=
"handlePPTupdate"
@
change-ppt=
"handleChangePPT"
/>
</div>
</div>
<!-- 侧边栏 -->
<v-aside
:chapters=
"detail.chapters"
:ppts=
"chatperResources.ppts"
></v-aside>
<v-aside
:chapters=
"chapters"
:active=
"activeChapter"
:ppts=
"ppts"
:pptIndex=
"pptIndex"
@
change-ppt=
"handleChangePPT"
v-if=
"detail.chapters"
></v-aside>
</div>
</
template
>
...
...
@@ -38,80 +47,115 @@
// api
import
*
as
api
from
'./api/index'
// components
import
VAside
from
'./components/aside/
aside
.vue'
import
Player
from
'./components/player/player.vue'
import
VAside
from
'./components/aside/
index
.vue'
export
default
{
name
:
'CourseViewer'
,
components
:
{
VAside
,
Player
},
components
:
{
VAside
},
data
()
{
return
{
detail
:
{},
chatperResources
:
{}
ppts
:
[],
pptIndex
:
0
}
},
computed
:
{
// 当前章节
watch
:
{
activeChapter
()
{
return
{
resource_id
:
'6414747439944695808'
}
this
.
ppts
=
[]
this
.
pptIndex
=
0
}
},
computed
:
{
// 学期ID
sid
()
{
return
'6552021107166150656'
},
//
视频资源
ID
resourceI
d
()
{
return
this
.
activeChapter
.
resource_id
//
课程
ID
ci
d
()
{
return
'6568035374902280192'
},
/**
* 视频提供者
* @return 1是CC加密; 2是非加密; 3是阿里云
*/
videoProvider
()
{
const
video
=
this
.
activeChapter
.
video
||
{}
return
video
.
video_provider
||
3
// 当前页面的ID
pid
()
{
return
this
.
$route
.
params
.
id
},
// 章节列表
chapters
()
{
const
chapters
=
this
.
detail
.
chapters
||
[]
return
chapters
.
concat
([
{
name
:
'大作业及资料'
,
children
:
[
{
name
:
'课程大作业'
,
id
:
'course_work'
,
type
:
3
},
{
name
:
'课程资料'
,
id
:
'course_info'
,
type
:
4
},
{
name
:
'教学评估'
,
id
:
'teach_evaluation'
}
]
}
])
},
// 当前选中的章节
activeChapter
()
{
const
id
=
this
.
pid
const
list
=
this
.
chapters
return
this
.
findChapter
(
id
,
list
)
},
// 课程资料
files
()
{
return
this
.
detail
.
files
||
[]
}
},
methods
:
{
// 查找当前章节
findChapter
(
id
,
list
)
{
for
(
const
item
of
list
)
{
if
(
item
.
id
===
id
)
{
return
item
}
if
(
item
.
children
&&
item
.
children
.
length
)
{
const
found
=
this
.
findChapter
(
id
,
item
.
children
)
if
(
found
)
{
return
found
}
}
}
return
null
},
// 获取课程详情
getCourse
()
{
api
.
getCourse
(
'6437296642994470912'
,
'6437335122927681536'
)
.
then
(
response
=>
{
this
.
detail
=
response
})
api
.
getCourse
(
this
.
sid
,
this
.
cid
).
then
(
response
=>
{
this
.
detail
=
response
})
},
// 获取章节视频详情
getChapterVideo
()
{
// 视频播放类型 1是CC加密; 2是非加密; 3是阿里云
if
(
this
.
videoProvider
===
3
)
{
api
.
getChapterVideoAliyun
(
this
.
resourceId
).
then
(
response
=>
{
this
.
chatperResources
=
response
})
}
else
{
api
.
getChapterVideo
(
this
.
resourceId
).
then
(
response
=>
{
this
.
chatperResources
=
response
})
}
// PPT列表更新
handlePPTupdate
(
list
)
{
this
.
ppts
=
list
},
// 右侧菜单选中的PPT修改
handleChangePPT
(
index
)
{
this
.
pptIndex
=
index
}
},
beforeMount
()
{
this
.
getCourse
()
this
.
getChapterVideo
()
}
}
</
script
>
<
style
lang=
"scss"
scoped
>
<
style
lang=
"scss"
>
.course-viewer
{
display
:
flex
;
background-color
:
#3f3f3f
;
height
:
100vh
;
overflow
:
hidden
;
}
.course-viewer-main
{
flex
:
1
;
display
:
flex
;
flex-direction
:
column
;
}
.course-viewer-hd
{
.course-viewer-
main-
hd
{
display
:
flex
;
align-items
:
center
;
background-color
:
#3f3f3f
;
height
:
56px
;
a
{
color
:
#fff
;
...
...
@@ -121,13 +165,59 @@ export default {
font-size
:
24px
;
}
}
.course-viewer-hd__title
{
.course-viewer-
main-
hd__title
{
flex
:
1
;
font-size
:
1
.5em
;
text-align
:
center
;
color
:
#a0a0a0
;
}
.course-viewer-bd
{
.course-viewer-
main-
bd
{
flex
:
1
;
height
:
calc
(
100vh
-
56px
);
overflow-y
:
auto
;
}
.course-viewer-content
{
// min-height: 50%;
max-width
:
900px
;
padding
:
40px
120px
80px
;
margin
:
40px
auto
;
background-color
:
#f2f2f2
;
box-shadow
:
0
0
2px
rgba
(
0
,
0
,
0
,
0
.05
);
}
.course-viewer-content-hd
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
padding
:
40px
0
;
// text-align: center;
}
.course-viewer-content-hd__title
{
position
:
relative
;
display
:
inline-block
;
margin
:
0
0
0
20px
;
padding
:
0
0
5px
;
font-size
:
20px
;
border-bottom
:
3px
solid
#707070
;
&
:
:
before
{
content
:
'·'
;
position
:
absolute
;
left
:
-30px
;
top
:
50%
;
font-size
:
30px
;
transform
:
translateY
(
-50%
);
}
&
:
:
after
{
content
:
''
;
position
:
absolute
;
left
:
0
;
bottom
:
-8px
;
width
:
100%
;
height
:
1px
;
background-color
:
#707070
;
}
}
.course-viewer-content-hd__aside
{
font-size
:
18px
;
// border-bottom: 3px solid #707070;
}
</
style
>
client/src/modules/viewer/routes.js
0 → 100644
浏览文件 @
506bd6c8
export
default
[
{
path
:
'/viewer'
,
component
:
()
=>
import
(
'./index.vue'
),
children
:
[
{
name
:
'viewerCourseChapter'
,
path
:
':id'
,
component
:
()
=>
import
(
'./components/layout.vue'
)
}
]
}
]
client/src/router/routes.js
浏览文件 @
506bd6c8
// import viewerRoutes from '@/modules/viewer/routes.js'
export
default
[
{
path
:
'/'
,
redirect
:
'/app/learn/course'
},
{
...
...
@@ -284,5 +286,6 @@ export default [
// { path: '/survey-phone/*', redirect: '/learn-error/learn-error' },
/* 如果所有页面都没找到 - 指向 */
{
path
:
'*'
,
component
:
()
=>
import
(
'@/components/errorPages/404.vue'
)
}
// { path: '/viewer', component: () => import('@/modules/viewer/index.vue') }
// viewer module routes
// ...viewerRoutes
]
package-lock.json
deleted
100644 → 0
浏览文件 @
fbf2f920
{
"requires"
:
true
,
"lockfileVersion"
:
1
,
"dependencies"
:
{
"js-cookie"
:
{
"version"
:
"2.2.1"
,
"resolved"
:
"https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz"
,
"integrity"
:
"sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
},
"js-md5"
:
{
"version"
:
"0.7.3"
,
"resolved"
:
"https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz"
,
"integrity"
:
"sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ=="
},
"vuex"
:
{
"version"
:
"3.1.3"
,
"resolved"
:
"https://registry.npmjs.org/vuex/-/vuex-3.1.3.tgz"
,
"integrity"
:
"sha512-k8vZqNMSNMgKelVZAPYw5MNb2xWSmVgCKtYKAptvm9YtZiOXnRXFWu//Y9zQNORTrm3dNj1n/WaZZI26tIX6Mw=="
}
}
}
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论