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

chore: update

上级 10af3b74
...@@ -62,3 +62,16 @@ export function getProjectList(params: { organization_id?: string; project_id?: ...@@ -62,3 +62,16 @@ export function getProjectList(params: { organization_id?: string; project_id?:
export function getQuestionCategory(params: { project_tag: string }) { export function getQuestionCategory(params: { project_tag: string }) {
return httpRequest.get(`/api/qbs/admin/v2/question-category/tree/${params.project_tag}`, { params }) return httpRequest.get(`/api/qbs/admin/v2/question-category/tree/${params.project_tag}`, { params })
} }
// 获取试题分类
export function collectionResource(params: {
course_id: string
semester_id: string
chapter_id: string
section_id: string
source_id: string
type: number
status: number
}) {
return httpRequest.get('/api/saas/api/v1/collection/resource', { params })
}
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
'primary': ( 'primary': (
'base': #aa1941 'base': #aa1941
) )
),
$dialog: (
'border-radius': '8px'
) )
); );
......
<script setup lang="ts">
interface Props {
resource_type: number
type: string
}
defineProps<Props>()
</script>
<template>
<img src="@/assets/images/icon_chapter.png" />
</template>
...@@ -5,8 +5,8 @@ import type { UploadProps, UploadUserFile } from 'element-plus' ...@@ -5,8 +5,8 @@ import type { UploadProps, UploadUserFile } from 'element-plus'
import md5 from 'blueimp-md5' import md5 from 'blueimp-md5'
import { getSignature } from '@/api/base' import { getSignature } from '@/api/base'
const props = withDefaults(defineProps<{ modelValue: string | []; prefix?: string }>(), { const props = withDefaults(defineProps<{ modelValue: string | UploadUserFile[]; prefix?: string }>(), {
prefix: 'upload/admin/' prefix: 'upload/saas-learn/'
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
...@@ -48,13 +48,7 @@ const handleSuccess = (response: any, file: any, files: any) => { ...@@ -48,13 +48,7 @@ const handleSuccess = (response: any, file: any, files: any) => {
emit( emit(
'update:modelValue', 'update:modelValue',
files.map((item: any) => { files.map((item: any) => {
console.log(item, 'items') return { name: item.name, url: item.url || item.raw.url, size: item.raw.size, type: item.raw.type }
return {
name: item.name,
url: item.url || item.raw.url,
size: item.raw.size,
type: item.raw.type || item.raw.url
}
}) })
) )
} else { } else {
...@@ -105,7 +99,7 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => { ...@@ -105,7 +99,7 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => {
<el-icon><Plus /></el-icon> <el-icon><Plus /></el-icon>
</template> </template>
<template v-else> <template v-else>
<el-button type="primary" class="app-upload-btn">点击上传</el-button> <el-button type="primary" round>点击上传</el-button>
</template> </template>
</template> </template>
<div class="avatar-uploader" v-else> <div class="avatar-uploader" v-else>
......
import { collectionResource } from '@/api/base'
export function toggleCollectionResource(params) {
return collectionResource(params).then(res => {
console.log(res)
})
}
...@@ -14,6 +14,7 @@ import AppList from '@/components/base/AppList.vue' ...@@ -14,6 +14,7 @@ import AppList from '@/components/base/AppList.vue'
import modules from './modules' import modules from './modules'
import { permissionDirective } from '@/utils/permission' import { permissionDirective } from '@/utils/permission'
import { useMapStore } from '@/stores/map'
const app = createApp(App) const app = createApp(App)
// 注册公共组件 // 注册公共组件
...@@ -27,3 +28,5 @@ app.use(router) ...@@ -27,3 +28,5 @@ app.use(router)
app.use(ElementPlus, { locale: zhCn }) app.use(ElementPlus, { locale: zhCn })
app.mount('#app') app.mount('#app')
useMapStore().getMapList()
import httpRequest from '@/utils/axios' import httpRequest from '@/utils/axios'
// 获取课程列表 // 获取课程列表
export function getCourseList(params?: { id?: string }) { export function getCourseList(params?: { id?: string; semester_ids?: string; elective_types?: string }) {
return httpRequest.get('/api/saas/api/v1/course/list', { params }) return httpRequest.get('/api/saas/api/v1/course/list', { params })
} }
// 搜索课程 // 搜索课程
export function searchCourseList(params?: { id?: string }) { export function searchCourseList(params?: { search_name?: string; semester_ids?: string; elective_types?: string }) {
return httpRequest.get('/api/saas/api/v1/course/search', { params }) return httpRequest.get('/api/saas/api/v1/course/search', { params })
} }
// 获取学期
export function getSemesterList() {
return httpRequest.get('/api/saas/api/v1/semester/student-semesters')
}
// 置顶课程 // 置顶课程
export function topCourse(data: { id: string; status: number }) { export function topCourse(data: { id: string; status: number }) {
return httpRequest.post('/api/saas/api/v1/course/sticky-top', data) return httpRequest.post('/api/saas/api/v1/course/sticky-top', data)
} }
// 获取课程详情信息 // 获取课程详情信息
export function getCourse(params: { course_id: string; semester_id: string }) { export function getCourse(params: { course_id: string; semester_id: string }) {
return httpRequest.get(`/api/saas/api/v1/course/${params.course_id}/detail/${params.semester_id}`, { params }) return httpRequest.get(`/api/saas/api/v1/course/${params.course_id}/detail/${params.semester_id}`, { params })
} }
// 获取课程列表 // 获取章节列表
export function getChapterTreeList(data: { course_id: string; semester_id: string }) { export function getChapterTreeList(data: { course_id: string; semester_id: string }) {
return httpRequest.post('/api/saas/api/v1/chapter/tree', data) return httpRequest.post('/api/saas/api/v1/chapter/tree', data)
} }
// 获取大作业详情
export function getCourseWork(data: { course_id: string; semester_id: string }) {
return httpRequest.post('/api/saas/api/v1/job/detail', data)
}
// 提交大作业
export function submitCourseWork(data: {
course_id: string
semester_id: string
title: string
content: string
attachments?: string
}) {
return httpRequest.post('/api/saas/api/v1/job/submit', data)
}
// 获取课程小节信息
export function getCourseSection(data: { section_id: string; semester_id: string }) {
return httpRequest.post('/api/saas/api/v1/chapter/section/detail', data)
}
// 获取课程视频信息
export function getCoursePlayInfo(params: { source_id: string }) {
return httpRequest.get(`/api/saas/api/v1/video/vod/${params.source_id}/playing-info`, { params })
}
// 获取视频观看记录
export function getVideoRecords(data: {
sso_id: string
semester_id: string
chapter_id: string
course_id: string
video_id: string
section_id: string
current_playing_time: string
max_playing_time: string
valid_playing_time: string
cumulative_playing_time: string
}) {
return httpRequest.post('/api/saas/api/v1/course/video/recent-viewings', data)
}
// 上传视频观看记录
export function uploadVideoRecords(data: {
semester_id: string
chapter_id: string
course_id: string
video_id: string
section_id: string
}) {
return httpRequest.post('/api/saas/api/v1/course/video/upload-records', data)
}
<script setup lang="ts"> <script setup lang="ts">
import type { CourseType } from '@/types' import type { CourseType } from '@/types'
import { useMapStore } from '@/stores/map'
import { topCourse } from '../api'
import { ElMessage } from 'element-plus'
interface Props { interface Props {
data: CourseType data: CourseType
} }
defineProps<Props>() const props = defineProps<Props>()
const emit = defineEmits(['change'])
const route = useRoute()
let courseId = $ref<string>('')
watchEffect(() => {
courseId = route.query.course_id as string
})
const mapStore = useMapStore()
// 选课类型
const electiveTypes = mapStore.getMapValuesByKey('system_elective_type')
// 选课类型文本
const electiveTypeText = computed(() => {
return electiveTypes.find(item => parseInt(item.value) === props.data.elective_type)?.label || ''
})
// 是否选中
const isActive = computed(() => props.data.course_id === courseId)
// 置顶 // 置顶
function handleTop(data) {} function handleTop(data: CourseType) {
topCourse({ id: data.id, status: data.is_top ? 0 : 1 }).then(() => {
emit('change')
ElMessage.success('操作成功')
})
}
</script> </script>
<template> <template>
<div class="course-item"> <div class="course-item" :class="{ 'is-active': isActive }">
<div class="course-item__top" :class="{ 'is-active': !!data.is_top }" @click="handleTop(data)"></div> <el-tooltip :content="!!data.is_top ? '取消置顶' : '置顶'">
<div class="course-item__top" :class="{ 'is-active': !!data.is_top }" @click="handleTop(data)"></div>
</el-tooltip>
<router-link :to="`/course/view?course_id=${data.course_id}&semester_id=${data.semester_id}`"> <router-link :to="`/course/view?course_id=${data.course_id}&semester_id=${data.semester_id}`">
<div class="course-item-bd"> <div class="course-item-bd">
<div class="course-item-pic"><img :src="data.cover" /><span class="course-item__type">必修课</span></div> <div class="course-item-pic">
<img :src="data.cover" /><span class="course-item__type">{{ electiveTypeText }}</span>
</div>
<div class="course-item-main"> <div class="course-item-main">
<h2 class="course-item__name">{{ data.name }}</h2> <h2 class="course-item__name">{{ data.name }}</h2>
<div class="course-item-progress">总进度<el-progress :percentage="data.watch_video_length" /></div> <div class="course-item-progress">总进度<el-progress :percentage="data.watch_video_length" /></div>
...@@ -22,7 +53,7 @@ function handleTop(data) {} ...@@ -22,7 +53,7 @@ function handleTop(data) {}
</div> </div>
</router-link> </router-link>
<div class="course-item-ft"> <div class="course-item-ft">
<div class="course-item-playlist" v-if="data.section"> <div class="course-item-playlist" v-if="data.section?.id">
<p class="t1">{{ data.section.name }}</p> <p class="t1">{{ data.section.name }}</p>
<p class="t2">已观看{{ data.section.watch_video_length }}%</p> <p class="t2">已观看{{ data.section.watch_video_length }}%</p>
<el-button type="primary" round>点击观看</el-button> <el-button type="primary" round>点击观看</el-button>
...@@ -45,6 +76,9 @@ function handleTop(data) {} ...@@ -45,6 +76,9 @@ function handleTop(data) {}
display: block; display: block;
} }
} }
&.is-active {
background: rgba(247, 248, 250, 1);
}
} }
.course-item__top { .course-item__top {
display: none; display: none;
......
<script setup lang="ts">
import { Search, Filter } from '@element-plus/icons-vue'
import type { SemesterType } from '@/types'
import type { CourseListParamsTypes } from '../types'
import { searchCourseList, getSemesterList } from '../api'
import { useMapStore } from '@/stores/map'
const emit = defineEmits(['change'])
const mapStore = useMapStore()
// 选课类型
const electiveTypes = mapStore.getMapValuesByKey('system_elective_type')
// 列表参数
const params = reactive<CourseListParamsTypes>({ id: '', semester_ids: [], elective_types: [] })
// 搜索课程
const searchValue = $ref('')
function querySearch(query: string, cb: (arg: any) => void) {
searchCourseList({ search_name: query }).then(res => {
let results = res.data.data || []
if (query) {
results = searchHighlight(results, query)
}
cb(results)
})
}
// 设置关键词高亮
function searchHighlight(list: Record<string, any>, query: string) {
return list.map((item: any) => {
item.name = item.name.replace(query, `<span class="search-highlight">${query}</span>`)
if (item.chapters) {
item.chapters = searchHighlight(item.chapters, query)
}
if (item.sections) {
item.sections = searchHighlight(item.sections, query)
}
return item
})
}
// 搜索选择
function handleSelect(data: any) {
params.id = data.id
emit('change', params)
}
// 获取学期列表
let semesterList = $ref<SemesterType[]>([])
// 整理后的学期列表
const currentSemesterList = $computed(() => {
return semesterList.map(item => ({ value: item.id, label: item.name }))
})
function fetchSemesterList() {
getSemesterList().then(res => {
semesterList = res.data
})
}
onMounted(() => {
fetchSemesterList()
})
// 筛选列表
const filterList = computed(() => {
return [
{ model: 'semester_ids', label: '学期', options: currentSemesterList },
{ model: 'elective_types', label: '课程类型', options: electiveTypes }
]
})
</script>
<template>
<div class="course-search">
<el-autocomplete
clearable
placeholder="搜索"
v-model="searchValue"
:fetch-suggestions="querySearch"
:prefix-icon="Search"
@select="handleSelect"
class="course-search__input"
>
<template #default="{ item }">
<div class="search-course-item">
<h4 class="search-course-item__name" v-html="item.name"></h4>
<div class="search-course-chapters" v-if="item.chapters?.length">
<div class="search-course-chapter-item" v-for="chapter in item.chapters" :key="chapter.id">
<p class="search-course-chapter-item__name" v-html="chapter.name"></p>
<div class="search-course-sections" v-if="chapter.sections?.length">
<div
class="search-course-section-item"
v-for="section in chapter.sections"
:key="section.id"
v-html="section.name"
></div>
</div>
</div>
</div>
</div>
</template>
</el-autocomplete>
<el-popover placement="bottom-end" :width="292" trigger="click">
<template #reference>
<el-button :icon="Filter" class="course-filter__button"></el-button>
</template>
<div class="course-filter">
<dl class="course-filter-item" v-for="item in filterList" :key="item.model">
<dt class="course-filter-item__label">{{ item.label }}</dt>
<dd class="course-filter-item__main">
<el-checkbox-group v-model="params[item.model]" @change="$emit('change', params)">
<el-checkbox-button v-for="option in item.options" :label="option.value" :key="option.value">
{{ option.label }}
</el-checkbox-button>
</el-checkbox-group>
</dd>
</dl>
</div>
</el-popover>
</div>
</template>
<style lang="scss">
// 搜索
.course-search {
display: flex;
align-items: center;
}
.course-search__input {
flex: 1;
}
.search-highlight {
color: var(--main-color);
}
.search-course-item {
padding: 10px 0;
border-bottom: 1px dashed #d6d6d6;
}
.search-course-item__name {
font-size: 16px;
font-weight: 500;
line-height: 30px;
color: #333333;
}
.search-course-chapter-item__name {
position: relative;
font-size: 16px;
font-weight: 400;
line-height: 24px;
color: #333333;
padding-left: 15px;
&::before {
content: '';
position: absolute;
left: 5px;
top: 50%;
width: 5px;
height: 5px;
background: var(--main-color);
border-radius: 50%;
transform: translateY(-50%);
}
}
.search-course-sections {
margin-left: 30px;
}
.search-course-section-item {
font-size: 14px;
font-weight: 400;
line-height: 24px;
color: #666666;
}
// 筛选
.course-filter__button {
margin-left: 10px;
padding: 8px;
}
.course-filter {
padding: 8px;
}
.course-filter-item + .course-filter-item {
margin-top: 20px;
}
.course-filter-item__label {
font-size: 16px;
line-height: 1;
color: #333;
}
.course-filter-item__main {
margin-top: 12px;
.el-checkbox-group {
display: flex;
justify-content: space-between;
}
.el-checkbox-button__inner {
padding: 10px 20px;
border: 1px solid #adadad !important;
border-radius: 18px !important;
box-shadow: none !important;
}
}
</style>
<!-- 学习 --> <!-- 学习 -->
<script setup lang="ts"></script> <script setup lang="ts">
<template>学习</template> import * as api from '../api'
let chapterList = $ref<CourseType[]>([])
const { query } = useRoute()
const courseId = $ref(query.course_id as string)
const semesterId = $ref(query.semester_id as string)
// 获取章节列表
function fetchList() {
if (!courseId || !semesterId) {
return
}
api.getChapterTreeList({ course_id: courseId, semester_id: semesterId }).then(res => {
chapterList = res.data.items
})
}
onMounted(() => {
fetchList()
})
</script>
<template>
<div class="course-player-chapter">
<el-tabs>
<el-tab-pane label="章节"></el-tab-pane>
<el-tab-pane label="讲义"></el-tab-pane>
</el-tabs>
<el-collapse>
<el-collapse-item :title="item.name" :name="item.id" v-for="item in chapterList" :key="item.id">
<el-collapse>
<el-collapse-item :title="section.name" :name="section.id" v-for="section in item.sections" :key="section.id">
<ul>
<li v-for="resource in section.resources" :key="resource.id">
<router-link
:to="`/course/player?course_id=${courseId}&section_id=${section.id}&semester_id=${semesterId}`"
>
{{ resource.name }}
</router-link>
</li>
</ul>
</el-collapse-item>
</el-collapse>
</el-collapse-item>
</el-collapse>
</div>
</template>
<style lang="scss" scoped>
.course-player-chapter {
width: 258px;
padding: 20px;
background-color: #1f1e24;
}
li {
display: flex;
align-items: center;
height: 48px;
line-height: 48px;
border-bottom: 1px solid #e6e6e6;
a {
flex: 1;
}
}
</style>
...@@ -2,100 +2,28 @@ ...@@ -2,100 +2,28 @@
import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue' import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue'
import type { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js' import type { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
import { getCoursePlayInfo } from '../api'
import type { PlayItemTypes } from '../types'
const options = $ref<VideoJsPlayerOptions>() const options = $ref<VideoJsPlayerOptions>()
const playList = [
{ const { query } = useRoute()
Status: 'Normal', const sourceId = $ref(query.source_id as string)
StreamType: 'video',
HDRType: 'other', let playList = $ref<PlayItemTypes[]>([])
Size: 2989764, const currentPlayList = $computed<PlayItemTypes[]>(() => {
Definition: 'FD', return playList.filter(item => item.StreamType === 'video' && item.Format === 'mp4')
Fps: '25', })
Specification: 'H264.LD',
ModificationTime: '2022-07-07T09:17:14Z', function fetchInfo() {
Duration: '52.2251', getCoursePlayInfo({ source_id: sourceId }).then(res => {
Bitrate: '457.981', playList = res.data.playing_list
BitDepth: 8, changeSrc(currentPlayList[0])
Encrypt: 0, })
PreprocessStatus: 'UnPreprocess', }
Format: 'm3u8', onMounted(fetchInfo)
NarrowBandType: '0',
PlayURL: 'https://media.w3.org/2010/05/sintel/trailer.mp4', let src = $ref({ type: '', src: '' })
CreationTime: '2022-07-07T09:17:09Z',
Height: 360,
Width: 640,
JobId: '5d679a7ddacd479da554ad825ca6199a'
},
{
Status: 'Normal',
StreamType: 'video',
HDRType: 'other',
Size: 6089320,
Definition: 'LD',
Fps: '25',
Specification: 'H264.SD',
ModificationTime: '2022-07-07T09:17:14Z',
Duration: '52.2251',
Bitrate: '932.78',
BitDepth: 8,
Encrypt: 0,
PreprocessStatus: 'UnPreprocess',
Format: 'm3u8',
NarrowBandType: '0',
PlayURL:
'https://vod.ezijing.com/cd737c13318749668e2d43ba85834637/c4037699664e457a861ef50f978ad639-a072aad43705bb8db65e0eb0befd0353-ld.m3u8?auth_key=1657933158-c408366eb5e14c239e06854a312258ad-0-1afb40ec08bef7139e9cf1df05289118',
CreationTime: '2022-07-07T09:17:09Z',
Height: 540,
Width: 960,
JobId: '7e504265b68748d48c5ee719668cff6d'
},
{
Status: 'Normal',
StreamType: 'video',
HDRType: 'other',
Size: 9788972,
Definition: 'SD',
Fps: '25',
Specification: 'H264.SD',
ModificationTime: '2022-07-07T09:17:16Z',
Duration: '52.2251',
Bitrate: '1499.504',
BitDepth: 8,
Encrypt: 0,
PreprocessStatus: 'UnPreprocess',
Format: 'm3u8',
NarrowBandType: '0',
PlayURL:
'https://vod.ezijing.com/cd737c13318749668e2d43ba85834637/c4037699664e457a861ef50f978ad639-786ec47c6129e95b464fa1a789c60fba-sd.m3u8?auth_key=1657933158-0dd779f01e9f457caf99397d5de6bc6c-0-dc818d66343fdc6510424b1f86bd9e41',
CreationTime: '2022-07-07T09:17:09Z',
Height: 720,
Width: 1280,
JobId: '0ece5ba795584a9eb1ad77b90859b45a'
},
{
Status: 'Normal',
StreamType: 'audio',
Size: 831803,
Definition: 'SQ',
Fps: '0',
Specification: 'Audio',
ModificationTime: '2022-07-07T09:17:14Z',
Duration: '51.9576',
Bitrate: '128.074',
Encrypt: 0,
PreprocessStatus: 'UnPreprocess',
Format: 'mp3',
NarrowBandType: '0',
PlayURL:
'https://vod.ezijing.com/cd737c13318749668e2d43ba85834637/c4037699664e457a861ef50f978ad639-58975598a5cef52ba59f3a18af9c98a8-sq.mp3?auth_key=1657933158-1385695e64154355b775f1cebb924f26-0-fec34e38bbdb9973a4e577b89e123232',
CreationTime: '2022-07-07T09:17:09Z',
Height: 0,
Width: 0,
JobId: 'ba0bb88b427048cab7d4abe8a112833f'
}
]
let src = $ref({ src: '/trailer.mp4', type: 'video/mp4' })
// 跳过片头 // 跳过片头
const isSkip = useStorage('isSkip', false) const isSkip = useStorage('isSkip', false)
// 连续播放 // 连续播放
...@@ -108,8 +36,8 @@ const onReady = (player: VideoJsPlayer) => { ...@@ -108,8 +36,8 @@ const onReady = (player: VideoJsPlayer) => {
isReady = true isReady = true
console.log(videoJsPlayer) console.log(videoJsPlayer)
} }
function changeSrc(data: any) { function changeSrc(data: PlayItemTypes) {
console.log(data) // src = { src: data.PlayURL, type: 'application/x-mpegURL' }
src = { src: data.PlayURL, type: 'video/mp4' } src = { src: data.PlayURL, type: 'video/mp4' }
} }
</script> </script>
...@@ -125,7 +53,7 @@ function changeSrc(data: any) { ...@@ -125,7 +53,7 @@ function changeSrc(data: any) {
</button> </button>
</template> </template>
<ul> <ul>
<li v-for="(item, index) in playList" :key="index" @click="changeSrc(item)">{{ item.Definition }}</li> <li v-for="(item, index) in currentPlayList" :key="index" @click="changeSrc(item)">{{ item.Definition }}</li>
</ul> </ul>
</el-popover> </el-popover>
<el-popover trigger="hover" effect="dark" placement="top" :teleported="false"> <el-popover trigger="hover" effect="dark" placement="top" :teleported="false">
......
<!-- 学习 --> <!-- 学习 -->
<script setup lang="ts"> <script setup lang="ts">
import { Star } from '@element-plus/icons-vue'
import type { ChapterType } from '@/types'
import * as api from '../api' import * as api from '../api'
let chapterList = $ref<CourseType[]>([ let chapterList = $ref<ChapterType[]>([])
{
id: '6952463551923486720',
resource_type: 1,
resource_id: '0',
name: '第一章 测试基础',
lft: 2,
rgt: 19,
depth: 1,
sections: [
{
id: '6952463626879893504',
resource_type: 1,
resource_id: '0',
name: '1.1 软件测试流程',
lft: 3,
rgt: 18,
depth: 2,
resources: [
{
id: '6952463676506898432',
resource_type: 2,
resource_id: '6952457870440923136',
name: '自考和成考毕业证哪一个更好呢? - 知乎',
lft: 4,
rgt: 5,
depth: 3,
collection_count: 0,
info: {
id: '6952457870440923136',
name: '自考和成考毕业证哪一个更好呢? - 知乎',
length: 60,
size: 11522066,
cover: 'https://img1.ezijing.com/curriculum/vods/904f6f495d377aa905e1b20f19c2b1cd.jpg',
pdf: '',
source_id: '8b2c9319016d4e56b13fb629bde66b62'
}
},
{
id: '6952463714540847104',
resource_type: 10,
resource_id: '6952458453805694976',
name: '2022届毕业论文模板',
lft: 6,
rgt: 7,
depth: 3,
collection_count: 0,
info: {}
},
{
id: '6952463732052066304',
resource_type: 11,
resource_id: '6952458579903250432',
name: '葛涵芮(改)',
lft: 8,
rgt: 9,
depth: 3,
collection_count: 0,
info: {}
},
{
id: '6952463756471304192',
resource_type: 4,
resource_id: '6952458799777054720',
name: '阿里云镜像服务私有仓库',
lft: 10,
rgt: 11,
depth: 3,
collection_count: 0,
info: {}
},
{
id: '6952463779271540736',
resource_type: 3,
resource_id: '405319875141836801',
name: '账号006 测试课后作业',
lft: 12,
rgt: 13,
depth: 3,
collection_count: 0,
info: {}
},
{
id: '6952463812154884096',
resource_type: 9,
resource_id: '405320397106192384',
name: '账号006 测试考试-全题型1',
lft: 14,
rgt: 15,
depth: 3,
collection_count: 0,
info: {}
},
{
id: '6952463848183955456',
resource_type: 6,
resource_id: '6951001370124091392',
name: '张传梁测试使用的直播0708',
lft: 16,
rgt: 17,
depth: 3,
collection_count: 0,
info: {}
}
]
}
]
},
{
id: '6952463591937146880',
resource_type: 1,
resource_id: '0',
name: '第二章 专项测试',
lft: 20,
rgt: 27,
depth: 1,
sections: [
{
id: '6952463902990925824',
resource_type: 1,
resource_id: '0',
name: '2.1 移动端测试',
lft: 21,
rgt: 26,
depth: 2,
resources: [
{
id: '6952463957328134144',
resource_type: 2,
resource_id: '6952458166898524160',
name: '转转APP宣传动画_影视_Motion Graphic_CCLab - 原创作品 - 站酷 (ZCOOL)',
lft: 22,
rgt: 23,
depth: 3,
collection_count: 0,
info: {
id: '6952458166898524160',
name: '转转APP宣传动画_影视_Motion Graphic_CCLab - 原创作品 - 站酷 (ZCOOL)',
length: 16,
size: 2624963,
cover: 'https://img1.ezijing.com/curriculum/vods/64f2f365d25e4e3cc9e68e6e72ef3fde.jpg',
pdf: '',
source_id: 'aa0cf2146067476aaf4c2d4e61ae6f96'
}
},
{
id: '6952464226233352192',
resource_type: 4,
resource_id: '6952459029746548736',
name: 'apache-ant-1.10.1',
lft: 24,
rgt: 25,
depth: 3,
collection_count: 0,
info: {}
}
]
}
]
}
])
const { query } = useRoute() const { query } = useRoute()
const courseId = $ref<string | null>(query.course_id as string) const courseId = $ref(query.course_id as string)
const semesterId = $ref<string | null>(query.semester_id as string) const semesterId = $ref(query.semester_id as string)
// 获取章节列表 // 获取章节列表
function fetchList() { function fetchList() {
if (!courseId || !semesterId) { if (!courseId || !semesterId) {
...@@ -179,18 +21,23 @@ onMounted(() => { ...@@ -179,18 +21,23 @@ onMounted(() => {
}) })
</script> </script>
<template> <template>
<!-- <router-link to="/course/player">学习</router-link> --> <el-collapse class="course-chapters">
<el-collapse> <el-collapse-item :name="item.id" v-for="item in chapterList" :key="item.id">
<el-collapse-item :title="item.name" :name="item.id" v-for="item in chapterList" :key="item.id"> <template #title><i class="icon-chapter"></i>{{ item.name }}</template>
<el-collapse> <el-collapse class="course-sections">
<el-collapse-item :title="section.name" :name="section.id" v-for="section in item.sections" :key="section.id"> <el-collapse-item :name="section.id" v-for="section in item.sections" :key="section.id">
<template #title><i class="icon-chapter"></i>{{ item.name }}</template>
<ul> <ul>
<li v-for="resource in section.resources" :key="resource.id"> <li v-for="resource in section.resources" :key="resource.id">
<router-link <router-link
:to="`/course/player?course_id=${courseId}&chapter_id=${section.id}&semester_id=${semesterId}&resource_id=${resource.resource_id}`" :to="`/course/player?course_id=${courseId}&section_id=${
section.id
}&semester_id=${semesterId}&&source_id=${resource.info?.source_id || ''}`"
target="_blank"
> >
{{ resource.name }} {{ resource.name }}
</router-link> </router-link>
<el-icon><Star /></el-icon>
</li> </li>
</ul> </ul>
</el-collapse-item> </el-collapse-item>
...@@ -198,3 +45,53 @@ onMounted(() => { ...@@ -198,3 +45,53 @@ onMounted(() => {
</el-collapse-item> </el-collapse-item>
</el-collapse> </el-collapse>
</template> </template>
<style lang="scss" scoped>
li {
display: flex;
align-items: center;
padding: 0 48px 0 60px;
height: 48px;
line-height: 48px;
border-bottom: 1px solid #e6e6e6;
a {
flex: 1;
}
}
.icon-chapter {
margin-right: 10px;
display: inline-block;
width: 12px;
height: 14px;
background: url(@/assets/images/icon_chapter.png);
background-size: contain;
}
.course-chapters {
:deep(.el-collapse-item__header) {
padding: 0 20px;
background-color: #e8e9eb;
border-bottom: none;
}
}
.course-sections {
margin-top: 10px;
:deep(.el-collapse-item__header) {
padding: 0 40px;
background-color: #f7f8fa;
border-bottom: none;
}
}
.el-collapse {
border-top: none;
border-bottom: none;
}
:deep(.el-collapse-item__wrap) {
border-bottom: none;
}
:deep(.el-collapse-item__content) {
padding-bottom: 0;
}
:deep(.el-collapse-item) {
margin-bottom: 10px;
}
</style>
<!-- 大作业 --> <!-- 大作业 -->
<script setup lang="ts"></script> <script setup lang="ts">
<template>大作业</template> import AppUpload from '@/components/base/AppUpload.vue'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { getCourseWork, submitCourseWork } from '../api'
const { query } = useRoute()
const courseId = $ref(query.course_id as string)
const semesterId = $ref(query.semester_id as string)
const formRef = $ref<FormInstance>()
const form = reactive({
id: '',
course_id: courseId,
semester_id: semesterId,
title: '',
content: '',
attachments: []
})
const rules = ref<FormRules>({
title: [{ required: true, message: '请输入作业标题', trigger: 'blur' }],
content: [{ required: true, message: '请输入作业正文', trigger: 'blur' }]
})
const disabled = computed(() => !!form.id)
// 提交
function handleSubmit() {
formRef?.validate().then(update)
}
// 修改
const update = () => {
const params = Object.assign({}, form, { attachments: JSON.stringify(form.attachments) })
submitCourseWork(params).then(() => {
ElMessage({ message: '保存成功', type: 'success' })
})
}
// 获取作业
function fetchInfo() {
getCourseWork({ course_id: courseId, semester_id: semesterId }).then(res => {
const { detail } = res.data
Object.assign(form, detail)
})
}
onMounted(() => {
fetchInfo()
})
</script>
<template>
<el-form ref="formRef" :model="form" :rules="rules" hide-required-asterisk label-position="top" disabled>
<el-form-item label="作业标题" prop="old_password">
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="作业正文" prop="password">
<el-input type="textarea" v-model="form.content" />
</el-form-item>
<el-form-item label="上传附件" prop="password_r">
<AppUpload v-model="form.attachments" disabled></AppUpload>
</el-form-item>
<el-form-item>
<el-button type="primary" round auto-insert-space @click="handleSubmit">保存</el-button>
</el-form-item>
</el-form>
</template>
export interface CourseListParamsTypes {
[key: string]: any
id: string
semester_ids: string[]
elective_types: string[]
}
export interface PlayItemTypes {
BitDepth: number
Bitrate: string
CreationTime: string
Definition: string
Duration: string
Encrypt: 0
Format: string
Fps: string
HDRType: string
Height: number
JobId: string
ModificationTime: string
NarrowBandType: string
PlayURL: string
PreprocessStatus: string
Size: number
Specification: string
Status: string
StreamType: string
Width: number
}
<script setup lang="ts"> <script setup lang="ts">
import { Search, Filter } from '@element-plus/icons-vue' import CourseListSearch from '../components/CourseListSearch.vue'
import CourseListItem from '../components/CourseListItem.vue' import CourseListItem from '../components/CourseListItem.vue'
import type { CourseType } from '@/types' import type { CourseType } from '@/types'
import * as api from '../api' import type { CourseListParamsTypes } from '../types'
let courseList = $ref<CourseType[]>([ import { getCourseList } from '../api'
{
id: '408283947030548481',
course_id: '6952463518419386368',
semester_id: '6954980183342317568',
schedule: '0.00',
watch_video_length: 100,
is_finished: 0,
is_top: 1,
name: '账号006的第一门课程',
cover: 'https://webapp-pub.oss-cn-beijing.aliyuncs.com/center_resource/course-cover.png',
elective_type: 2,
online_type: 2,
section: {
id: '408286099845160961',
name: '1.1 软件测试流程',
schedule: '1.00',
watch_video_length: 100,
is_finished: 0
},
semester: {
id: '6954980183342317568',
name: '第一学期'
}
}
])
function handleSelect() {}
function querySearch() {}
const filter = reactive<Record<string, any>>({
name: [],
type: []
})
const filterList = [
{
model: 'name',
label: '学期',
options: [
{
label: '第一学期',
value: '第一学期'
},
{
label: '第二学期',
value: '第二学期'
}
]
},
{
model: 'type',
label: '课程类型',
options: [
{
label: '必修课',
value: '必修课'
},
{
label: '选修课',
value: '选修课'
},
{
label: '重修课',
value: '重修课'
}
]
}
]
// 列表参数
const listParams = reactive<CourseListParamsTypes>({ id: '', semester_ids: [], elective_types: [] })
let courseList = $ref<CourseType[]>([])
// 获取课程列表
function fetchList() { function fetchList() {
api.getCourseList({ id: '' }).then(res => { const params = Object.assign({}, listParams, {
semester_ids: listParams.semester_ids?.length ? JSON.stringify(listParams.semester_ids) : '',
elective_types: listParams.elective_types?.length ? JSON.stringify(listParams.elective_types) : ''
})
getCourseList(params).then(res => {
courseList = res.data.data courseList = res.data.data
}) })
} }
// 筛选
function handleSearch(params: CourseListParamsTypes) {
Object.assign(listParams, params)
fetchList()
}
onMounted(() => { onMounted(() => {
fetchList() fetchList()
}) })
...@@ -83,32 +31,9 @@ onMounted(() => { ...@@ -83,32 +31,9 @@ onMounted(() => {
<template> <template>
<section class="course"> <section class="course">
<div class="course-left"> <div class="course-left">
<div class="course-search"> <CourseListSearch @change="handleSearch"></CourseListSearch>
<el-autocomplete
:fetch-suggestions="querySearch"
placeholder="搜索"
@select="handleSelect"
:prefix-icon="Search"
class="course-search__input"
/>
<el-popover placement="bottom-end" :width="292" trigger="click">
<template #reference>
<el-button :icon="Filter" class="course-search-filter__button"></el-button>
</template>
<div class="course-search-filter">
<dl class="course-search-filter-item" v-for="item in filterList" :key="item.model">
<dt class="course-search-filter-item__label">{{ item.label }}</dt>
<dd class="course-search-filter-item__main">
<el-checkbox-group v-model="filter[item.model]">
<el-checkbox-button v-for="option in item.options" v-bind="option" :key="option.value" />
</el-checkbox-group>
</dd>
</dl>
</div>
</el-popover>
</div>
<div class="course-list"> <div class="course-list">
<CourseListItem v-for="item in courseList" :data="item" :key="item.id"></CourseListItem> <CourseListItem v-for="item in courseList" :data="item" :key="item.id" @change="fetchList"></CourseListItem>
</div> </div>
</div> </div>
<div class="course-right"><router-view :key="$route.fullPath"></router-view></div> <div class="course-right"><router-view :key="$route.fullPath"></router-view></div>
...@@ -126,46 +51,11 @@ onMounted(() => { ...@@ -126,46 +51,11 @@ onMounted(() => {
.course-left { .course-left {
padding: 18px; padding: 18px;
width: 354px; width: 354px;
flex: 0 0 354px;
overflow-y: auto; overflow-y: auto;
border-right: 1px solid #e6e6e6; border-right: 1px solid #e6e6e6;
box-sizing: border-box; box-sizing: border-box;
} }
.course-search {
display: flex;
align-items: center;
}
.course-search__input {
flex: 1;
}
.course-search-filter__button {
margin-left: 10px;
padding: 8px;
}
.course-search-filter {
padding: 8px;
}
.course-search-filter-item + .course-search-filter-item {
margin-top: 20px;
}
.course-search-filter-item__label {
font-size: 16px;
line-height: 1;
color: #333;
}
.course-search-filter-item__main {
margin-top: 12px;
.el-checkbox-group {
display: flex;
justify-content: space-between;
}
.el-checkbox-button__inner {
padding: 10px 20px;
border: 1px solid #adadad !important;
border-radius: 18px !important;
box-shadow: none !important;
}
}
// 右侧 // 右侧
.course-right { .course-right {
flex: 1; flex: 1;
......
<script setup lang="ts"> <script setup lang="ts">
import { getCourseSection } from '../api'
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'))
const { query } = useRoute()
const semesterId = $ref(query.semester_id as string)
const sectionId = $ref(query.section_id as string)
const sourceId = $ref(query.source_id as string)
function fetchInfo() {
getCourseSection({ section_id: sectionId, semester_id: semesterId }).then(res => {
console.log(res)
})
}
onMounted(fetchInfo)
</script> </script>
<template> <template>
<div class="course-player"> <div class="course-player">
<div class="course-player-main"> <div class="course-player-main">
<CoursePlayerVideo /> <CoursePlayerVideo v-if="sourceId" />
<el-tabs> <el-tabs>
<el-tab-pane label="课件"> </el-tab-pane> <el-tab-pane label="课件"> </el-tab-pane>
<el-tab-pane label="教案" lazy> </el-tab-pane> <el-tab-pane label="教案" lazy> </el-tab-pane>
......
<script setup lang="ts"> <script setup lang="ts">
import { useMapStore } from '@/stores/map'
import type { CourseType } from '@/types' import type { CourseType } from '@/types'
import * as api from '../api' import * as api from '../api'
const CourseViewChapter = defineAsyncComponent(() => import('../components/CourseViewChapter.vue')) const CourseViewChapter = defineAsyncComponent(() => import('../components/CourseViewChapter.vue'))
const CourseViewBBS = defineAsyncComponent(() => import('../components/CourseViewBBS.vue')) const CourseViewBBS = defineAsyncComponent(() => import('../components/CourseViewBBS.vue'))
const CourseViewWork = defineAsyncComponent(() => import('../components/CourseViewWork.vue')) const CourseViewWork = defineAsyncComponent(() => import('../components/CourseViewWork.vue'))
...@@ -12,104 +14,119 @@ const { query } = useRoute() ...@@ -12,104 +14,119 @@ const { query } = useRoute()
const courseId = $ref<string | null>(query.course_id as string) const courseId = $ref<string | null>(query.course_id as string)
const semesterId = $ref<string | null>(query.semester_id as string) const semesterId = $ref<string | null>(query.semester_id as string)
let loading = $ref<boolean>(false)
const mapStore = useMapStore()
// 选课类型
const electiveTypes = mapStore.getMapValuesByKey('system_elective_type')
// 选课类型文本
const electiveTypeText = computed(() => {
return electiveTypes.find(item => parseInt(item.value) === detail.elective_type)?.label || ''
})
const detail = reactive<CourseType>({ const detail = reactive<CourseType>({
id: '6952463518419386368', id: '',
name: '账号006的第一门课程', course_id: '',
cover: 'https://webapp-pub.oss-cn-beijing.aliyuncs.com/center_resource/course-cover.png', semester_id: '',
online_type: 2, schedule: '',
elective_type: 2,
represent: '', represent: '',
credit: 5, watch_video_length: 0,
is_finished: 0,
is_top: 0,
name: '',
cover: '',
elective_type: 0,
online_type: 0,
semester: { id: '', name: '' },
credit: 0,
previous_preparation: '', previous_preparation: '',
target: '', target: '',
semester: { lecturers: []
id: '6954980183342317568',
name: '第一学期',
start_time: '2027-08-31',
end_time: '2023-01-31'
},
lecturers: [
{
id: '6942651161874792448',
name: '凤姐',
avatar: 'https://webapp-pub.ezijing.com/upload/admin/b26d67c22a8380970766e27af3278d6a.png',
summarize:
'<p>真正的产品应该是有一个端到端的一个解决方案。比如说:电子阅读中的从购书,到阅读,再到阅读心得分享,再到推荐,这一整套的解决方案。看看苹果的产品的端到端的解决方案,就知道什么是产品的样子了。</p>\n<p>真正的产品应该是有价值的。这种价值表现在&mdash;&mdash;你可以从中获得有价值的内容,并且你也可以通过他创造对你有价值的东西。比如,像豆瓣,像Stackoverflow,甚至像Twitter和微博这样让信息平等让信息传递更快的社区,或是像AWS或是Apple的开发平台,等等。可见,我们无法通过QQ获得有价值的东西,我们也无法通过QQ创造有价值的东西。</p>'
},
{
id: '6942651161874792449',
name: '凤姐',
avatar: 'https://webapp-pub.ezijing.com/upload/admin/b26d67c22a8380970766e27af3278d6a.png',
summarize:
'<p>真正的产品应该是有一个端到端的一个解决方案。比如说:电子阅读中的从购书,到阅读,再到阅读心得分享,再到推荐,这一整套的解决方案。看看苹果的产品的端到端的解决方案,就知道什么是产品的样子了。</p>\n<p>真正的产品应该是有价值的。这种价值表现在&mdash;&mdash;你可以从中获得有价值的内容,并且你也可以通过他创造对你有价值的东西。比如,像豆瓣,像Stackoverflow,甚至像Twitter和微博这样让信息平等让信息传递更快的社区,或是像AWS或是Apple的开发平台,等等。可见,我们无法通过QQ获得有价值的东西,我们也无法通过QQ创造有价值的东西。</p>'
}
]
}) })
// 获取课程信息 // 获取课程信息
function fetchInfo() { function fetchInfo() {
if (!courseId || !semesterId) { if (!courseId || !semesterId) {
return return
} }
loading = true
api.getCourse({ course_id: courseId, semester_id: semesterId }).then(res => { api.getCourse({ course_id: courseId, semester_id: semesterId }).then(res => {
Object.assign(detail, res.data.detail) Object.assign(detail, res.data.detail)
loading = false
}) })
} }
onMounted(() => { onMounted(() => {
fetchInfo() fetchInfo()
}) })
const dialogInfo = reactive({
visible: false,
title: '',
content: ''
})
function showInfo(title: string, content: string) {
dialogInfo.visible = true
dialogInfo.title = title
dialogInfo.content = content
}
</script> </script>
<template> <template>
<section class="course-view-hd"> <div class="course-view" v-loading="loading">
<div class="course-info"> <section class="course-view-hd">
<h1>{{ detail.name }}</h1> <div class="course-info">
<ul> <h1>{{ detail.name }}</h1>
<li>{{ detail.elective_type }}</li> <ul>
<li>{{ detail.credit }}学分</li> <li>{{ electiveTypeText }}</li>
<li v-if="detail.semester">{{ detail.semester.name }}</li> <li>{{ detail.credit }}学分</li>
</ul> <li v-if="detail.semester">{{ detail.semester.name }}</li>
<el-button round>课程简介</el-button> </ul>
<el-button round>预备知识</el-button> <el-button round @click="showInfo('课程简介', detail.represent)">课程简介</el-button>
<el-button round>授课目标</el-button> <el-button round @click="showInfo('预备知识', detail.previous_preparation)">预备知识</el-button>
<el-button round @click="showInfo('授课目标', detail.target)">授课目标</el-button>
</div>
<div class="course-lecturers" v-if="detail.lecturers?.length">
<el-carousel :autoplay="false" indicator-position="none" arrow="always" height="126px">
<el-carousel-item v-for="item in detail.lecturers" :key="item.id" class="lecturer-item">
<img :src="item.avatar" class="lecturer-item__pic" />
<div class="lecturer-item__info">
<h2>{{ item.name }}</h2>
<div v-html="item.summarize"></div>
</div>
</el-carousel-item>
</el-carousel>
</div>
</section>
<div class="course-view-bd">
<el-tabs>
<el-tab-pane label="学习">
<CourseViewChapter />
</el-tab-pane>
<el-tab-pane label="论坛" lazy>
<CourseViewBBS />
</el-tab-pane>
<el-tab-pane label="大作业" lazy>
<CourseViewWork />
</el-tab-pane>
<el-tab-pane label="考试" lazy>
<CourseViewExam />
</el-tab-pane>
<el-tab-pane label="课程考核" lazy>
<CourseViewAssess />
</el-tab-pane>
<el-tab-pane label="直播" lazy>
<CourseViewLive />
</el-tab-pane>
</el-tabs>
</div> </div>
<div class="course-lecturers">
<el-carousel :autoplay="false" indicator-position="none" arrow="always" height="126px">
<el-carousel-item v-for="item in detail.lecturers" :key="item.id" class="lecturer-item">
<img :src="item.avatar" class="lecturer-item__pic" />
<div class="lecturer-item__info">
<h2>{{ item.name }}</h2>
<div v-html="item.summarize"></div>
</div>
</el-carousel-item>
</el-carousel>
</div>
</section>
<div class="course-view-bd">
<el-tabs>
<el-tab-pane label="学习">
<CourseViewChapter />
</el-tab-pane>
<el-tab-pane label="论坛" lazy>
<CourseViewBBS />
</el-tab-pane>
<el-tab-pane label="大作业" lazy>
<CourseViewWork />
</el-tab-pane>
<el-tab-pane label="考试" lazy>
<CourseViewExam />
</el-tab-pane>
<el-tab-pane label="课程考核" lazy>
<CourseViewAssess />
</el-tab-pane>
<el-tab-pane label="直播" lazy>
<CourseViewLive />
</el-tab-pane>
</el-tabs>
</div> </div>
<el-dialog v-model="dialogInfo.visible" :title="dialogInfo.title" width="472px" center>
<div v-html="dialogInfo.content"></div>
</el-dialog>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.course-view {
height: 100%;
}
.course-view-hd { .course-view-hd {
display: flex; display: flex;
padding: 20px; padding: 20px;
......
...@@ -40,5 +40,3 @@ export const useMapStore = defineStore({ ...@@ -40,5 +40,3 @@ export const useMapStore = defineStore({
} }
} }
}) })
useMapStore().getMapList()
...@@ -73,7 +73,7 @@ export interface CourseType { ...@@ -73,7 +73,7 @@ export interface CourseType {
credit: number credit: number
previous_preparation: string previous_preparation: string
target: string target: string
lecturers: LecturerType[] lecturers?: LecturerType[]
} }
// 章节信息 // 章节信息
...@@ -89,8 +89,8 @@ export interface ChapterType { ...@@ -89,8 +89,8 @@ export interface ChapterType {
export interface SemesterType { export interface SemesterType {
id: string id: string
name: string name: string
start_time: string start_time?: string
end_time: string end_time?: string
} }
export interface LecturerType { export interface LecturerType {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论