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

chore: update

上级 e58a1c6d
...@@ -10,7 +10,9 @@ module.exports = { ...@@ -10,7 +10,9 @@ module.exports = {
'./.eslintrc-auto-import.json' './.eslintrc-auto-import.json'
], ],
rules: { rules: {
'vue/no-mutating-props': 'off',
'vue/multi-word-component-names': 'off', 'vue/multi-word-component-names': 'off',
'vue/no-setup-props-destructure': 'off',
'@typescript-eslint/no-explicit-any': 'off' '@typescript-eslint/no-explicit-any': 'off'
} }
} }
差异被折叠。
...@@ -3,12 +3,11 @@ ...@@ -3,12 +3,11 @@
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"dev": "vite --mode dev", "dev": "vite --mode dev",
"build": "run-p type-check build-only --mode prod && npm run deploy", "build": "vue-tsc --noEmit && vite build --mode prod && npm run deploy",
"build:test": "run-p type-check build-only --mode test", "build:test": "vue-tsc --noEmit && vite build --mode test",
"build:pre": "run-p type-check build-only --mode pre", "build:pre": "vue-tsc --noEmit && vite build --mode pre",
"preview": "vite preview --port 4173", "preview": "vite preview --port 4173",
"build-only": "vite build", "typecheck": "vue-tsc --noEmit",
"type-check": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"deploy": "node ./deploy.js" "deploy": "node ./deploy.js"
}, },
...@@ -42,7 +41,6 @@ ...@@ -42,7 +41,6 @@
"ali-oss": "^6.17.1", "ali-oss": "^6.17.1",
"eslint": "^8.5.0", "eslint": "^8.5.0",
"eslint-plugin-vue": "^9.3.0", "eslint-plugin-vue": "^9.3.0",
"npm-run-all": "^4.1.5",
"sass": "^1.54.0", "sass": "^1.54.0",
"typescript": "~4.7.4", "typescript": "~4.7.4",
"unplugin-auto-import": "^0.10.1", "unplugin-auto-import": "^0.10.1",
......
...@@ -2,7 +2,7 @@ import httpRequest from '@/utils/axios' ...@@ -2,7 +2,7 @@ import httpRequest from '@/utils/axios'
// 获取用户信息 // 获取用户信息
export function getUser() { export function getUser() {
return httpRequest.get('/api/resource/v1/util/info') return httpRequest.get('/api/usercenter/v2/frontend/user/get-user-info')
} }
// 退出登录 // 退出登录
......
...@@ -43,7 +43,7 @@ function handleClick(path: string) { ...@@ -43,7 +43,7 @@ function handleClick(path: string) {
<nav class="nav"> <nav class="nav">
<el-menu collapse :default-active="defaultActive" class="app-menu"> <el-menu collapse :default-active="defaultActive" class="app-menu">
<template v-for="item in menus" :key="item.path"> <template v-for="item in menus" :key="item.path">
<el-sub-menu :index="item.path" v-permission="item.tag" v-if="item.children"> <el-sub-menu :index="item.path" v-if="item.children">
<template #title> <template #title>
<el-icon><component :is="item.icon"></component></el-icon>{{ item.name }} <el-icon><component :is="item.icon"></component></el-icon>{{ item.name }}
</template> </template>
...@@ -51,13 +51,12 @@ function handleClick(path: string) { ...@@ -51,13 +51,12 @@ function handleClick(path: string) {
:index="subitem.path" :index="subitem.path"
v-for="subitem in item.children" v-for="subitem in item.children"
:key="subitem.path" :key="subitem.path"
v-permission="subitem.tag"
@click="handleClick(subitem.path)" @click="handleClick(subitem.path)"
> >
{{ subitem.name }} {{ subitem.name }}
</el-menu-item> </el-menu-item>
</el-sub-menu> </el-sub-menu>
<el-menu-item :index="item.path" v-permission="item.tag" @click="handleClick(item.path)" v-else> <el-menu-item :index="item.path" @click="handleClick(item.path)" v-else>
<el-icon><component :is="item.icon"></component></el-icon>{{ item.name }} <el-icon><component :is="item.icon"></component></el-icon>{{ item.name }}
</el-menu-item> </el-menu-item>
</template> </template>
......
...@@ -33,7 +33,7 @@ const logout = async () => { ...@@ -33,7 +33,7 @@ const logout = async () => {
<img :src="userInfo.avatar || 'https://webapp-pub.ezijing.com/website/base/images/avatar.svg'" /> <img :src="userInfo.avatar || 'https://webapp-pub.ezijing.com/website/base/images/avatar.svg'" />
</div> </div>
<div class="app-header-user-main"> <div class="app-header-user-main">
<h3>{{ userInfo.name }}</h3> <h3>{{ userStore.name }}</h3>
<p>{{ userInfo.email || userInfo.mobile }}</p> <p>{{ userInfo.email || userInfo.mobile }}</p>
</div> </div>
<div class="app-header-user-buttons"> <div class="app-header-user-buttons">
......
...@@ -13,13 +13,11 @@ import AppList from '@/components/base/AppList.vue' ...@@ -13,13 +13,11 @@ import AppList from '@/components/base/AppList.vue'
import modules from './modules' import modules from './modules'
import { permissionDirective } from '@/utils/permission'
import { useMapStore } from '@/stores/map' import { useMapStore } from '@/stores/map'
const app = createApp(App) const app = createApp(App)
// 注册公共组件 // 注册公共组件
app.component('AppCard', AppCard).component('AppList', AppList) app.component('AppCard', AppCard).component('AppList', AppList)
app.directive('permission', permissionDirective)
// 注册模块 // 注册模块
modules({ router }) modules({ router })
......
...@@ -103,3 +103,40 @@ export function uploadVideoRecords(data: { ...@@ -103,3 +103,40 @@ export function uploadVideoRecords(data: {
}) { }) {
return httpRequest.post('/api/saas/api/v1/course/video/upload-records', data) return httpRequest.post('/api/saas/api/v1/course/video/upload-records', data)
} }
/**
*
* 考试、测验start
*/
// 获取试卷详情
export function getPaper(data: { course_id: string; semester_id: string; paper_id: string; type: number }) {
return httpRequest.post('/api/saas/api/v1/question-bank/paper-question/detail', data)
}
// 缓存试卷
export function cachePaper(data: {
course_id: string
semester_id: string
paper_id: string
type: number
question: any
}) {
return httpRequest.post('/api/saas/api/v1/question-bank/paper-question/cache', data)
}
// 提交试卷
export function submitPaper(data: {
course_id: string
semester_id: string
paper_id: string
type: number
question: any
}) {
return httpRequest.post('/api/saas/api/v1/question-bank/paper/hand', data, {
headers: { 'Content-Type': 'application/json' }
})
}
/**
*
* 考试、测验end
*/
<script setup lang="ts">
import type { PaperQuestionType } from '@/types'
import { ElMessage } from 'element-plus'
import { ArrowLeftBold } from '@element-plus/icons-vue'
import { cachePaper, submitPaper } from '../api'
import CourseExamQuestion from './CourseExamQuestion.vue'
import CourseExamQuestionNumbers from './CourseExamQuestionNumbers.vue'
interface Props {
status: number
title?: string
submitButtonText?: string
}
const props = withDefaults(defineProps<Props>(), { title: '考试', submitButtonText: '交卷' })
const emit = defineEmits<{
(e: 'update'): void
}>()
const route = useRoute()
const courseId = $ref(route.query.course_id as string)
const semesterId = $ref(route.query.semester_id as string)
const paperId = $ref(route.query.paper_id as string)
const type = $ref(parseInt(route.query.type as string))
// 所有试题
const questionList = $(inject<PaperQuestionType[]>('questionList'))
// 试题索引
let questionIndex = $ref<number>(0)
provide('questionIndex', $$(questionIndex))
// 试题数量
const questionLength = $computed(() => {
return questionList.length
})
// 当前试题
const currentQuestion = $computed(() => {
return questionList[questionIndex] || {}
})
// 提交中
let submitLoading = $ref<boolean>(false)
// 是否禁用
const disabled = $computed(() => {
return props.status !== 1 || submitLoading
})
provide('disabled', $$(disabled))
// 上一题
function handlePrev() {
if (questionIndex < 1) return
questionIndex--
}
// 下一题
function handleNext() {
if (questionIndex === questionLength - 1) return
questionIndex++
}
// 提交
function handleSubmit() {
submitLoading = true
clearInterval(timer)
submitPaper({
course_id: courseId,
semester_id: semesterId,
paper_id: paperId,
type,
question: JSON.stringify(genSubmitQuestion(questionList))
}).then(() => {
ElMessage.success('提交成功')
emit('update')
})
}
// 自动提交
function handleAutoSubmit() {
if (disabled) return
cachePaper({
course_id: courseId,
semester_id: semesterId,
paper_id: paperId,
type,
question: JSON.stringify(genSubmitQuestion(questionList))
})
}
const timer = setInterval(handleAutoSubmit, 5000)
onUnmounted(() => {
clearInterval(timer)
})
// 生成提交试卷的数据
function genSubmitQuestion(questionList: PaperQuestionType[]) {
return questionList.map(question => {
const userAnswer = question.user_answer ? question.user_answer.split(',') : []
const options = question.question_options.map(item => {
return { ...item, user_checked: userAnswer.includes(item.id) }
})
if (question.children && question.children.length) {
question.children = genSubmitQuestion(question.children)
}
return { ...question, question_options: options }
})
}
</script>
<template>
<div class="course-exam-card">
<div class="course-exam-card-hd">
<router-link :to="`/course/view?course_id=${courseId}&semester_id=${semesterId}`">
<el-button :icon="ArrowLeftBold" circle></el-button>
</router-link>
<div class="title">{{ title }}</div>
</div>
<div class="course-exam-card-bd">
<div class="course-exam-left">
<div class="course-exam-scroll">
<CourseExamQuestion :question="currentQuestion" :index="questionIndex + 1">
<template #index>{{ questionIndex + 1 }}/{{ questionLength }}</template>
</CourseExamQuestion>
</div>
<div class="course-exam-buttons">
<el-button size="large" :disabled="questionIndex === 0" @click="handlePrev">上一题</el-button>
<el-button size="large" :disabled="questionIndex === questionLength - 1" @click="handleNext">
下一题
</el-button>
</div>
</div>
<div class="course-exam-right">
<div class="course-exam-scroll">
<CourseExamQuestionNumbers :index="questionIndex" :status="1" />
</div>
<div class="course-exam-buttons">
<el-button
size="large"
type="primary"
auto-insert-space
:disabled="disabled"
@click="handleSubmit"
style="width: 100%"
>
{{ submitButtonText }}
</el-button>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.course-exam-card {
display: flex;
flex-direction: column;
width: 100%;
height: calc(100vh - 105px);
min-height: 600px;
background-color: #fff;
overflow: hidden;
}
.course-exam-card-hd {
border-bottom: 1px solid #ccc;
height: 80px;
background: #ffffff;
display: flex;
align-items: center;
padding-left: 40px;
.title {
padding-left: 20px;
font-size: 24px;
font-weight: bold;
color: #222222;
line-height: 80px;
}
.right {
width: 260px;
margin-left: auto;
display: flex;
justify-content: space-around;
align-items: center;
.count {
font-size: 18px;
font-weight: bold;
color: #222222;
}
}
}
.course-exam-card-bd {
flex: 1;
display: flex;
overflow: hidden;
}
.course-exam-left {
flex: 1;
display: flex;
flex-direction: column;
.course-exam-scroll {
padding: 0 40px;
}
.course-exam-buttons {
padding: 20px 40px;
border-top: 1px solid #ccc;
}
}
.course-exam-right {
display: flex;
flex-direction: column;
border-left: 1px solid #ccc;
position: relative;
width: 220px;
background: #fff;
padding: 0 20px;
}
.course-exam-scroll {
flex: 1;
overflow-x: hidden;
overflow-y: auto;
}
.course-exam-buttons {
padding: 20px 0;
}
</style>
<script setup lang="ts">
import type { PaperQuestionType } from '@/types'
import CourseExamQuestionItem from './CourseExamQuestionItem.vue'
import { questionType } from '@/utils/dictionary'
interface Props {
index: number
question: PaperQuestionType
}
const props = defineProps<Props>()
// 试题类型
const questionTypeText = computed(() => {
return questionType[props.question.question_type] || props.question.question_type
})
</script>
<template>
<div class="course-exam-question">
<div class="course-exam-question-hd">
<h4 class="course-exam-question-hd__title">{{ questionTypeText }}</h4>
<aside class="course-exam-question-hd__aside"><slot name="index"></slot></aside>
</div>
<div class="course-exam-question-bd">
<h2 class="course-exam-question-title" v-if="question.common_content" v-html="question.common_content"></h2>
<template v-if="question.children && question.children.length">
<CourseExamQuestionItem
v-for="(item, index) in question.children"
:question="item"
:index="index + 1"
:key="item.id"
/>
</template>
<CourseExamQuestionItem :question="question" :index="index" v-else />
</div>
</div>
</template>
<style lang="scss" scoped>
.course-exam-question {
position: relative;
}
.course-exam-question-hd {
padding-top: 20px;
display: flex;
align-items: center;
height: 45px;
}
.course-exam-question-hd__title {
flex: 1;
font-size: 18px;
color: #222;
}
.course-exam-question-hd__aside {
font-size: 18px;
color: #222;
}
.course-exam-question-title {
margin: 20px 0 30px;
font-size: 18px;
color: #222;
.num {
font-size: 32px;
font-weight: bold;
color: #222;
line-height: 45px;
margin-top: 5px;
}
}
</style>
<script setup lang="ts">
import type { PaperQuestionType } from '@/types'
interface Props {
index: number
question: PaperQuestionType
}
const { question } = defineProps<Props>()
const disabled = $ref<boolean>(inject('disabled'))
// 处理后的options数据
const currentOptions = computed(() => {
if (!question.question_options) {
return []
}
return question.question_options.map((item: any, index: number) => {
// 英文字母 + 名称
item.abc = A_Z[index]
item.abc_option = `${A_Z[index]}. ${item.option}`
// 提交时的选中状态
// item.checked = this.question.user_answer.includes(item.id)
// 处理正确的选中状态
// const hasChecked = Object.prototype.hasOwnProperty.call(item, 'isRight')
// const rightAnswer = this.question.question_answer || []
// if (!hasChecked && rightAnswer) {
// item.isRight = Array.isArray(rightAnswer) ? rightAnswer.includes(item.id) : rightAnswer === item.id
// }
return item
})
})
// 26个英文字母
const A_Z = $computed(() => {
const result = []
for (let i = 0; i < 26; i++) {
result.push(String.fromCharCode(65 + i))
}
return result
})
const questionType = computed(() => {
return question.child_question_type || question.question_type
})
const value = computed({
get() {
return question.user_answer
},
set(value) {
question.user_answer = value
}
})
const checkboxValues = computed({
get() {
return question.user_answer ? question.user_answer.split(',') : []
},
set(values) {
question.user_answer = values.join(',')
}
})
</script>
<template>
<div class="question-item">
<div class="question-item-hd">
<div class="question-item-hd__num">{{ index }}.</div>
<div class="question-item-hd__title" v-html="question.question_content"></div>
</div>
<div class="question-item-bd">
<!-- 单选 -->
<template v-if="[1, 6].includes(questionType)">
<el-radio-group :disabled="disabled" v-model="value">
<div class="question-option-item" v-for="item in currentOptions" :key="item.id">
<el-radio :label="item.id">
<div class="question-option-item__text" v-html="item.abc_option"></div>
</el-radio>
</div>
</el-radio-group>
</template>
<!-- 多选 -->
<template v-if="questionType === 2">
<el-checkbox-group :disabled="disabled" v-model="checkboxValues">
<div class="question-option-item" v-for="item in currentOptions" :key="item.id">
<el-checkbox :label="item.id">
<div class="question-option-item__text" v-html="item.abc_option"></div>
</el-checkbox>
</div>
</el-checkbox-group>
</template>
<!-- 简答题 -->
<template v-if="questionType === 3">
<el-input
type="textarea"
placeholder="请输入答案内容"
:autosize="{ minRows: 4, maxRows: 6 }"
:disabled="disabled"
:maxlength="500"
:show-word-limit="true"
v-model="value"
></el-input>
</template>
</div>
<!-- <div class="question-item-ft" v-if="hasResult">
<h3 class="question-item-ft__title">答案解析</h3>
<template v-if="questionType !== 3">
<div class="answer-item">
<div class="answer-item-label">正确答案:</div>
<div class="answer-item-content">{{ correctAnswerText }}</div>
</div>
<div class="answer-item">
<div class="answer-item-label">您的答案:</div>
<div class="answer-item-content">{{ submitAnswerText }}</div>
</div>
</template>
<template v-else>
<div class="answer-item" v-if="question.comment">
<div class="answer-item-label">老师点评:</div>
<div class="answer-item-content">{{ question.comment }}</div>
</div>
</template>
<div class="answer-item" v-if="question.question_analysis">
<div class="answer-item-label">解析:</div>
<div class="answer-item-content" v-html="question.question_analysis"></div>
</div>
</div> -->
</div>
</template>
<style lang="scss" scoped>
.question-item {
margin-bottom: 40px;
}
.question-item-hd {
display: flex;
}
.question-item-hd__num {
font-size: 32px;
font-weight: bold;
color: #222;
line-height: 45px;
margin-top: 5px;
}
.question-item-hd__title {
margin-left: 5px;
padding-top: 18px;
font-size: 18px;
font-weight: bold;
color: #222;
line-height: 25px;
}
.question-option-item {
margin-top: 20px;
width: 100%;
}
.question-option-item__text {
display: inline-block;
font-size: 18px;
color: #222;
white-space: normal;
}
.question-item-ft {
margin-top: 20px;
}
.question-item-ft__title {
font-size: 18px;
font-weight: bold;
color: #222;
line-height: 25px;
}
.answer-item {
margin-top: 10px;
margin-left: 28px;
display: flex;
font-size: 18px;
color: #222;
line-height: 25px;
}
.answer-item-label {
white-space: nowrap;
}
.answer-item-content {
flex: 1;
overflow: hidden;
}
</style>
<script setup lang="ts">
import type { PaperQuestionType } from '@/types'
interface Props {
index: number
status: number
}
const props = defineProps<Props>()
// 所有试题
const questionList = $ref<PaperQuestionType[]>(inject('questionList'))
// 试题索引
let questionIndex = $ref<number>(inject('questionIndex'))
const questionNumTips: any = {
1: [
{ class: 'is-info', name: '已答' },
{ class: 'is-default', name: '未答' },
{ class: 'is-success', name: '当前' }
],
2: [
{ class: 'is-success', name: '答对' },
{ class: 'is-error', name: '答错' },
{ class: 'is-info', name: '未答' }
]
}
const questionNum = computed(() => {
return questionNumTips[props.status]
})
function genClass(data: any, index: number) {
// answer(0:未做,1:正确,2:错误)
if (props.status === 1) {
return {
'is-info': !!data.user_answer, // 已做
'is-success': index === questionIndex // 当前
}
}
if (props.status === 2) {
return {
'is-success': data.answer === 1, // 答对
'is-error': data.answer === 2, // 答错
'is-info': data.answer === 0 // 未答
}
}
if (props.status === 3) {
return {
'is-success': data.checked_flag, // 已批阅
'is-error': !data.checked_flag, // 未批阅
'is-info': data.answer === 0 // 未做
}
}
}
function handleClick(index: number) {
questionIndex = index
}
</script>
<template>
<div class="question-numbers">
<div class="question-num">
<!-- <div v-for="item in dataList" :key="item.question_item_id">
<div class="tit">{{ item.title }}</div> -->
<ul>
<li
v-for="(item, index) in questionList"
class="question-num-item"
:class="genClass(item, index)"
:key="index"
@click="handleClick(index)"
>
{{ index + 1 }}
</li>
</ul>
<!-- </div> -->
</div>
<ul class="question-num-tips">
<li v-for="(item, index) in questionNum" :key="index">
<div class="question-num-tips-item" :class="item.class"></div>
<div class="txt">{{ item.name }}</div>
</li>
</ul>
</div>
</template>
<style lang="scss" scoped>
.question-numbers {
height: 100%;
display: flex;
flex-direction: column;
}
.question-num {
flex: 1;
padding-top: 20px;
.tit {
font-size: 12px;
color: #999999;
line-height: 17px;
margin-bottom: 10px;
}
ul {
display: flex;
list-style: none;
padding: 0;
margin: 0;
flex-wrap: wrap;
}
}
.question-num-item {
cursor: pointer;
position: relative;
border-radius: 50px;
width: 24px;
height: 24px;
font-size: 14px;
line-height: 24px;
margin-right: 20px;
margin-bottom: 10px;
text-align: center;
border: 2px solid #ccc;
color: #666;
&:nth-child(5n) {
margin-right: 0;
}
}
.question-num-tips {
padding-bottom: 20px;
display: flex;
align-items: center;
justify-content: space-between;
.txt {
margin-top: 5px;
font-size: 12px;
color: #ccc;
line-height: 17px;
text-align: center;
}
}
.question-num-tips-item {
cursor: pointer;
position: relative;
border-radius: 50px;
width: 24px;
height: 24px;
font-size: 14px;
line-height: 24px;
text-align: center;
border: 2px solid #ccc;
color: #666;
}
.is-default {
color: #666;
border: 2px solid #ccc;
}
.is-info {
color: #fff;
background-color: #999;
border: 2px solid #999;
}
.is-success {
color: #666;
border: 2px solid #0fc118;
}
.is-info.is-success {
color: #fff;
}
.is-error {
color: #666;
border: 2px solid #c01540;
}
.is-mark::after {
content: '';
position: absolute;
top: -1px;
right: -1px;
width: 4px;
height: 4px;
background: #c01540;
border-radius: 50%;
}
</style>
...@@ -48,15 +48,20 @@ function handleTop(data: CourseListItemType) { ...@@ -48,15 +48,20 @@ function handleTop(data: CourseListItemType) {
</div> </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="parseFloat(data.schedule)" /></div>
</div> </div>
</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?.id"> <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.schedule }}%</p>
<router-link
:to="`/course/player?course_id=${data.course_id}&chapter_id=${data.section.chapter_id}&section_id=${data.section.section_id}&resource_id=${data.section.resource_id}&semester_id=${data.semester_id}`"
target="_blank"
>
<el-button type="primary" round>点击观看</el-button> <el-button type="primary" round>点击观看</el-button>
</router-link>
</div> </div>
<p class="t3" v-else>尚未观看</p> <p class="t3" v-else>尚未观看</p>
</div> </div>
......
...@@ -18,9 +18,11 @@ interface Props { ...@@ -18,9 +18,11 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
// 跳转链接 // 跳转链接
const targetHref = computed(() => { const targetUrl = computed(() => {
const info = props.data.info const info = props.data.info
if (['pptx', 'doc', 'docx', 'xls', 'xlsx'].includes(info.type)) { 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`
} else if (['pptx', 'doc', 'docx', 'xls', 'xlsx'].includes(info.type)) {
return `https://view.officeapps.live.com/op/view.aspx?src=${info.url}` return `https://view.officeapps.live.com/op/view.aspx?src=${info.url}`
} else { } else {
return info.url return info.url
...@@ -58,7 +60,7 @@ function downloadFile(data: CourseResourceType) { ...@@ -58,7 +60,7 @@ function downloadFile(data: CourseResourceType) {
<template> <template>
<div class="course-resource-item"> <div class="course-resource-item">
<p> <p>
<a :href="targetHref" target="_blank"> <a :href="targetUrl" target="_blank">
<ResourceIcon :resourceType="data.resource_type" /> <ResourceIcon :resourceType="data.resource_type" />
{{ data.name }} {{ data.name }}
</a> </a>
......
<script setup lang="ts"> <script setup lang="ts">
import type { CourseChapterType, CourseSectionType, CourseResourceType, VideoRecordType, PlayItemType } from '../types' import type { CourseChapterType, CourseResourceType, VideoRecordType, PlayItemType } from '../types'
import type { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js' import type { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js'
import { throttle } from 'lodash' import { throttle } from 'lodash'
import { useStorage } from '@vueuse/core' import { useStorage } from '@vueuse/core'
...@@ -31,16 +31,16 @@ watchEffect(() => { ...@@ -31,16 +31,16 @@ watchEffect(() => {
}) })
// 当前章 // 当前章
const chapter = $computed<CourseSectionType>(() => { const chapter = $computed(() => {
return props.chapterList.find(item => item.id === chapterId) return props.chapterList.find(item => item.id === chapterId)
}) })
// 当前节 // 当前节
const section = $computed<CourseSectionType>(() => { const section = $computed(() => {
return chapter?.sections.find(item => item.id === sectionId) return chapter?.sections.find(item => item.id === sectionId)
}) })
// 当前节 // 当前节
const resource = $computed<CourseResourceType>(() => { const resource = $computed(() => {
return section?.resources.find(item => item.resource_id === resourceId) return section?.resources.find(item => item.resource_id === resourceId)
}) })
// 资源视频列表 // 资源视频列表
......
<!-- 课程考核 --> <!-- 课程考核 -->
<script setup lang="ts"> <script setup lang="ts">
import type { CourseAssessChapterType } from '../types'
import { getChapterVideoTreeList } from '../api' import { getChapterVideoTreeList } from '../api'
const { query } = useRoute() const { query } = useRoute()
const courseId = $ref(query.course_id as string) const courseId = $ref(query.course_id as string)
const semesterId = $ref(query.semester_id as string) const semesterId = $ref(query.semester_id as string)
let list = $ref([])
let list = $ref<CourseAssessChapterType[]>([])
// 树列表 // 树列表
const treeList = $computed(() => { const treeList = $computed<CourseAssessChapterType[]>(() => {
return makeTree(list) return makeTree(list)
}) })
// 扁平列表
const flatList = $computed(() => {
return tree2List(list)
})
// 累计学习时长 // 累计学习时长
const studyTime = $computed(() => { const studyTime = $computed<number>(() => {
return list.reduce((total, item) => { return list.reduce((total, item) => {
return total + item.watch_video_length || 0 return total + item.watch_video_length || 0
}, 0) }, 0)
}) })
// 视频统计 // 视频统计
const videoStatistics = $computed(() => { const videoStatistics = $computed(() => {
return flatList.reduce( const results = { length: 0, completedLength: 0 }
(results, item) => { list.forEach(chapter => {
if (item.depth === 3) { chapter.sections.forEach(section => {
section.resources.forEach(resource => {
results.length++ results.length++
if (item.is_finished) { resource.is_finished && results.completedLength++
results.completedLength++ })
} })
} })
return results return results
},
{ length: 0, completedLength: 0 }
)
}) })
// 完成率 // 完成率
const completedPercent = $computed(() => { const completedPercent = $computed(() => {
const percent = (videoStatistics.completedLength / videoStatistics.length) * 100 const percent = (videoStatistics.completedLength / videoStatistics.length) * 100
return parseFloat(percent.toFixed(2)) || 0 return parseFloat(percent.toFixed(2)) || 0
}) })
// 树结构转换为列表 // 将列表转换为树结构
function tree2List(tree, parent) { function makeTree(list: any, parent?: any) {
return tree.reduce(function (acc, item) { return list.map((item: any) => {
if (item.depth === 2) { if (item.depth === 2 && parent) {
item.chapter_id = parent.id item.chapter_id = parent.id
} }
if (item.depth === 3) { if (item.depth === 3 && parent) {
item.chapter_id = parent.chapter_id item.chapter_id = parent.chapter_id
item.section_id = parent.id item.section_id = parent.id
} }
acc.push(item) if (item.sections || item.resources) item.children = makeTree(item.sections || item.resources, item)
if (item.sections) acc = acc.concat(tree2List(item.sections, item))
if (item.resources) acc = acc.concat(tree2List(item.resources, item))
return acc
}, [])
}
// 将列表转换为树结构
function makeTree(list: any) {
return list.map(item => {
if (item.sections || item.resources) item.children = makeTree(item.sections || item.resources)
return item return item
}) })
} }
......
...@@ -4,12 +4,20 @@ interface Props { ...@@ -4,12 +4,20 @@ interface Props {
data: PaperType data: PaperType
} }
defineProps<Props>() defineProps<Props>()
const { query } = useRoute()
const courseId = $ref(query.course_id as string)
const semesterId = $ref(query.semester_id as string)
</script> </script>
<template> <template>
<div class="course-exam-item" :key="data.id"> <div class="course-exam-item" :key="data.id">
<p> <p>
{{ data.paper_title }} <router-link
:to="`/course/exam?course_id=${courseId}&semester_id=${semesterId}&paper_id=${data.id}&type=1`"
target="_blank"
>{{ data.paper_title }}</router-link
>
</p> </p>
</div> </div>
</template> </template>
......
...@@ -13,6 +13,13 @@ export interface CourseListItemType extends CourseType { ...@@ -13,6 +13,13 @@ export interface CourseListItemType extends CourseType {
watch_video_length: number watch_video_length: number
} }
export interface CourseAssessChapterType extends CourseChapterType {
schedule: number
video_count: number
video_length: number
watch_video_length: number
}
// 课程章类型 // 课程章类型
export interface CourseChapterType { export interface CourseChapterType {
id: string id: string
...@@ -20,6 +27,7 @@ export interface CourseChapterType { ...@@ -20,6 +27,7 @@ export interface CourseChapterType {
resource_id: string resource_id: string
resource_type: number resource_type: number
sections: CourseSectionType[] sections: CourseSectionType[]
depth: number
} }
// 课程节类型 // 课程节类型
...@@ -29,6 +37,7 @@ export interface CourseSectionType { ...@@ -29,6 +37,7 @@ export interface CourseSectionType {
resource_id: string resource_id: string
resource_type: number resource_type: number
resources: CourseResourceType[] resources: CourseResourceType[]
depth: number
} }
// 课程资源类型 // 课程资源类型
...@@ -39,6 +48,8 @@ export interface CourseResourceType { ...@@ -39,6 +48,8 @@ export interface CourseResourceType {
resource_type: number resource_type: number
collection_count: number collection_count: number
info: ResourceType info: ResourceType
depth: number
is_finished?: number
} }
export interface VideoRecordType { export interface VideoRecordType {
......
<script setup lang="ts"> <script setup lang="ts">
const CoursePlayerVideo = defineAsyncComponent(() => import('../components/CoursePlayerVideo.vue')) import type { PaperQuestionType } from '@/types'
const CoursePlayerChapter = defineAsyncComponent(() => import('../components/CoursePlayerChapter.vue')) import CourseExamCard from '../components/CourseExamCard.vue'
import { getPaper } from '../api'
const route = useRoute()
const courseId = $ref(route.query.course_id as string)
const semesterId = $ref(route.query.semester_id as string)
const paperId = $ref(route.query.paper_id as string)
const type = $ref(parseInt(route.query.type as string))
const detail = reactive({ status: 1 })
const questionList = ref<PaperQuestionType[]>([])
provide('questionList', questionList)
// 获取试卷详情
function fetchInfo() {
getPaper({ course_id: courseId, semester_id: semesterId, paper_id: paperId, type }).then(res => {
Object.assign(detail, res.data)
questionList.value = res.data.question
})
}
onMounted(() => {
fetchInfo()
})
</script> </script>
<template> <template>
<div class="course-player"> <CourseExamCard :status="detail.status" @update="fetchInfo"></CourseExamCard>
<div class="course-player-main">
<CoursePlayerVideo />
<el-tabs>
<el-tab-pane label="课件"> </el-tab-pane>
<el-tab-pane label="教案" lazy> </el-tab-pane>
<el-tab-pane label="作业" lazy> </el-tab-pane>
<el-tab-pane label="资料" lazy> </el-tab-pane>
<el-tab-pane label="考试/测验" lazy></el-tab-pane>
<el-tab-pane label="直播" lazy></el-tab-pane>
</el-tabs>
</div>
<div class="course-player-aside">
<CoursePlayerChapter />
</div>
</div>
</template> </template>
<style lang="scss" scoped>
.course-player {
display: flex;
}
.course-player-main {
flex: 1;
}
</style>
...@@ -71,6 +71,12 @@ function canDownload(type: number) { ...@@ -71,6 +71,12 @@ function canDownload(type: number) {
function downloadFile(data: CollectionType) { function downloadFile(data: CollectionType) {
data.info.url && saveAs(data.info.url, data.info.name) data.info.url && saveAs(data.info.url, data.info.name)
} }
function targetUrl(item: CollectionType) {
if (item.type === 5) {
return `/course/exam?course_id=${item.course_id}&semester_id=${item.semester_id}&paper_id=${item.info.id}&type=2`
}
return `/course/player?course_id=${item.course_id}&chapter_id=${item.chapter_id}&section_id=${item.section_id}&resource_id=${item.source_id}&semester_id=${item.semester_id}`
}
</script> </script>
<template> <template>
...@@ -84,10 +90,7 @@ function downloadFile(data: CollectionType) { ...@@ -84,10 +90,7 @@ function downloadFile(data: CollectionType) {
<ul v-if="list.length"> <ul v-if="list.length">
<li class="collection-item" v-for="item in list" :key="item.id"> <li class="collection-item" v-for="item in list" :key="item.id">
<p> <p>
<router-link <router-link :to="targetUrl(item)" target="_blank">
:to="`/course/player?course_id=${item.course_id}&chapter_id=${item.chapter_id}&section_id=${item.section_id}&resource_id=${item.source_id}&semester_id=${item.semester_id}`"
target="_blank"
>
<ResourceIcon :resourceType="item.type" /> <ResourceIcon :resourceType="item.type" />
{{ item.info.name || item.info.paper_title }} {{ item.info.name || item.info.paper_title }}
</router-link> </router-link>
......
import httpRequest from '@/utils/axios' import httpRequest from '@/utils/axios'
// 修改用户资料 // 修改用户资料
export function updateUser(data: { name: string; avatar: string; sex: string }) { export function updateUser(data: { real_name: string; avatar: string; gender: number }) {
return httpRequest.post('/api/usercenter/v2/frontend/user/update-user', data) return httpRequest.post('/api/usercenter/v2/frontend/user/update-user', data)
} }
// 通过cookie修改密码 // 通过cookie修改密码
......
...@@ -3,18 +3,24 @@ import { ElMessage } from 'element-plus' ...@@ -3,18 +3,24 @@ import { ElMessage } from 'element-plus'
import AppUpload from '@/components/base/AppUpload.vue' import AppUpload from '@/components/base/AppUpload.vue'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { updateUser } from '../api' import { updateUser } from '../api'
import { useUserStore } from '@/stores/user'
import { pick } from 'lodash-es'
const userStore = useUserStore()
const userInfo = userStore.user
const formRef = $ref<FormInstance>() const formRef = $ref<FormInstance>()
const form = reactive({ const form = reactive({
name: '', real_name: '',
avatar: '', avatar: '',
sex: '' gender: 1
}) })
const rules = ref<FormRules>({ const rules = ref<FormRules>({
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }], name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
mobile: [{ required: true, message: '请输入手机号', trigger: 'blur' }] mobile: [{ required: true, message: '请输入手机号', trigger: 'blur' }]
}) })
onMounted(() => {
Object.assign(form, pick(userInfo, ['real_name', 'avatar', 'gender']))
})
// 提交 // 提交
function handleSubmit() { function handleSubmit() {
formRef?.validate().then(update) formRef?.validate().then(update)
...@@ -37,16 +43,16 @@ const update = () => { ...@@ -37,16 +43,16 @@ const update = () => {
label-position="left" label-position="left"
style="width: 360px" style="width: 360px"
> >
<el-form-item label="昵称" prop="name"> <el-form-item label="昵称" prop="real_name">
<el-input v-model="form.name" /> <el-input v-model="form.real_name" />
</el-form-item> </el-form-item>
<el-form-item label="头像" prop="avatar"> <el-form-item label="头像" prop="avatar">
<AppUpload v-model="form.avatar"></AppUpload> <AppUpload v-model="form.avatar"></AppUpload>
</el-form-item> </el-form-item>
<el-form-item label="性别" prop="sex"> <el-form-item label="性别" prop="gender">
<el-radio-group v-model="form.sex" class="ml-4"> <el-radio-group v-model="form.gender" class="ml-4">
<el-radio label="1" size="large"></el-radio> <el-radio :label="1" size="large"></el-radio>
<el-radio label="2" size="large"></el-radio> <el-radio :label="2" size="large"></el-radio>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
......
...@@ -15,9 +15,11 @@ router.beforeEach(async (to, from, next) => { ...@@ -15,9 +15,11 @@ router.beforeEach(async (to, from, next) => {
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
user.isLogin ? next() : next('/401') if (!user.isLogin) {
location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(location.href)}`
return return
} }
}
next() next()
}) })
......
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { getUser, logout } from '@/api/base' import { getUser, logout } from '@/api/base'
import type { UserType, ProjectType, OrganizationType, RoleType, PermissionType } from '@/types' import type { UserType } from '@/types'
interface State { interface State {
user: UserType | null user: UserType | null
project: ProjectType | null
organization: OrganizationType | null
roles: RoleType[]
permissions: PermissionType[]
} }
export const useUserStore = defineStore({ export const useUserStore = defineStore({
id: 'user', id: 'user',
state: (): State => ({ state: (): State => ({
user: null, user: null
organization: null,
project: null,
roles: [],
permissions: []
}), }),
getters: { getters: {
isLogin: state => !!state.user isLogin: state => !!state.user,
name: state => state.user?.realname || state.user?.nickname || state.user?.username
}, },
actions: { actions: {
async getUser() { async getUser() {
const res = await getUser() const res = await getUser()
const { info } = res.data this.user = res.data
const { organization, project, roles, permissions } = res.data.permissions
this.user = info
this.organization = organization
this.project = project
this.roles = roles
this.permissions = permissions
}, },
async logout() { async logout() {
await logout() await logout()
......
import type { Component } from 'vue' import type { Component } from 'vue'
import type { questionType } from '@/utils/dictionary'
export interface IMenuItem { export interface IMenuItem {
tag?: string tag?: string
...@@ -15,43 +16,10 @@ export interface UserType { ...@@ -15,43 +16,10 @@ export interface UserType {
id: string id: string
mobile: string mobile: string
name: string name: string
realname: string
nickname: string
username: string username: string
} gender: number
// 项目类型
export interface ProjectType {
id: string
name: string
tab: string
}
// 机构类型
export interface OrganizationType {
contact_information: string
contact_name: string
id: string
is_valid: 1 | 2
name: string
validity_date: string
}
// 角色类型
export interface RoleType {
desc: string
id: string
name: string
}
// 权限类型
export interface PermissionType {
desc: string
effect_uris: string
id: string
name: string
parent_id: string
system_tag: number
tag: string
type: number
} }
// 课程类型 // 课程类型
...@@ -74,6 +42,9 @@ export interface CourseType { ...@@ -74,6 +42,9 @@ export interface CourseType {
// 章节类型 // 章节类型
export interface ChapterType { export interface ChapterType {
chapter_id?: string
section_id?: string
resource_id?: string
id: string id: string
name: string name: string
schedule: string schedule: string
...@@ -109,6 +80,7 @@ export interface ResourceType { ...@@ -109,6 +80,7 @@ export interface ResourceType {
size: number size: number
source_id: string source_id: string
paper_title?: string paper_title?: string
paper_type?: number
} }
// 直播类型 // 直播类型
...@@ -151,3 +123,33 @@ export interface PaperCategoryType { ...@@ -151,3 +123,33 @@ export interface PaperCategoryType {
category_name: string category_name: string
name: string name: string
} }
export type QuestionType = keyof typeof questionType
export interface PaperQuestionType {
id: string
project_prefix: string
permission: number
question_type: QuestionType
question_title: string
question_content: string
common_content: string
question_options: PaperQuestionOptionType[]
question_analysis: string
question_difficulty: number
status: number
group_id: string
question_order: number
question_tag: string
is_parent: number
child_question_type: number
score: number
children?: PaperQuestionType[]
user_answer: string
}
export interface PaperQuestionOptionType {
checked_option: string
option: string
id: string
user_checked: boolean
}
...@@ -52,6 +52,11 @@ httpRequest.interceptors.response.use( ...@@ -52,6 +52,11 @@ httpRequest.interceptors.response.use(
location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(location.href)}` location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(location.href)}`
return Promise.reject(data) return Promise.reject(data)
} }
if ([4008, 4011, 4012, 4013].includes(data.code)) {
// 未授权
router.push('/401')
return Promise.reject(data)
}
if (Object.hasOwn(data, 'code') && data.code !== 0) { if (Object.hasOwn(data, 'code') && data.code !== 0) {
ElMessage.error(data.message || data.msg) ElMessage.error(data.message || data.msg)
return Promise.reject(data) return Promise.reject(data)
......
// json to array // json to array
export const json2Array = function (data, isValueToNumber = true) { export const json2Array = function (data: any, isValueToNumber = true) {
return Object.keys(data).map(value => ({ label: data[value], value: isValueToNumber ? parseInt(value) : value })) return Object.keys(data).map(value => ({ label: data[value], value: isValueToNumber ? parseInt(value) : value }))
} }
......
import { useUserStore } from '@/stores/user'
import type { DirectiveBinding } from 'vue'
// 判断是否有权限
export function checkPermission(value: string | string[]): boolean {
const userStore = useUserStore()
const permissions = userStore.permissions
if (Array.isArray(value)) {
return permissions.some(item => value.includes(item.tag))
} else {
return !!permissions.find(item => item.tag === value)
}
}
// 权限指令
export function permissionDirective(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding
if (!value) return
if (!checkPermission(value)) {
el.parentNode && el.parentNode.removeChild(el)
}
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论