提交 502787b9 authored 作者: lihuihui's avatar lihuihui

feat: 页面修改开发

上级
VITE_BASE_URL=https://learn-api.ezijing.com
VITE_LOGIN_URL=https://login.ezijing.com/xlearn/login/index
VITE_X_TRAINING_URL=https://x-training.ezijing.com
VITE_BASE_URL=https://learn-api.ezijing.com
VITE_LOGIN_URL=https://login.ezijing.com/xlearn/login/index
VITE_X_TRAINING_URL=https://x-training.ezijing.com
VITE_BASE_URL=https://learn-api2.ezijing.com
VITE_LOGIN_URL=https://login2.ezijing.com/xlearn/login/index
VITE_X_TRAINING_URL=https://x-training2.ezijing.com
module.exports = {
env: {
node: true
},
extends: ['plugin:vue/essential', 'standard'],
rules: {
'vue/comment-directive': 'off',
'space-before-function-paren': 'off'
}
}
node_modules
.DS_Store
dist
dist-ssr
*.local
const fs = require('fs')
const path = require('path')
const chalk = require('chalk')
const OSS = require('ali-oss')
const log = console.log
const client = new OSS({
region: 'oss-cn-beijing',
accessKeyId: 'LTAIOTuuLTaWoGJj',
accessKeySecret: 'dE5tTGm2lh35eItct2krW2DeH2lf2I',
bucket: 'webapp-pub'
})
async function uploadTarget(src, dist) {
try {
const result = await client.put(dist, path.join(__dirname, src))
log(chalk.green('上传成功', result.url))
} catch (e) {
log(chalk.red('上传失败', src))
log(e)
}
}
function generateUploadTarget(src, dist) {
fs.readdir(path.join(__dirname, src), function (err, files) {
if (err) {
log(err)
return
}
files.forEach(function (file) {
const _src = src + '/' + file
const _dist = dist + '/' + file
const stats = fs.statSync(path.join(__dirname, _src))
// 判断是否为文件
stats.isFile() && uploadTarget(_src, _dist)
// 判断是否为文件夹
stats.isDirectory() && generateUploadTarget(_src, _dist)
})
})
}
generateUploadTarget('./dist', '/website/prod/x-learn')
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAn0EINdIXTDCzmR7J5FOjOV+PbXt7GNO6fanoCGe2O0CPRlNf
2Ea/wv6SlRtJPd0ohmnKqZdUbBpAsiV4ggOdOqeEB6utVYQWY/zhXRKYeRjN/iDu
WCRY5S+eRVkSzVOJP9DlBn6dnHSsWj55h1PrkIac8B862F/cVno/Wk5dqU55ZUoN
wHGw5Goz3R37w+Q0C9HRS5mrmPqI+Ogy8TJrIRxw9YAj5OlvuqBAeYAW1sNdEfsi
mMB0H2fbbXqEL4AsipE5ppP7Ij3vxVpxvmnl/SO7N6+Fit6r25VeFSvplK+PIV3c
UsK3PCKV2sOo0BDWtWFQh5hW3fK5RYjLpNDHCwIDAQABAoIBAEkiBDMzF5/VfaSD
jxNblUlzqNoOKqlsEehDblrtxbHQI/uXrhwT4VwarBXtQeU2+rU/P+JBrHM4Wx10
N7L9FecppmgfXqo2zlF8f8HOGFcEHRTm6o1vo6McCwKttQS1qAG2XHZvDtIagkuv
BQAwea0VJFzg+pUC8JyF5zIBauGkfk8eHTLFVuIEJoSJbPWBYzp7Vf1SCjXqs3YY
aZ5QkOqY7S81D2EULFAWiMIMdY/PVT5DSXxsjaJFkvxjDedA4jNCplyODBKdpnBb
kfoJTJ7qsSnqgJ2y2xRdRlvZalE49lr2MkW254s5GH35+hMYam0bffgLXdPz6RIs
7X0atYECgYEA1A9G+0+uYlyxddyR54QlWGK7L3wP+REMXultudT9rq4S6qkHoOgP
rhi2kvZOqA0sMR7XMVz5nw0ouUMUVfW0YzudgAK99tdIuk6dP6VqVo9T4kqa0rXi
3ZKD51qGXbF22SndEWV68QEPzMCbf0E+kXl5MGGNnFtjZ5nxTGS+uH8CgYEAwECs
0T36EnLOCXZoi3rTeHr2pSO20VuFSgljnHA6Ups9Chu6h/iZ8t0XVNb8J14q7lFi
NY6b4D3FR/vwO3nFt7dvFYNFaFGuFrkAaH002p8EYWSckhlGcucBuKivBVUbhXuM
HMGmqGhAnnGCvCj/v4n5/wv3wtFYfzYWnYPHC3UCgYBZgbFGNhW28sT8qIL1I3PX
4KR9oHHlgOqlzQVBYMNKzbKyVXIg2pJzu36kfU4p5JV4jjnqXgIGvjkoKUYWGkVv
dSQ/eejQnYHXEYOR77H4ozqW00KSGa+OMl92cWExfsxZUTA8PYcs3nPayplXlyRf
ptQeNa7eBjzo57NPuV4+5QKBgQCrJihzUlBYshmYNPBXE25FOHpwgz3SXT5orbke
4I4bUhXh9NN3DqrGmWqW3Zi2108ywALFGQLNe1AwiCnSWNLafZOHvEhC2Uw48FNb
sfMmmR/GMFJugc/EpMBUit7cyWppx5XxV7gs/jpgkz7GkV00P/ntwtK7fbDh9t3l
NhYxrQKBgDVE4HSDqOvZOaXGRoM0pJ3uYRTTSIDGVNMZ9t2C/t3uwoyFBe+Om2t+
G6w2Gr+Dck1v+zizU3khbAHvE67rYoUtrDvae41bmLuVcnYh4UsXfhB6BWOSaQ+l
l8aQwTfmV74szsEDcFkg038zQ6Q4c8iiurYp29nwEM7/mayBGOcv
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIHEDCCBfigAwIBAgIQC53CSHjB5MGsHDzx/2AxzjANBgkqhkiG9w0BAQsFADBb
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMRowGAYDVQQDExFTZWN1cmUgU2l0ZSBDQSBHMjAeFw0y
MDA2MTAwMDAwMDBaFw0yMjA5MTIxMjAwMDBaMFsxCzAJBgNVBAYTAkNOMRAwDgYD
VQQIEwdCZWlqaW5nMSIwIAYDVQQKExlUSEggWmlqaW5nIChCZWlqaW5nKSBJbmMu
MRYwFAYDVQQDDA0qLmV6aWppbmcuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEAn0EINdIXTDCzmR7J5FOjOV+PbXt7GNO6fanoCGe2O0CPRlNf2Ea/
wv6SlRtJPd0ohmnKqZdUbBpAsiV4ggOdOqeEB6utVYQWY/zhXRKYeRjN/iDuWCRY
5S+eRVkSzVOJP9DlBn6dnHSsWj55h1PrkIac8B862F/cVno/Wk5dqU55ZUoNwHGw
5Goz3R37w+Q0C9HRS5mrmPqI+Ogy8TJrIRxw9YAj5OlvuqBAeYAW1sNdEfsimMB0
H2fbbXqEL4AsipE5ppP7Ij3vxVpxvmnl/SO7N6+Fit6r25VeFSvplK+PIV3cUsK3
PCKV2sOo0BDWtWFQh5hW3fK5RYjLpNDHCwIDAQABo4IDzjCCA8owHwYDVR0jBBgw
FoAUxBF+iECGwkG/ZfMa4bRTQKOr7H0wHQYDVR0OBBYEFHxjLRRYXe2jIjYECuN8
r3EnjOTFMCUGA1UdEQQeMByCDSouZXppamluZy5jb22CC2V6aWppbmcuY29tMA4G
A1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwbwYD
VR0fBGgwZjAxoC+gLYYraHR0cDovL2NybDMuZGlnaWNlcnQuY29tL1NlY3VyZVNp
dGVDQUcyLmNybDAxoC+gLYYraHR0cDovL2NybDQuZGlnaWNlcnQuY29tL1NlY3Vy
ZVNpdGVDQUcyLmNybDBMBgNVHSAERTBDMDcGCWCGSAGG/WwBATAqMCgGCCsGAQUF
BwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMAgGBmeBDAECAjBsBggr
BgEFBQcBAQRgMF4wIQYIKwYBBQUHMAGGFWh0dHA6Ly9vY3NwLmRjb2NzcC5jbjA5
BggrBgEFBQcwAoYtaHR0cDovL2NybC5kaWdpY2VydC1jbi5jb20vU2VjdXJlU2l0
ZUNBRzIuY3J0MAwGA1UdEwEB/wQCMAAwggH1BgorBgEEAdZ5AgQCBIIB5QSCAeEB
3wB2AEalVet1+pEgMLWiiWn0830RLEF0vv1JuIWr8vxw/m1HAAABcpwT21oAAAQD
AEcwRQIgWTyqiBOL3dFTJBE2Q6cgSBzk9W5iTaC2B8T1f8gFCP0CIQDhngm9WJbO
J7v14h6w+B2Li7WEAkWLSLiTKzh7na2SuQB1ACJFRQdZVSRWlj+hL/H3bYbgIyZj
rcBLf13Gg1xu4g8CAAABcpwT2zEAAAQDAEYwRAIgckmPL6WJx9Jke4AfVLmy//ye
tsmT5si8FO8p9Fd52VECICPqDvdjlN2DtfQznTGTxaL0PQ5N8eNiX3fJn6sRCfcU
AHYAUaOw9f0BeZxWbbg3eI8MpHrMGyfL956IQpoN/tSLBeUAAAFynBPbfQAABAMA
RzBFAiEAwYooscdEijXGnRdJYnz0ClmvWcxtJ169Bq+sywhPReACIDjvE5a5d7mb
n3YTgfLOtbnuDpkDRjUfdY7cs6UfderhAHYAQcjKsd8iRkoQxqE6CUKHXk4xixsD
6+tLx2jwkGKWBvYAAAFynBPa0wAABAMARzBFAiAmJVwNfWFMKrqWTvEfHk9O/5/r
Crj/W3BqjV6p0D09hgIhAIKb4drMok8s1X0Evh4Nbzd3Nv9PuwITdICztemCrk4e
MA0GCSqGSIb3DQEBCwUAA4IBAQBWSrE/pt//MKeGpf6vMISGD0LZArebPFQ7wlgv
Y13HpCY5lqwrZItsuXWS5IYMv8ueYarCm081OJOBvSUKHOtYSe6wdFqsXehokUiy
7oVNief7Li5RvLcf6z5fyjB+i017dds73Dt94mE1imV1DR1WErp1U6QCMEh+TKFa
PL52V9X5VWiYdImzdm8AWOlNBrgicmVzEEQuglejF5uaALf9iiyAjP36apqXv77T
UtxKgjONB1tnRw4XRqzwrEK+QjeOhziKCn1v2ppFX/Z11YYA7ajICVrG6wGJ+ENc
ukf5+v8r+TU7PqxQmb62zocX22jhe8HM644UJ4FWCiBh4Lb1
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFFjCCA/6gAwIBAgIQCH4Y+4+qkn7odgoNiYL1EjANBgkqhkiG9w0BAQsFADBh
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
QTAeFw0xOTA2MjAxMjIxMzVaFw0yOTA2MjAxMjIxMzVaMFsxCzAJBgNVBAYTAlVT
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
b20xGjAYBgNVBAMTEVNlY3VyZSBTaXRlIENBIEcyMIIBIjANBgkqhkiG9w0BAQEF
AAOCAQ8AMIIBCgKCAQEAx7s903fR6SgpA08UdhKEUIZHa2Ig7KPNkTtwMS1+08YS
5QSEDM4DQxy48jP8dZkyyU9J/0WCm8Nlv5ga7HOAxhdJcv+CPP4oadx8EbdrmjAH
rGOv64oHvt7Ina7uzLd3krqxd0doeuxRpTHvFAyjaUhxjSfZx0wh1f6W7prPm7V5
0VcTudj4rI+xtHXUcFAuFz4bcapTcru5aaZ1v6F2usMCMVM+xJxEZcsUM4uTxdIf
W5FUTI0dbP8NyZkr/WVzL59aGwBE4ZU0JKBlgEmtkFpLPR7JCzYunafu7nMk5YY2
6WDOmezpWDjzDxJ8xakizykWYT5gdJYE3ULlUe31WQIDAQABo4IBzjCCAcowHQYD
VR0OBBYEFMQRfohAhsJBv2XzGuG0U0Cjq+x9MB8GA1UdIwQYMBaAFAPeUDVW0Uy7
ZvCj4hsbw5eyPdFVMA4GA1UdDwEB/wQEAwIBhjAdBgNVHSUEFjAUBggrBgEFBQcD
AQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zAxBggrBgEFBQcBAQQlMCMwIQYI
KwYBBQUHMAGGFWh0dHA6Ly9vY3NwLmRjb2NzcC5jbjBEBgNVHR8EPTA7MDmgN6A1
hjNodHRwOi8vY3JsLmRpZ2ljZXJ0LWNuLmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RD
QS5jcmwwgc4GA1UdIASBxjCBwzCBwAYEVR0gADCBtzAoBggrBgEFBQcCARYcaHR0
cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzCBigYIKwYBBQUHAgIwfgx8QW55IHVz
ZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2Yg
dGhlIFJlbHlpbmcgUGFydHkgQWdyZWVtZW50IGxvY2F0ZWQgYXQgaHR0cHM6Ly93
d3cuZGlnaWNlcnQuY29tL3JwYS11YTANBgkqhkiG9w0BAQsFAAOCAQEAE+8lW5Yw
IuiRsHn4gYRRVbLmIypWwYH74lIXnQiALeUsUkWfW7KA0ARF1el3YaTAg8/r6zyX
eZTdlhndxKOKvO5N+rnHWJB6a3fJURn6e0I+rDzKV1Zacv2Vx/ZHLZmza/bp4Azi
BrDOiPlW/Ktj6ALQzAgq70Oytk9htLupBWPuplJDdyhGqb9RfQvWc1Fa1HwXdBQi
oJPibfMaYkHMY3pTbOv2rzMKEoZwHDHqyC73RI9JgqqiXHw0rIL8A1uL3IrymXEr
mycTqbSozQwiiEfb+cxzY82YaNzaLpJyIst0T2QmdDDngmyd2LEmm4NKeXRrcFRh
XDDFfpIn93B7JA==
-----END CERTIFICATE-----
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="https://zws-imgs-pub.ezijing.com/pc/base/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>金融产品数字化营销职业技能等级证书学习平台</title>
<link rel="stylesheet" href="https://g.alicdn.com/de/prismplayer/2.9.3/skins/default/aliplayer-min.css" />
<link rel="stylesheet" href="//at.alicdn.com/t/font_2173492_ctgt96uojqw.css" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script src="https://g.alicdn.com/de/prismplayer/2.9.3/aliplayer-min.js"></script>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"version": "0.0.0",
"scripts": {
"dev": "vite --mode dev",
"build": "cross-env BUILD_ENV=prod vite build && npm run deploy",
"build:pre": "vite build",
"build:test": "vite build --mode test",
"preview": "vite preview",
"deploy": "node ./deploy.js",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src"
},
"dependencies": {
"axios": "^0.24.0",
"blueimp-md5": "^2.19.0",
"echarts": "^5.2.2",
"element-ui": "^2.15.6",
"js-base64": "^3.7.2",
"lodash": "^4.17.21",
"query-string": "^7.0.1",
"vue": "^2.6.14",
"vue-html2pdf": "^1.8.0",
"vue-router": "^3.5.3",
"vuex": "^3.6.2"
},
"devDependencies": {
"@rollup/plugin-eslint": "^8.0.1",
"ali-oss": "^6.16.0",
"chalk": "^4.1.2",
"cross-env": "^7.0.3",
"eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-vue": "^7.20.0",
"sass": "1.43.5",
"vite": "^2.6.14",
"vite-plugin-vue2": "^1.9.0",
"vue-template-compiler": "^2.6.14"
}
}
<template>
<div id="app">
<router-view />
</div>
</template>
import httpRequest from '@/utils/axios'
// 登录
export function login(data) {
return httpRequest.post('/api/passport/rest/login', data)
}
// 绑定微信
export function bindWechat(data) {
return httpRequest.post('/api/passport/rest/wechat/bind-unionid', data)
}
// 修改密码
export function updatePassword(data) {
return httpRequest.post('/api/usercenter/user/change-pwd-by-cookie', data)
}
// 重置密码
export function resetPassword(data) {
return httpRequest.post('/api/usercenter/user/update-pwd', data)
}
// 发送验证码
export function sendCode(data) {
return httpRequest.post('/api/usercenter/user/send-code', data)
}
// 登出
export function logout() {
return httpRequest.get('/api/zy/user/logout')
}
// 获取用户信息
export function getUser() {
return httpRequest.get('/api/fd/user/get-student-info')
}
// 修改用户信息
export function updateUser(data) {
return httpRequest.post('/api/usercenter/user/update-user', data)
}
// 绑定游客
export function bindVisitor(data) {
return httpRequest.post('/api/zy/user/bind-account', data)
}
// 获取是否VIP
export function getIsVip() {
return httpRequest.get('/api/zy/user/is-vip')
}
// 创建游客用户
export function createGuestUser() {
return httpRequest.get('/api/zy/user/create-guest-user')
}
// 校验验证码
export function checkCode(params) {
return httpRequest.get('/api/usercenter/user/check-code', { params })
}
// 选择用户角色
export function chooseRole(data) {
return httpRequest.post('/api/zy/user/choose-role', data)
}
// 获取所有权限
export function getPermissions() {
return httpRequest.get('/api/zy/user/get-permissions')
}
import httpRequest from '@/utils/axios'
/**
* 获取课程列表
*/
export function getCourseModule() {
return httpRequest.get('/api/zy/v2/education/mokuai')
}
/**
* 获取课程列表
*/
export function getCourseList() {
return httpRequest.get('/api/zy/v2/education/courses/list')
}
/**
* 获取课程详情
* @param {string} courseId 课程ID
*/
export function getCourse(courseId) {
return httpRequest.get(`/api/zy/v2/education/courses/${courseId}`).then(response => {
// response.chapters = response.chapters.filter(item => {
// item.children = item.children.filter(child => child.type === 2)
// return item.children.length
// })
return response
})
}
/**
* 获取课程知识点
* @param {string} courseId 课程ID
*/
export function getCourseTagList(courseId) {
return httpRequest.get(`/api/zy/v2/education/tag/tree/${courseId}`)
}
/**
* 知识点详情
* @param {string} tagId 知识点ID
*/
export function getCourseTag(tagId) {
return httpRequest.get(`/api/zy/v2/education/tag/${tagId}`)
}
import httpRequest from '@/utils/axios'
/**
* 获取模拟考试试题
*/
export function getExamList(params) {
return httpRequest.get('/api/zy/v2/examination/examination-papers-list', { params })
}
/**
* 获取模拟考试试题
*/
export function getExamQuestion(params) {
return httpRequest.get(
'/api/zy/v2/examination/examination-papers',
{ params },
{ headers: { 'Content-Type': 'multipart/form-data' } }
)
}
/**
* 缓存模拟考试试题
*/
export function setCache(params) {
return httpRequest.post('/api/zy/v2/examination/examination-papers', params)
}
/**
* 获取考试的状态
*/
export function getExamStatus(params) {
return httpRequest.get('/api/zy/v2/examination/examination-papers-status', { params })
}
/**
* 模拟考试设置角色
*/
export function setRoles(params) {
return httpRequest.post('/api/zy/v2/examination/role', params)
}
/**
* 获取我的已做试题
*/
export function getMyQuestion(params) {
return httpRequest.get(
'/api/zy/v2/examination/my-question',
{ params },
{
headers: { 'Content-Type': 'multipart/form-data' }
}
)
}
/**
* 获取我的所有试题
*/
export function getAllQuestion(params) {
return httpRequest.get('/api/zy/v2/examination/my-question-all', { params })
}
/**
* 缓存错题集
*/
export function setMyCache(params) {
return httpRequest.post('/api/zy/v2/examination/cache-question', params)
}
/**
* 收藏试题
*/
export function addCollection(params) {
return httpRequest.post('/api/zy/v2/examination/add-collection', params)
}
/**
* 取消收藏试题
*/
export function deleteCollection(params) {
return httpRequest.post('/api/zy/v2/examination/delete-my-question', params)
}
/**
* 删除试题
*/
export function deleteQuestion(params) {
return httpRequest.post('/api/zy/v2/examination/delete-my-question', params)
}
/**
* 知识点题获取
*/
export function getCourseQuestion(params) {
return httpRequest.get('/api/zy/v2/examination/course-papers', { params })
}
/**
* 知识点题缓存
*/
export function setCourseCache(params) {
return httpRequest.post('/api/zy/v2/examination/course-papers', params)
}
/**
* 老师批阅
*/
export function getReviewDetails(params) {
return httpRequest.get('/api/zy/v3-teacher/examination/sheet-details', { params })
}
/**
* 提交批阅
*/
export function submitReviewDetails(params) {
return httpRequest.post('/api/zy/v3-teacher/examination/sheet-submit', params)
}
body,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
p,
blockquote,
dl,
dt,
dd,
ul,
ol,
li,
pre,
form,
fieldset,
legend,
button,
input,
textarea,
th,
td {
margin: 0;
padding: 0;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: 100%;
}
ul,
ol,
li {
list-style: none;
}
em,
i {
font-style: normal;
}
strong,
b {
font-weight: normal;
}
img {
border: none;
}
input,
img {
vertical-align: middle;
}
a {
color: inherit;
text-decoration: none;
}
input,
button,
select,
textarea {
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-webkit-appearance: none;
border: 0;
border-radius: 0;
font: inherit;
}
textarea:focus {
outline: 0;
}
body {
font-size: 14px;
line-height: 1.4;
color: #222;
background-color: #f9f9f9;
}
:root {
--main-color: #c01540;
}
.empty {
padding: 120px;
text-align: center;
font-size: 18px;
color: #999;
}
\ No newline at end of file
$--color-primary: #c01540;
$--color-info: #3c4043;
// border
$--border-radius-small: 8px !default;
// dialog
$--message-close-size: 20px !default;
/* 改变 icon 字体路径变量,必需 */
$--font-path: 'element-ui/lib/theme-chalk/fonts';
@import 'element-ui/packages/theme-chalk/src/index';
<template>
<el-button v-bind="$attrs" :disabled="currentDisabled" @click="start">{{ curretnValue }}</el-button>
</template>
<script>
export default {
name: 'Countdown',
props: {
step: { type: Number, default: 1000 },
disabled: { type: Boolean, default: false },
seconds: { type: Number, default: 60 },
defaultValue: { type: String, default: '获取验证码' }
},
data() {
return {
currentDisabled: false,
currentSeconds: 0,
timer: null
}
},
watch: {
disabled: {
immediate: true,
handler(value) {
this.currentDisabled = value
}
}
},
computed: {
curretnValue() {
const longTime = this.seconds - this.currentSeconds
return longTime < this.seconds ? `${longTime}S` : this.defaultValue
}
},
methods: {
genTimer() {
this.clearTimer()
this.timer = setInterval(() => {
this.currentSeconds++
if (this.currentSeconds === this.seconds) {
this.stop()
}
}, this.step)
},
clearTimer() {
this.timer && clearInterval(this.timer)
},
start() {
this.currentDisabled = true
this.genTimer()
this.$emit('start')
},
stop() {
this.clearTimer()
this.currentSeconds = 0
this.currentDisabled = false
this.$emit('stop')
}
}
}
</script>
<template>
<div class="course-list" element-loading-text="加载中..." v-loading="!loaded">
<template v-if="currentList.length">
<course-list-item v-for="item in currentList" :data="item" :key="item.id" v-bind="$attrs" v-on="$listeners" />
</template>
<template v-else>
<slot name="empty">
<div class="empty">暂无相关课程</div>
</slot>
</template>
</div>
</template>
<script>
import * as api from '@/api/course.js'
import CourseListItem from './CourseListItem.vue'
export default {
name: 'CourseList',
components: { CourseListItem },
props: {
requestCallback: Function,
searchValue: String
},
data() {
return {
loaded: false,
list: []
}
},
computed: {
currentList() {
if (!this.searchValue) {
return this.list
}
return this.list.filter(item => {
return item.course_name.includes(this.searchValue.trim())
})
}
},
methods: {
getCourseList() {
api
.getCourseList()
.then(response => {
this.list = this.requestCallback ? this.requestCallback(response) : response
this.$emit('request-success', response)
})
.finally(() => {
this.loaded = true
})
}
},
beforeMount() {
this.getCourseList()
}
}
</script>
<template>
<div class="course-item" @click="$emit('on-click', data)">
<img class="course-item-pic" :src="data.curriculum.curriculum_picture" />
<div class="course-item-content">
<div class="course-item__title">{{ data.curriculum.curriculum_name }}</div>
<div class="course-top__tips">第x次重修</div>
<div class="course-item__tools">
<div class="course-item__progress">
<span>视频观看进度</span>
<el-progress :percentage="data.video_progress"></el-progress>
</div>
<div class="course-item__buttons">
<el-button type="primary" size="small" round>查看课程</el-button>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'CourseListItem',
props: {
data: { type: Object, required: true },
showProgress: { type: Boolean, default: false }
},
data() {
return {}
},
filters: {
progressText(value) {
value = parseInt(value)
if (value === 0) {
return '未开始'
}
if (value === 100) {
return '已学完'
}
return `已学${value}%`
}
}
}
</script>
<style lang="scss" scoped>
.course-item {
display: flex;
padding: 14px 0;
cursor: pointer;
}
.course-item + .course-item {
border-top: 1px solid #ccc;
}
.course-item-pic {
width: 160px;
height: 90px;
margin-right: 20px;
border-radius: 2px;
overflow: hidden;
cursor: pointer;
}
.course-item-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
.course-item__title {
font-size: 18px;
font-weight: bold;
color: #222;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.course-item__tools {
display: flex;
align-items: center;
}
.course-item__progress {
display: flex;
flex: 1;
.el-progress {
margin: 0 10px;
width: 50%;
}
}
</style>
<template>
<div class="app-card">
<div class="app-card-hd">
<slot name="header">
<h2 class="app-card-hd__title" v-if="title">{{ title }}</h2>
<slot name="header-aside"></slot>
</slot>
</div>
<div class="app-card-bd">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'AppCard',
props: { title: String }
}
</script>
<style>
.app-card {
background: #ffffff;
box-shadow: 0 1px 6px 0 rgb(228 232 235 / 20%);
border-radius: 8px;
margin-bottom: 20px;
padding: 32px;
}
.app-card-hd {
display: flex;
}
.app-card-hd__title {
flex: 1;
font-size: 18px;
font-weight: 700;
margin-bottom: 16px;
}
</style>
<template>
<div class="app-container">
<div class="app-container-hd" v-if="title">
<div class="app-container-hd__title">{{ title }}</div>
<div class="app-container-hd__right">
<slot name="header-right"></slot>
</div>
</div>
<div class="app-container-bd">
<slot></slot>
</div>
<slot name="footer"></slot>
</div>
</template>
<script>
export default {
name: 'AppContainer',
props: { title: { type: String } }
}
</script>
<style lang="scss">
.app-container {
display: flex;
flex-direction: column;
height: 100%;
padding: 30px;
background-color: #fff;
border-radius: 8px;
box-sizing: border-box;
}
.app-container-hd {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 8px;
margin-bottom: 20px;
border-bottom: 1px solid #ccc;
}
.app-container-hd__title {
font-size: 18px;
line-height: 1;
}
.app-container-bd {
flex: 1;
}
.app-container-ft {
background-color: #fff;
position: sticky;
bottom: 0;
padding-top: 30px;
border-top: 1px solid #ccc;
}
</style>
<template>
<div class="table-list">
<div class="table-list-hd">
<!-- 筛选 -->
<div class="table-list-filter">
<el-form v-if="filters.length" :inline="true" :model="params" ref="filterForm">
<template v-for="item in filters">
<el-form-item :label="item.label" :prop="item.prop" :key="item.prop" class="filter-form-item">
<template v-if="item.slots">
<slot :name="item.slots" v-bind="{ params }"></slot>
</template>
<template v-else>
<!-- input -->
<el-input v-model="params[item.prop]" v-bind="item" clearable v-if="item.type === 'input'" />
<!-- select -->
<el-select
v-model="params[item.prop]"
clearable
v-bind="item"
v-if="item.type === 'select'"
@change="search"
>
<template v-for="(option, index) in item.options">
<el-option
:label="option[item.labelKey] || option.label"
:value="option[item.valueKey] || option.value"
:key="index"
></el-option>
</template>
</el-select>
</template>
</el-form-item>
</template>
<el-form-item class="filter-buttons" v-if="!searchResetSeparateLine">
<el-button type="primary" icon="el-icon-search" @click="search">搜索</el-button>
<el-button icon="el-icon-refresh-left" @click="reset">重置</el-button>
</el-form-item>
</el-form>
<div class="filter-bar">
<div class="filte-bar-left-btns">
<template v-if="searchResetSeparateLine">
<el-button type="primary" icon="el-icon-search" size="small" @click="search">搜索</el-button>
<el-button icon="el-icon-refresh-left" size="small" @click="reset">重置</el-button>
</template>
</div>
<div class="filter-bar-right">
<slot name="filter-bar-right" />
</div>
</div>
</div>
<div class="table-list-hd-aside"><slot name="header-aside" /></div>
</div>
<slot></slot>
<div class="table-list-bd">
<slot name="body" v-bind="{ data: dataList }">
<el-table :data="dataList" v-loading="loading" v-bind="$attrs" v-on="$listeners" ref="table">
<template v-for="item in columns">
<el-table-column v-bind="item" :key="item.prop" v-if="visible(item)" align="center">
<template v-slot:default="scope" v-if="item.slots || item.computed">
<slot :name="item.slots" v-bind="scope" v-if="item.slots"></slot>
<div v-html="item.computed(scope)" v-if="item.computed"></div>
</template>
</el-table-column>
</template>
</el-table>
</slot>
</div>
<div class="table-list-ft">
<div>
<slot name="footer"></slot>
</div>
<el-pagination
class="table-list-pagination"
:layout="pagationLayout"
:page-sizes="[10, 20, 30, 50, 100]"
:page-size="page.size"
:total="page.total"
:current-page.sync="page.currentPage"
@size-change="pageSizeChange"
@current-change="fetchList()"
v-if="hasPagination"
>
</el-pagination>
</div>
</div>
</template>
<script>
export default {
name: 'AppList',
props: {
// 接口请求
remote: { type: Object, default: () => ({}) },
// 筛选
filters: { type: Array, default: () => [] },
searchResetSeparateLine: { type: Boolean, default: false },
// 列表项
columns: { type: Array, default: () => [] },
// 列表数据
data: { type: Array, default: () => [] },
// 是否含有翻页
hasPagination: { type: Boolean, default: true },
// 每页多少条数据
limit: { type: Number, default: 20 },
pagationLayout: { type: String, default: 'total, prev, pager, next, sizes, jumper' }
},
data() {
return {
loading: false,
params: this.remote.params || {},
dataList: this.data,
page: { total: 0, size: this.limit, currentPage: 1 }
}
},
watch: {
'remote.params': {
immediate: true,
handler(data) {
this.params = data || {}
}
},
data: {
immediate: true,
handler(data) {
this.dataList = data
}
}
},
computed: {
table() {
return this.$refs.table
}
},
methods: {
fetchList(isReset) {
/**
* @param function httpRequest api接口
* @param function beforeRequest 接口请求之前
* @param function callback 接口请求成功回调
*/
const { httpRequest, beforeRequest, callback } = this.remote
if (!httpRequest) {
return
}
// 参数设置
let params = this.params
// 翻页参数设置
if (this.hasPagination) {
params.page = this.page.currentPage.toString()
params.page_size = this.page.size.toString()
}
// 接口请求之前
if (beforeRequest) {
params = beforeRequest(params, isReset)
}
for (const key in params) {
if (params[key] === '' || params[key] === undefined || params[key] === null) {
delete params[key]
}
}
this.loading = true
httpRequest(params)
.then(res => {
const { data = {} } = res || {}
this.page.total = parseInt(data.total || 0)
this.dataList = callback ? callback(data) : data.list
})
.catch(() => {
this.page.total = 0
this.dataList = []
})
.finally(() => {
this.loading = false
})
},
// 搜索
search() {
this.page.currentPage = 1
this.fetchList()
},
// 重置
reset() {
// 清空筛选条件
this.$refs.filterForm && this.$refs.filterForm.resetFields()
// 初始化页码
this.page.currentPage = 1
// 刷新列表
this.fetchList(true)
},
// 刷新
refetch(isForce) {
isForce ? this.reset() : this.fetchList()
},
// 页数改变
pageSizeChange(value) {
this.page.currentPage = 1
this.page.size = value
this.fetchList()
},
visible(item) {
return Object.prototype.hasOwnProperty.call(item, 'visible') ? item.visible : true
}
},
beforeMount() {
this.fetchList()
}
}
</script>
<style lang="scss">
.table-list {
height: 100%;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.table-list-hd {
display: flex;
}
.table-list-filter {
flex: 1;
}
.filter-bar {
display: flex;
margin-bottom: 15px;
}
.filte-bar-left-btns {
flex: 1;
}
.el-form--inline .el-form-item {
margin-right: 30px;
}
.table-list-bd {
flex: 1;
}
.table-list-ft {
display: flex;
align-items: center;
justify-content: space-between;
}
.table-list-pagination {
padding: 10px 0;
text-align: right;
}
.el-table-column--selection .cell {
padding: 0 14px !important;
}
</style>
<template>
<div class="search-bar">
<el-input
prefix-icon="el-icon-search"
size="medium"
clearable
v-model="currentValue"
@change="onChange"
@keyup.enter.native="onSearch"
ref="formInput"
></el-input>
<el-button type="text" size="medium" @click="onSearch">搜索</el-button>
</div>
</template>
<script>
export default {
props: {
value: String,
focus: { type: Boolean, default: false }
},
data() {
return {
currentValue: this.value,
debounced: null
}
},
watch: {
value(value) {
this.currentValue = value
}
},
computed: {
formInput() {
return this.$refs.formInput
}
},
methods: {
bindFocus() {
this.focus && this.formInput.focus()
},
onChange() {
this.$emit('change', this.currentValue)
},
onSearch() {
this.$emit('search', this.currentValue)
}
},
mounted() {
this.bindFocus()
}
}
</script>
<style lang="scss">
.search-bar {
display: flex;
align-items: center;
.el-input {
width: 240px;
}
.el-button {
margin-left: 10px;
font-size: 18px;
font-weight: 400;
color: #333;
}
}
</style>
<template>
<div>
<div class="answer-box">
<div class="head" id="head-h">
<el-button icon="el-icon-arrow-left" circle @click="$emit('back')"></el-button>
<div class="title">{{ title }}</div>
<div class="right">
<div class="count" v-if="hasCountdown">{{ countdownText }}</div>
</div>
</div>
<div class="exam-main">
<div class="left">
<question-list
:data="currentQuestionGroup"
:page="currentGroupPage"
:index="currentQuestionIndex"
:disabled="disabled"
:hasResult="hasResult"
@change="handleChange"
>
<template #index>{{ currentGroupPage }}/{{ currentGroupCount }}</template>
<template v-slot:default="data">
<slot name="question-item" v-bind="data"></slot>
</template>
</question-list>
</div>
<div class="right">
<question-numbers
:status="status"
:page="currentGroupPage"
:data="currentQuestionGroup"
:list="numberGroups.length ? numberGroups : questionGroups"
@page-change="handlePageChange"
>
<slot name="students" v-bind="{ data: currentQuestionGroup }"></slot>
</question-numbers>
</div>
</div>
<div class="foot" id="foot-h">
<div class="exam-btn">
<div class="confirm" @click="showResult" v-if="hasShowResultBtn">确认答案</div>
<div :class="prevClass" @click="prev">上一题</div>
<div :class="nextClass" @click="next">下一题</div>
</div>
<div class="rigth-btn">
<div class="sign" v-if="hasCollect" @click="toggleCollect">
<div :class="firstQuestion.is_collection ? 'icon active' : 'icon'"></div>
<div class="txt">{{ firstQuestion.is_collection ? '已收藏' : '收藏' }}</div>
</div>
<div class="sign2" v-if="hasMark" @click="toggleMark">
<div :class="firstQuestion.sign ? 'icon active' : 'icon'"></div>
<div class="txt">{{ firstQuestion.sign ? '已标记' : '标记' }}</div>
</div>
<div class="del-btn" v-if="hasDeleteBtn" @click="$emit('delete', currentQuestionGroup)">删除</div>
<div class="end-exam-btn">
<div class="btn" v-if="hasSubmitBtn && !disabled" @click="submit">{{ submitButtonText }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import * as api from '@/api/exam.js'
import questionList from '@/components/exam/questionList.vue'
import questionNumbers from '@/components/exam/questionNumbers.vue'
export default {
components: { questionList, questionNumbers },
props: {
status: { type: Number, default: 1 }, // 1:答题;2:查看;3:批阅
title: { type: String },
hasMark: { type: Boolean, default: true }, // 标记
hasCollect: { type: Boolean, default: true }, // 收藏
hasSubmitBtn: { type: Boolean, default: true }, // 提交按钮
hasDeleteBtn: { type: Boolean, default: false }, // 删除按钮
hasShowResultBtn: { type: Boolean, default: false }, // 显示答案按钮
hasCountdown: { type: Boolean, default: true }, // 计时
submitButtonText: { type: String, default: '交卷' }, // 提交按钮显示的文字
data: { type: Object, default: () => {} }, // 模拟考试返回的数据,内部组装
groups: { type: Array, default: () => [] }, // 收藏、错题组装好的数据
groupPage: { type: Number, default: 1 }, // 大题当前页
groupPageSize: { type: Number, default: 0 }, // 大题一页的数量
groupPageCount: { type: Number, default: 0 }, // 大题总页数
numberGroups: { type: Array, default: () => [] } // 答题卡
},
data() {
return {
disabled: false, // 是否禁用输入框
hasResult: false, // 是否显示解析
duration: 0, // 答题所用时间
countdownTimer: null, // 倒计时计时器
countdownText: '', // 倒计时显示时间
questionGroups: this.groups, // 所有试题分组,一组一页
currentGroupPage: this.groupPage, // 大题页码
currentGroupCount: this.groupPageCount, // 大题总页数
isCountDownEnd: false
}
},
computed: {
// 当前页面试题组
currentQuestionGroup() {
return this.questionGroups[this.currentPage] || {}
},
// 当前页面的试题列表
currentQuestionList() {
return this.currentQuestionGroup.question_list || []
},
// 当前页面第一个试题
firstQuestion() {
return this.currentQuestionList[0] || {}
},
// 当前题号
currentQuestionIndex() {
return this.questionGroups.reduce((result, item, index) => {
if (index <= this.currentPage) {
result += item.question_list.length
}
return result
}, 0)
},
// 当前页码
currentPage() {
return (this.currentGroupPage - 1) % (this.groupPageSize || this.questionGroups.length)
},
// 上一题按钮的class
prevClass() {
return { active: this.currentGroupPage !== 1 }
},
// 下一题按钮的class
nextClass() {
return { active: this.currentGroupPage < this.currentGroupCount }
}
},
watch: {
data: {
immediate: true,
handler(data) {
data && this.dataInit(data)
}
},
groups: {
handler(groups) {
this.questionGroups = groups
}
},
groupPage(value) {
this.currentGroupPage = value
},
groupPageCount(value) {
this.currentGroupCount = value
},
currentPage() {
if (this.hasShowResultBtn) {
this.hasResult = this.currentQuestionGroup.hasResult
}
}
},
beforeDestroy() {
clearInterval(this.countdownTimer) // 停止倒计时
},
methods: {
// 倒计时
countDown(time) {
let sec = parseInt(time)
clearInterval(this.countdownTimer)
this.countdownTimer = setInterval(() => {
sec--
if (sec === 0) {
clearInterval(this.countdownTimer)
this.isCountDownEnd = true
this.$alert('考试时间结束,自动提交试卷', '', {
confirmButtonText: '确定',
callback: action => {
this.submit()
}
}) // 是否显示解析
return false
}
this.countdownText = this.secondToDate(sec) // 倒计时显示时间
this.duration++
this.$emit('timeupdate', this.duration, this.questionGroups)
}, 1000)
},
secondToDate(result) {
const h = Math.floor(result / 3600) < 10 ? '0' + Math.floor(result / 3600) : Math.floor(result / 3600)
const m =
Math.floor((result / 60) % 60) < 10 ? '0' + Math.floor((result / 60) % 60) : Math.floor((result / 60) % 60)
const s = Math.floor(result % 60) < 10 ? '0' + Math.floor(result % 60) : Math.floor(result % 60)
if (h === 0) {
result = m + ':' + s
} else {
result = h + ':' + m + ':' + s
}
return result
},
// 显示答案
showResult() {
this.hasResult = true
this.questionGroups[this.currentPage].hasResult = true
this.$emit('primary', this.currentGroupPage, this.questionGroups)
},
// 上一大题
prev() {
if (this.currentGroupPage <= 1) return
this.currentGroupPage--
this.handlePageChange(this.currentGroupPage)
},
// 下一大题
next() {
if (this.currentGroupPage >= this.currentGroupCount) return
this.currentGroupPage++
this.handlePageChange(this.currentGroupPage)
},
// 答案改变
handleChange(data) {
if (this.numberGroups.length) {
this.updateNumberGroupsAnswer(data)
}
},
// 翻页
handlePageChange(index) {
this.currentGroupPage = index
this.$emit('page-change', this.currentGroupPage, this.currentQuestionGroup, this.questionGroups)
},
// 是否正确
updateNumberGroupsAnswer(data) {
this.numberGroups.forEach(group => {
const found = group.question_list.find(item => item.question_id === data.question_id)
if (found) {
found.answer = 3
}
})
},
// 收藏
toggleCollect() {
const ids = []
// 第一题的收藏状态
const isCollection = this.firstQuestion.is_collection
this.currentQuestionList.forEach(item => {
item.is_collection = !isCollection
ids.push(item.id || item.question_id)
})
isCollection
? api.deleteCollection({ type: 2, question_id: ids.join() })
: api.addCollection({ question_id: ids.join() })
},
// 标记
toggleMark() {
this.currentQuestionList.forEach(item => {
item.sign = !item.sign
})
},
// 提交
submit() {
this.$emit('submit', this.questionGroups)
},
// 数据初始化
dataInit(data) {
if (!data.questions) {
return
}
const isSubmited = ['1', '2'].includes(data.status)
this.disabled = isSubmited
this.hasResult = isSubmited && this.status !== 3
this.genQuestions(data)
this.currentGroupCount = this.questionGroups.length
if (this.$route.query.id) {
this.findGroupPageByQuestionId(this.$route.query.id)
}
// 答题倒计时
if (this.hasCountdown) {
this.countDown(this.data.remaining_times)
this.duration = this.data.duration || 0
}
},
// 组装试题数据
genQuestions(data) {
const { questions, answers = {}, score_items: scores = {} } = data
if (!questions) return []
this.questionGroups = questions.question_items.reduce((result, question) => {
if (question.question_list.length) {
question.question_list.forEach(list => {
list = list.map(item => {
let userAnswers = []
let sign = false
let scoreItem = {}
// 答案
if (answers) {
// 大题答案包含所有小题答案
const bigQuestionAnswer = answers[question.question_item_id]
if (bigQuestionAnswer) {
// 小题答案
const questionAnswer = bigQuestionAnswer[item.id] || {}
userAnswers = questionAnswer.answer || []
sign = questionAnswer.sign || false
}
}
// 分数与结果
if (scores) {
const bigQuestionScore = scores[question.question_item_id]
if (bigQuestionScore) {
// 小题分数
scoreItem = bigQuestionScore[item.id] || {}
scoreItem.user_score = scoreItem.score
if (userAnswers.length) {
scoreItem.answer = scoreItem.is_right ? 1 : 2
} else {
scoreItem.answer = 0
}
}
}
return {
...{ comment_visible: false, result_visible: false },
...scoreItem,
...item,
user_answer: userAnswers,
sign
}
})
result.push(Object.assign({}, question, { question_list: list }))
})
} else {
result.push(Object.assign({}, question, { question_list: [] }))
}
return result
}, [])
},
// 重置
reset() {
this.currentGroupPage = 1
},
// 通过小题ID查找大题页码
findGroupPageByQuestionId(id) {
this.questionGroups.forEach((item, index) => {
const findIndex = item.question_list.findIndex(data => data.id === id)
if (findIndex !== -1) {
this.currentGroupPage = index + 1
}
})
}
}
}
</script>
<style lang="scss" scoped>
.answer-box {
display: flex;
flex-direction: column;
width: 100%;
height: 100vh;
overflow: hidden;
// background: #f9f9f9;
.head {
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;
}
.time {
display: flex;
.icon {
width: 23px;
height: 23px;
// background: url(../../assets/images/tick.png);
background-size: 100% 100%;
}
.mun {
font-size: 18px;
font-weight: bold;
color: #222222;
line-height: 25px;
margin-left: 10px;
}
}
}
}
.exam-main {
flex: 1;
display: flex;
overflow: hidden;
.left {
background: #fff;
flex: 1;
padding: 10px 20px 0 53px;
overflow-y: scroll;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
.right {
border-left: 1px solid #ccc;
position: relative;
width: 220px;
background: #fff;
padding: 0 20px;
overflow-y: scroll;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
}
.foot {
border-top: 1px solid #ccc;
height: 60px;
background: #ffffff;
display: flex;
align-items: center;
.exam-btn {
display: flex;
padding-left: 40px;
cursor: pointer;
div {
width: 100px;
height: 40px;
border-radius: 4px;
border: 1px solid #cccccc;
font-size: 14px;
font-weight: bold;
color: #999999;
line-height: 40px;
text-align: center;
margin-right: 20px;
&.active {
background: #c01540;
border-radius: 4px;
color: #fff;
}
}
}
.rigth-btn {
display: flex;
margin-left: auto;
.del-btn {
margin-top: 10px;
width: 100px;
height: 40px;
border-radius: 4px;
border: 1px solid #cccccc;
line-height: 40px;
font-size: 14px;
font-weight: bold;
color: #999999;
text-align: center;
margin-right: 30px;
}
.end-exam-btn {
background: #fff;
height: 62px;
margin-top: -2px;
border-left: 1px solid #ccc;
width: 260px;
display: flex;
justify-content: center;
align-items: center;
.btn {
cursor: pointer;
width: 200px;
height: 40px;
background: #c01540;
border-radius: 4px;
font-size: 14px;
font-weight: bold;
color: #fff;
line-height: 40px;
text-align: center;
}
}
.sign {
margin-right: 20px;
margin-top: 8px;
.icon {
margin: 0 auto;
width: 24px;
height: 24px;
background: url(@/assets/images/collection.png);
background-size: 100% 100%;
&.active {
background: url(@/assets/images/collection2.png);
background-size: 100% 100%;
}
}
.txt {
font-size: 14px;
color: #cccccc;
line-height: 20px;
margin-top: 2px;
}
}
.sign2 {
margin-right: 20px;
margin-top: 8px;
.icon {
margin: 0 auto;
width: 24px;
height: 24px;
background: url(@/assets/images/sign.png);
background-size: 100% 100%;
&.active {
background: url(@/assets/images/sign2.png);
background-size: 100% 100%;
}
}
.txt {
font-size: 14px;
color: #cccccc;
line-height: 20px;
margin-top: 2px;
}
}
}
}
}
</style>
<template>
<div class="question-list">
<div class="question-list-hd">
<h4 class="question-list-hd__title">{{ questionTypeText }}</h4>
<aside class="question-list-hd__aside"><slot name="index"></slot></aside>
</div>
<div class="question-list-bd">
<h2 class="question-list-title" v-if="conmonTitle" v-html="conmonTitle">
<!-- <span class="num">{{ page }}.</span> -->
<!-- {{ conmonTitle }} -->
</h2>
<question-list-item
v-for="(item, i) in data.question_list"
:data="item"
:question="data"
:page="data.question_list.length > 1 ? i + 1 : page"
:key="item.id"
v-bind="$attrs"
v-on="$listeners"
>
<slot v-bind="{ item, data }"></slot>
</question-list-item>
</div>
</div>
</template>
<script>
import QuestionListItem from './questionListItem.vue'
export default {
props: {
data: { type: Object, default: () => ({}) },
page: { type: Number, default: 0 } // 页码
},
components: { QuestionListItem },
computed: {
// 试题类型
questionTypeText() {
const map = { 1: '单选题', 2: '多选题', 3: '问答题', 5: '案例题', 6: '判断题', 7: '实操题', 8: '情景题' }
return map[this.data.question_type] || this.data.question_type
},
// 公共题干
conmonTitle() {
const [first = {}] = this.data.question_list || []
return first.common_content
}
}
}
</script>
<style lang="scss" scoped>
.question-list {
position: relative;
}
.question-list-hd {
// position: sticky;
// top: 0;
display: flex;
align-items: center;
height: 45px;
background-color: #fff;
}
.question-list-hd__title {
flex: 1;
font-size: 18px;
color: #222;
}
.question-list-hd__aside {
font-size: 18px;
color: #222;
}
.question-list-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>
<template>
<div class="question-list-item">
<div class="question-list-item-hd">
<div class="question-list-item-hd__num">{{ page }}.</div>
<div class="question-list-item-hd__title" v-html="data.question_content"></div>
</div>
<div class="question-list-item-bd">
<!-- 单选 -->
<template v-if="[1, 6].includes(questionType)">
<el-radio-group v-model="data.user_answer[0]" :disabled="disabled" @change="handleChange">
<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 v-model="data.user_answer" :disabled="disabled" @change="handleChange">
<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"
v-model="data.user_answer[0]"
placeholder="请输入答案内容"
:autosize="{ minRows: 4, maxRows: 6 }"
:disabled="disabled"
@blur="handleChange"
:maxlength="100"
:show-word-limit="true"
></el-input>
</template>
<template v-if="questionType === 3">
<slot></slot>
</template>
</div>
<div class="question-list-item-ft" v-if="hasResult">
<h3 class="question-list-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="data.comment">
<div class="answer-item-label">老师点评:</div>
<div class="answer-item-content">{{ data.comment }}</div>
</div>
</template>
<div class="answer-item" v-if="data.question_analysis">
<div class="answer-item-label">解析:</div>
<div class="answer-item-content" v-html="data.question_analysis"></div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
page: { type: Number, default: 0 }, // 题号
data: { type: Object, default: () => ({}) }, // 小题
question: { type: Object, default: () => ({}) }, // 大题
disabled: { type: Boolean, default: false },
hasResult: { type: Boolean, default: false }
},
data() {
return {}
},
computed: {
questionType() {
// (1:单选题,2:多选题,3:问答题,5:案例题,6:判断题,7:实操题,8:情景题)
const questionType = parseInt(this.question.question_type)
const { answer_count: answerCount = 0 } = this.data
if ([5, 7, 8].includes(questionType)) {
if (answerCount >= 2) {
return 2
}
return answerCount || 3
} else {
return questionType
}
},
// 26个英文字母
A_Z() {
const result = []
for (let i = 0; i < 26; i++) {
result.push(String.fromCharCode(65 + i))
}
return result
},
// 处理后的options数据
currentOptions() {
if (!this.data.question_options) {
return []
}
return this.data.question_options.map((item, index) => {
// 英文字母 + 名称
item.abc = this.A_Z[index]
item.abc_option = `${this.A_Z[index]}. ${item.option}`
// 提交时的选中状态
item.checked = this.data.user_answer.includes(item.id)
// 处理正确的选中状态
const hasChecked = Object.prototype.hasOwnProperty.call(item, 'isRight')
const rightAnswer = this.data.question_answer || []
if (!hasChecked && rightAnswer) {
item.isRight = Array.isArray(rightAnswer) ? rightAnswer.includes(item.id) : rightAnswer === item.id
}
return item
})
},
// 正确答案显示的英文字母
correctAnswerText() {
const result = this.currentOptions.reduce((result, item) => {
item.isRight && result.push(item.abc)
return result
}, [])
return result.join('、')
},
// // 提交答案显示的英文字母
submitAnswerText() {
const result = this.currentOptions.reduce((result, item) => {
item.checked && result.push(item.abc)
return result
}, [])
return result.join('、')
}
},
methods: {
handleChange() {
this.$emit('change', this.data)
}
}
}
</script>
<style lang="scss" scoped>
.question-list-item {
margin-bottom: 40px;
}
.question-list-item-hd {
display: flex;
}
.question-list-item-hd__num {
font-size: 32px;
font-weight: bold;
color: #222;
line-height: 45px;
margin-top: 5px;
}
.question-list-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;
::v-deep .el-radio,
::v-deep .el-checkbox {
display: flex;
align-items: center;
}
}
.question-option-item__text {
display: inline-block;
font-size: 18px;
color: #222;
}
::v-deep .el-radio__inner,
::v-deep .el-checkbox__inner {
width: 18px;
height: 18px;
}
::v-deep .el-checkbox__inner::after {
width: 4px;
height: 9px;
left: 6px;
}
.question-list-item-ft {
margin-top: 20px;
}
.question-list-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>
<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 item.question_list"
class="question-num-item"
:class="genClass(item)"
:key="index"
@click="handleClick(item)"
>
{{ index + 1 }}
</li>
</ul>
</div>
</div>
<slot></slot>
<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>
<script>
export default {
props: {
status: { type: Number, default: 1 }, // 1:答题;2:查看;3:批阅
page: { type: Number, default: 0 }, // 当前大题的页码
data: { type: Object, default: () => ({}) }, // 当前大题
list: { type: Array, default: () => [] } // 所有大题
},
data() {
return {
questionNumTips: {
1: [
{ class: 'is-info', name: '已答' },
{ class: 'is-default', name: '未答' },
{ class: 'is-success', name: '当前' },
{ class: 'is-mark', name: '标记' }
],
2: [
{ class: 'is-success', name: '答对' },
{ class: 'is-error', name: '答错' },
{ class: 'is-info', name: '未答' }
],
3: [
{ class: 'is-success', name: '已批阅' },
{ class: 'is-error', name: '未批阅' },
{ class: 'is-info', name: '未答' }
]
}
}
},
computed: {
dataList() {
const results = []
this.list.forEach((item, index) => {
const findIndex = results.findIndex(data => data.question_item_id === item.question_item_id)
const quesitonlist = item.question_list.map(item => {
return { ...item, big_num: index + 1 }
})
if (findIndex === -1) {
results.push({
title: item.title,
question_type: item.question_type,
question_item_id: item.question_item_id,
question_list: quesitonlist
})
} else {
results[findIndex].question_list = results[findIndex].question_list.concat(quesitonlist)
}
})
return results
},
questionNum() {
return this.questionNumTips[this.status]
}
},
methods: {
genClass(data) {
// answer(0:未做,1:正确,2:错误)
if (this.status === 1) {
return {
'is-info': data.user_answer ? data.user_answer.length : false, // 已做
'is-success': data.big_num === this.page, // 当前
'is-mark': data.sign // 标记
}
}
if (this.status === 2) {
return {
'is-success': data.answer === 1, // 答对
'is-error': data.answer === 2, // 答错
'is-info': data.answer === 0 // 未答
}
}
if (this.status === 3) {
return {
'is-success': data.checked_flag, // 已批阅
'is-error': !data.checked_flag, // 未批阅
'is-info': data.answer === 0 // 未做
}
}
},
handleClick(data) {
this.$emit('page-change', data.big_num, data)
}
}
}
</script>
<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>
<template>
<div>
<!-- stu1已答 stu2当前 stu3标记 -->
<div class="order-num" v-if="questionParams.card">
<template v-for="(item, index) in questionParams.card">
<div :key="index" v-if="item">
<div class="tit">{{ item.find(tit => { return tit.question_item_title }).question_item_title }}</div>
<ul>
<template v-for="(cItem, cIndex) in item">
<li
:key="cItem.q_order + '-' + cIndex"
@click="goQuestion(cItem.q_order)"
:class="$route.query.id ? isAnalysisClass(cItem) + ' analy' : isClass(cItem)"
>{{ cItem.q_order }}</li>
</template>
</ul>
</div>
</template>
</div>
<ul class="flag-tips" v-if="!this.$route.query.id">
<li>
<div class="circle1"></div>
<div class="txt">已答</div>
</li>
<li>
<div class="circle2"></div>
<div class="txt">未答</div>
</li>
<li>
<div class="circle3"></div>
<div class="txt">当前</div>
</li>
<li>
<div class="circle4"></div>
<div class="txt">标记</div>
</li>
</ul>
<ul class="tips-box" v-else>
<li>
<div class="circle1"></div>
<div class="txt">答对</div>
</li>
<li>
<div class="circle2"></div>
<div class="txt">答错</div>
</li>
<li>
<div class="circle3"></div>
<div class="txt">未答</div>
</li>
</ul>
</div>
</template>
<script>
export default {
props: {
questionParams: { type: Object, default: () => {} },
info: { type: Object, default: () => {} }
},
mounted() {
this.msgCenter.$on('monitoringChanges', this.monitoringChanges)
},
computed: {
changeQuestionIndex() {
return this.questionParams.answerRecord
},
isClass() {
return (cItem) => {
const currentAnswer = this.questionParams.answerRecord[cItem.question_item_id]
return this.questionParams.questionIndex + 1 === cItem.q_order
? currentAnswer
? currentAnswer[cItem.id]
? currentAnswer[cItem.id].answer
? currentAnswer[cItem.id].answer.length !== 0
? currentAnswer[cItem.id].sign
? 'stu1 stu2 stu3'
: 'stu1 stu2'
: currentAnswer[cItem.id].sign
? 'stu2 stu3'
: 'stu2'
: currentAnswer[cItem.id].sign
? 'stu2 stu3'
: 'stu2'
: 'stu2'
: 'stu2'
: currentAnswer
? currentAnswer[cItem.id]
? currentAnswer[cItem.id].answer
? currentAnswer[cItem.id].answer.length !== 0
? currentAnswer[cItem.id].sign
? 'stu1 stu3'
: 'stu1'
: currentAnswer[cItem.id].sign
? 'stu3'
: ''
: currentAnswer[cItem.id].sign
? 'stu3'
: ''
: ''
: ''
}
},
isAnalysisClass() {
return (cItem) => {
const findItems = this.questionParams.beforeData.answers[cItem.question_item_id]
const scoreItems = this.questionParams.beforeData.score_items[cItem.question_item_id][cItem.id]
return findItems
? findItems[cItem.id]
? findItems[cItem.id].answer
? findItems[cItem.id].answer.length
? scoreItems.is_right
? 'stu1'
: 'stu2'
: 'stu3'
: 'stu3'
: 'stu3'
: 'stu3'
}
}
},
methods: {
onSignHandle(stu) {
const id = this.questionParams.question
this.questionParams.answerRecord[id.question_item_id]
? this.questionParams.answerRecord[id.question_item_id][id.id]
? this.questionParams.answerRecord[id.question_item_id][id.id].sign = stu
: this.questionParams.answerRecord[id.question_item_id][id.id] = {
sign: stu
}
: this.questionParams.answerRecord[id.question_item_id] = {
[id.id]: { sign: stu }
}
this.$forceUpdate()
},
monitoringChanges() {
this.$forceUpdate()
},
goQuestion(n) {
this.questionParams.questionIndex = n - 1
}
},
watch: {
changeQuestionIndex(newV, oldV) {
this.$forceUpdate()
}
}
}
</script>
<style lang="scss" scoped>
.info{
display: flex;
align-items: center;
height: 100px;
.shape{
width: 60px;
height: 60px;
border-radius: 50%;
overflow: hidden;
}
img{
width: 75px;
// transform: scale(1);
display: block;
}
.right{
margin-left: 22px;
.name{
font-size: 18px;
color: #222222;
line-height: 25px;
}
.code{
font-size: 14px;
color: #222222;
line-height: 20px;
margin-top: 5px;
}
}
}
.order-num{
padding-top: 20px;
padding-bottom: 90px;
.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;
li{
cursor: pointer;
position: relative;
border-radius: 50px;
width: 24px;
height: 24px;
border: 1px solid #CCCCCC;
font-size: 14px;
color: #666666;
line-height: 24px;
margin-right: 20px;
margin-bottom: 10px;
text-align: center;
&:nth-child(5n+5){
margin-right: 0;
}
&.analy{
&.stu1{
border: 2px solid #0FC118;
line-height: 22px;
background: #fff;
color: #999;
}
&.stu2{
border: 2px solid #C01540;
line-height: 22px;
background: #fff;
color: #999;
}
&.stu3{
color: #fff;
background: #999999;
&::after{
content: '';
position: absolute;
top: -1px;
right: -1px;
width: 4px;
height: 4px;
background: none;
border-radius: 50%;
}
}
}
&.stu1{
background: #999;
border: 1px solid #999;
color: #fff;
}
&.stu2{
width: 22px;
height: 22px;
line-height: 22px;
border: 2px solid #0FC118;
}
&.stu3{
&::after{
content: '';
position: absolute;
top: -1px;
right: -1px;
width: 4px;
height: 4px;
background: #C01540;
border-radius: 50%;
}
}
}
}
}
.flag-tips{
width: 260px;
position: fixed;
bottom: 60px;
right:0;
display: flex;
justify-content: space-around;
padding: 15px 0 10px;
background: #fff;
margin: 0;
list-style: none;
li{
.circle1{
width: 24px;
height: 24px;
background: #EEEEEE;
border: 1px solid #CCCCCC;
border-radius: 50%;
}
.circle1{
width: 24px;
height: 24px;
border: 1px solid #CCCCCC;
border-radius: 50%;
}
.circle2{
width: 24px;
height: 24px;
border: 1px solid #CCCCCC;
border-radius: 50%;
}
.circle3{
width: 24px;
height: 24px;
border: 2px solid #0FC118;
border-radius: 50%;
}
.circle4{
position: relative;
width: 24px;
height: 24px;
border: 1px solid #CCCCCC;
border-radius: 50%;
&::after{
content: '';
position: absolute;
top: -1px;
right: -1px;
width: 4px;
height: 4px;
background: #C01540;
border-radius: 50%;
}
}
.txt{
margin-top: 5px;
font-size: 12px;
color: #CCCCCC;
line-height: 17px;
}
}
}
.flag-tips{
width: 260px;
position: fixed;
bottom: 60px;
right:0;
display: flex;
justify-content: space-around;
padding: 15px 0 10px;
background: #fff;
margin: 0;
list-style: none;
li{
.circle1{
width: 24px;
height: 24px;
background: #999;
border: 1px solid #999;
border-radius: 50%;
}
.circle2{
width: 24px;
height: 24px;
border: 1px solid #CCCCCC;
border-radius: 50%;
}
.circle3{
width: 24px;
height: 24px;
border: 2px solid #0FC118;
background: rgba(15, 193, 24, 0.1);
border-radius: 50%;
}
.circle4{
position: relative;
width: 24px;
height: 24px;
border: 1px solid #CCCCCC;
border-radius: 50%;
&::after{
content: '';
position: absolute;
top: -1px;
right: -1px;
width: 4px;
height: 4px;
background: #C01540;
border-radius: 50%;
}
}
.txt{
margin-top: 5px;
font-size: 12px;
color: #CCCCCC;
line-height: 17px;
}
}
}
.tips-box{
width: 260px;
position: fixed;
bottom: 60px;
right:0;
display: flex;
justify-content: space-around;
padding: 15px 0 10px;
background: #fff;
margin: 0;
list-style: none;
li{
&:nth-child(2){
// margin: 0 50px;
}
.circle1{
width: 24px;
height: 24px;
background: #fff;
border: 2px solid #0FC118;
border-radius: 50%;
box-sizing: border-box;
}
.circle2{
width: 24px;
height: 24px;
border: 2px solid #C01540;
border-radius: 50%;
box-sizing: border-box;
}
.circle3{
width: 24px;
height: 24px;
background: #999999;
border-radius: 50%;
}
.circle4{
position: relative;
width: 24px;
height: 24px;
border: 1px solid #CCCCCC;
border-radius: 50%;
&::after{
content: '';
position: absolute;
top: -1px;
right: -1px;
width: 4px;
height: 4px;
background: #C01540;
border-radius: 50%;
}
}
.txt{
margin-top: 5px;
font-size: 12px;
color: #CCCCCC;
line-height: 17px;
}
}
}
</style>
<template>
<div class="chart">
<svg width="148" height="148" viewbox="0 0 148 148" class="svg-rotate">
<circle cx="74" cy="74" r="70" stroke-width="7" stroke="#efefef" fill="none"></circle>
<circle cx="74" v-if="this.accuracy" cy="74" r="70" stroke-width="7" stroke="#4cce8c" fill="none" :stroke-dasharray="data" stroke-linecap="round"></circle>
</svg>
<div class="chart-txt">
<slot name="tips"></slot>
</div>
</div>
</template>
<script>
export default {
props: {
accuracy: { type: Number },
accuracScore: { type: Number }
},
data() {
return {
data: ''
}
},
mounted() {
const percent = this.accuracy / this.accuracScore
const perimeter = Math.PI * 2 * 70
this.data = perimeter * percent + ' ' + perimeter * (1 - percent)
}
}
</script>
<style lang="scss" scoped>
.chart{
position: relative;
width: auto;
.chart-txt{
position: absolute;
top: 50%;
left: 0;
width: 100%;
text-align: center;
transform:translateY(-50%);
.num{
color: #333;
font-size: 38px;
font-weight: bold;
}
.t{
color: #999;
font-size: 18px;
}
}
}
.svg-rotate{
transform:rotate(-90deg);
}
</style>
<template>
<div class="app-aside">
<div class="inner">
<div class="user" v-if="showUser">
<div class="user-avatar"><img :src="avatar" /></div>
<div class="user-tools">
<span><router-link to="/account/password">修改密码</router-link></span>
<span @click="logout">退出登录</span>
</div>
</div>
<el-menu class="nav" :unique-opened="true" :default-active="defaultActive">
<template v-for="item in currentMenus">
<el-submenu :index="item.title" :key="item.title" v-show="menuVisible(item.tag)" v-if="item.children">
<template #title>
<i class="iconfont" :class="item.icon"></i><span>{{ item.title }}</span>
</template>
<el-menu-item
:index="item.path"
:key="item.title"
v-for="item in item.children"
@click="handleClick(item.path, item)"
v-show="menuVisible(item.tag)"
>
<template #title>
<template v-if="item.href">
<a :href="item.href" target="_blank">{{ item.title }}</a>
</template>
<template v-else>{{ item.title }}</template>
</template>
</el-menu-item>
</el-submenu>
<el-menu-item
:index="item.path"
:key="item.title"
@click="handleClick(item.path, item)"
v-show="menuVisible(item.tag)"
v-else
>
<i class="iconfont" :class="item.icon"></i>
<span slot="title">{{ item.title }}</span>
</el-menu-item>
</template>
</el-menu>
</div>
</div>
</template>
<script>
import defaultAvatar from '@/assets/images/avatar.png'
export default {
name: 'AppAside',
props: {
menus: { type: Array, default: () => [] },
showUser: { type: Boolean, default: false }
},
data() {
return {
studentMenus: [
{
tag: 'menu_help',
title: '学员须知',
icon: 'icon-bianzu8-hong',
path: '/notice'
},
{
tag: 'menu_course',
title: '我的课程',
icon: 'icon-bianzu6-hong',
children: [{ tag: 'menu_course_learn', title: '课程学习', path: '/course/learn' }]
},
{
tag: 'menu_exam',
title: '我的考试',
icon: 'icon-bianzuhong',
children: [
{ tag: 'menu_exam_paper_review', title: '试卷管理', path: '/exam/exam' },
{ tag: 'menu_exam_exercise_review', title: '考试记录', path: '/exam/record' }
]
},
{
tag: 'menu_training',
title: '我的证书',
icon: 'icon-kaoshihong',
path: '/cert'
},
{
tag: 'menu_my',
title: '个人中心',
icon: 'icon-guanlizhongxinbeifen-hong',
children: [
{ tag: 'menu_my_info', title: '个人信息', path: '/account' },
{ tag: 'menu_my_password', title: '修改密码', path: '/account/password' },
{ tag: 'menu_my_safe', title: '安全设置', path: '/account/safe' }
]
}
]
}
},
computed: {
currentMenus() {
if (this.menus && this.menus.length) {
return this.menus
}
return this.studentMenus
},
user() {
return this.$store.state.user
},
avatar() {
return this.user.avatar || defaultAvatar
},
// 菜单权限
menuPermissions() {
// role: 1学生,2老师
// system_tag: 2教师,3学生
return this.$store.state.permissions
},
defaultActive() {
// 扁平菜单
const flatMenuList = this.currentMenus.reduce((result, item) => {
result.push(item)
if (item.children) {
result = result.concat(item.children)
}
return result
}, [])
const found = flatMenuList.reverse().find(item => {
return this.$route.path.includes(item.path)
})
return found ? found.path : '/'
}
},
methods: {
menuVisible(tag) {
if (!tag) {
return true
}
return !!this.menuPermissions.find(item => item.tag === tag)
},
genClasses(data) {
const isActive = this.$route.fullPath.includes(data.path)
return { 'is-active': isActive }
},
// 退出登录
logout() {
this.$store.dispatch('logout').then(() => {
window.location.href = import.meta.env.VITE_LOGIN_URL
})
},
handleClick(path, item) {
if (item.href) return
path && this.$router.push(path)
}
}
}
</script>
<style lang="scss">
.app-aside {
width: 200px;
background-color: #fff;
.inner {
position: sticky;
top: 0;
}
.user {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 20px;
&:hover {
background-color: #fff4f7;
}
}
.user-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.user-tools {
padding: 10px 0;
span {
color: #999;
padding: 0 10px;
cursor: pointer;
}
}
.nav {
border: 0;
padding: 30px 0;
color: #ccc;
.iconfont {
display: inline-block;
width: 30px;
font-size: 16px;
color: currentColor;
}
.el-submenu__title:hover {
color: #c01540;
}
.is-active {
color: #c01540;
}
.is-active .el-submenu__title {
background: #fff4f7;
font-weight: bold;
color: #c01540;
}
.el-submenu__title {
height: 50px;
line-height: 50px;
padding-left: 25px !important;
}
.el-menu-item:hover,
.el-menu-item:focus {
color: #c01540;
background: transparent;
}
.el-submenu .el-menu-item {
height: 36px;
line-height: 36px;
padding-left: 55px !important;
}
}
}
</style>
<template>
<div class="app-header">
<div class="title">
<router-link to="/">{{ title }}</router-link>
</div>
<div class="tool">
<el-dropdown>
<div class="user">
<img :src="avatar" class="user-avatar" />
<span class="user-name" v-if="user.realname || user.nickname">{{ user.realname || user.nickname }}</span>
</div>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item icon="el-icon-user" @click.native="$router.push('/account')">个人中心</el-dropdown-item>
<el-dropdown-item icon="el-icon-switch-button" @click.native="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
// import AppSearchBar from '@/components/base/AppSearchBar.vue'
import defaultAvatar from '@/assets/images/avatar.png'
export default {
name: 'AppHeader',
// components: { AppSearchBar },
data() {
return {
title: '金融产品数字化营销职业技能等级证书'
}
},
computed: {
user() {
return this.$store.state.user
},
avatar() {
return this.user.avatar || defaultAvatar
}
},
methods: {
handleSearch(value) {
this.$router.replace({ path: '/search', query: { keywords: value } })
},
// 退出登录
logout() {
this.$store.dispatch('logout').then(() => {
window.location.href = import.meta.env.VITE_LOGIN_URL
})
}
}
}
</script>
<style lang="scss">
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 80px;
padding: 0 30px;
background-color: #fff;
.tool {
display: flex;
align-items: center;
}
.title {
font-size: 24px;
font-weight: 600;
color: #222;
}
.nav {
margin-left: 20px;
font-size: 18px;
color: #333;
a {
padding: 0 20px;
}
}
.user {
height: 80px;
padding: 0 10px;
display: flex;
align-items: center;
cursor: pointer;
&:hover {
background-color: #fff4f7;
}
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
object-fit: cover;
}
.user-name {
padding: 0 10px;
}
}
</style>
<template>
<div class="app-layout">
<app-header />
<div class="app-layout-bd">
<app-aside v-bind="$attrs" v-if="showAside" />
<app-main />
</div>
</div>
</template>
<script>
import AppHeader from './header.vue'
import AppAside from './aside.vue'
import AppMain from './main.vue'
export default {
components: { AppHeader, AppAside, AppMain },
props: { showAside: { type: Boolean, default: true } }
}
</script>
<style lang="scss">
.app-layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.app-layout-bd {
flex: 1;
display: flex;
.app-main {
flex: 1;
overflow: hidden;
}
}
</style>
<template>
<div class="app-main">
<router-view :key="$route.fullPath" />
</div>
</template>
<script>
export default {}
</script>
<style lang="scss">
.app-main {
padding: 20px;
}
</style>
<template>
<editor :init="init" v-bind="$attrs" v-on="$listeners" @onChange="onChange" @onBlur="onBlur" />
</template>
<script>
import Editor from '@tinymce/tinymce-vue'
import ImageUpload from './imageUpload'
export default {
components: {
editor: Editor
},
data() {
return {
init: {
min_height: 600,
max_height: 600,
menubar: false,
statusbar: false,
plugins: 'table autoresize charmap fullscreen hr lists link code preview quickbars',
toolbar:
'undo redo | fontsizeselect lineheight bold italic underline strikethrough forecolor backcolor | link quickimage image media table | align hangingindent indent outdent numlist bullist | charmap blockquote hr fullscreen | code preview',
// font_formats:
// '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Times New Roman',
fontsize_formats: '8px 10px 12px 14px 15px 16px 17px 18px 20px 24px',
lineheight_formats: '0.5 1 1.2 1.5 2',
images_upload_handler: ImageUpload,
automatic_uploads: true,
quickbars_insert_toolbar: false,
// style_formats: [{ title: '悬挂缩进', block: 'p', styles: { textIndent: '-2em', paddingLeft: '2em' } }],
content_style: 'img {max-width:100%;}'
}
}
},
methods: {
onChange(event, editor) {
this.dispatch('ElFormItem', 'el.form.change', editor.getContent())
},
onBlur(event, editor) {
this.dispatch('ElFormItem', 'el.form.blur', editor.getContent())
},
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root
var name = parent.$options.componentName
while (parent && (!name || name !== componentName)) {
parent = parent.$parent
if (parent) {
name = parent.$options.componentName
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params))
}
}
}
}
</script>
<style>
.tox .tox-tbtn--bespoke .tox-tbtn__select-label {
width: 4em !important;
}
</style>
import { getSignature, uploadFile } from '@/api/base'
import md5 from 'blueimp-md5'
export default function (blobInfo, succFun, failFun) {
const file = blobInfo.blob()
getSignature()
.then(response => {
const prefix = 'upload/admin/'
const fileName = file.name
const key = prefix + md5(fileName + new Date().getTime()) + fileName.substr(fileName.lastIndexOf('.'))
const { accessid, policy, signature, host } = response
const data = { key, OSSAccessKeyId: accessid, policy, signature, success_action_status: '200', file }
const fileUrl = `${host}/${key}`
uploadFile(data)
.then(() => {
succFun(fileUrl)
})
.catch(() => {
failFun('上传失败')
})
})
.catch(response => {
failFun('获取Signature失败')
})
}
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import modules from './modules'
// 公共css
import './assets/css/base.css'
// Element-UI
import ElementUI from 'element-ui'
import './assets/theme/style.scss'
// 公共组件
import AppContainer from './components/base/AppContainer.vue'
import AppCard from './components/base/AppCard.vue'
import AppList from './components/base/AppList.vue'
import beforeEnter from './utils/beforeEnter'
import * as echarts from 'echarts'
Vue.prototype.$echarts = echarts
// 注册element-ui组件
Vue.use(ElementUI)
// 注册公共组件
Vue.component('AppContainer', AppContainer)
Vue.component('AppCard', AppCard)
Vue.component('AppList', AppList)
// 注册模块
modules({ router, store })
router.beforeEach(beforeEnter)
new Vue({
store,
router,
render: h => h(App)
}).$mount('#app')
import httpRequest from '@/utils/axios'
// 登录
export function login(data) {
return httpRequest.post('/api/passport/rest/login', data)
}
// 绑定微信
export function bindWechat(data) {
return httpRequest.post('/api/passport/rest/wechat/bind-unionid', data)
}
// 修改密码
export function updatePassword(data) {
return httpRequest.post('/api/usercenter/user/change-pwd-by-cookie', data)
}
// 重置密码
export function resetPassword(data) {
return httpRequest.post('/api/usercenter/user/update-pwd', data)
}
// 发送验证码
export function sendCode(data) {
return httpRequest.post('/api/usercenter/user/send-code', data)
}
// 登出
export function logout() {
return httpRequest.get('/api/zy/user/logout')
}
// 获取用户信息
export function getUser() {
return httpRequest.get('/api/zy/user/getinfo')
}
// 修改用户信息
export function updateUser(data) {
return httpRequest.post('/api/usercenter/user/update-user', data)
}
// 绑定游客
export function bindVisitor(data) {
return httpRequest.post('/api/zy/user/bind-account', data)
}
// 获取是否VIP
export function getIsVip() {
return httpRequest.get('/api/zy/user/is-vip')
}
// 创建游客用户
export function createGuestUser() {
return httpRequest.get('/api/zy/user/create-guest-user')
}
// 校验验证码
export function checkCode(params) {
return httpRequest.get('/api/usercenter/user/check-code', { params })
}
// 选择用户角色
export function chooseRole(data) {
return httpRequest.post('/api/zy/user/choose-role', data)
}
// 获取所有权限
export function getPermissions() {
return httpRequest.get('/api/zy/user/get-permissions')
}
const routes = [
{
path: '/',
component: () => import('@/components/layout/index.vue'),
children: [
{ path: '/account', component: () => import('./views/index.vue') },
/* 修改密码 */
{ path: '/account/password', component: () => import('./views/password.vue') },
{ path: '/account/safe', component: () => import('./views/safe.vue') }
]
}
]
export { routes }
<template>
<app-container title="安全设置">
<el-form :model="ruleForm" :rules="rules" label-width="110px" ref="ruleForm" class="form" hide-required-asterisk>
<el-form-item label="设置新手机号码" prop="account">
<el-input v-model="ruleForm.account"></el-input>
</el-form-item>
<el-form-item label="输入验证码" prop="code">
<el-input maxlength="4" v-model="ruleForm.code">
<countdown slot="suffix" size="mini" :disabled="disabledSend" @start="sendCode" ref="countdown"></countdown>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="app-container-ft">
<el-button type="primary" @click="handleSubmit">保存</el-button>
</div>
</template>
</app-container>
</template>
<script>
import Countdown from '@/components/Countdown.vue'
import * as api from '../api.js'
export default {
components: { Countdown },
data() {
return {
ruleForm: { account: '', code: '' },
rules: {
account: [{ required: true, message: '请输入手机号码', trigger: 'blur' }],
code: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
}
}
},
computed: {
disabledSend() {
return !/^1[3-9]\d{9}$/.test(this.ruleForm.account)
}
},
methods: {
handleSubmit() {
this.$refs.ruleForm.validate().then(this.checkCode)
},
sendCode() {
api
.sendCode({ account: this.ruleForm.account })
.then(response => {
if (response.code === 0) {
this.$message({ type: 'success', message: '验证码发送成功' })
} else {
// 停止计时
this.$refs.countdown.stop()
this.$message({ type: 'error', message: response.msg })
}
})
.catch(this.$refs.countdown.stop)
},
checkCode() {
api.checkCode(this.ruleForm).then(response => {
this.updateUser()
})
},
updateUser() {
api
.updateUser({ mobile: this.ruleForm.account })
.then(response => {
this.$message({ type: 'success', message: '手机号码修改成功' })
this.$store.dispatch('getUser')
this.$emit('success')
})
.catch(() => {
this.$refs.countdown.stop()
})
}
}
}
</script>
<style lang="scss" scoped>
.form {
max-width: 400px;
}
</style>
<template>
<app-container title="安全设置">
<el-form :model="ruleForm" :rules="rules" label-width="82px" ref="ruleForm" class="form" hide-required-asterisk>
<el-form-item label="手机号码">
<el-input v-model="user.mobile" disabled></el-input>
<el-button @click="toggleUpdate" class="button-change">{{ buttonText }}</el-button>
</el-form-item>
<el-form-item label="输入验证码" prop="code" v-if="isUpdate && !isEmail">
<el-input maxlength="4" v-model="ruleForm.code">
<countdown slot="suffix" size="mini" @start="handlePhoneCode" ref="countdown"></countdown>
</el-input>
</el-form-item>
<div style="margin-bottom: 10px; margin-top: 40px; text-align: right">
<span>如手机无法收到验证码,请选择</span>
<el-button @click="handleEmailCode">注册邮箱接收验证码</el-button>
</div>
<template v-if="isEmail">
<!-- <p>请前往邮箱获取验证码后,</p> -->
<el-form-item label="输入验证码" prop="code">
<el-input maxlength="4" v-model="ruleForm.code"></el-input>
</el-form-item>
</template>
</el-form>
<template #footer>
<div class="app-container-ft">
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">提交</el-button>
</div>
</template>
</app-container>
</template>
<script>
import Countdown from '@/components/Countdown.vue'
import * as api from '../api.js'
export default {
components: { Countdown },
data() {
return {
ruleForm: {
account: '',
code: ''
},
rules: {
code: [{ required: true, message: '请输入验证码', trigger: 'blur' }]
},
isUpdate: false,
isEmail: false,
submitLoading: false
}
},
computed: {
user() {
return this.$store.state.user
},
buttonText() {
return this.isUpdate ? '取消' : '更改'
}
},
methods: {
toggleUpdate() {
this.isUpdate = !this.isUpdate
this.isEmail = false
},
handlePhoneCode() {
const account = this.user.mobile
if (!account) {
this.$message({ message: '该账号尚未绑定手机号码', type: 'error' })
return
}
this.ruleForm.account = account
this.sendPhoneCode(account)
},
// 发送手机验证码
sendPhoneCode(account) {
api
.sendCode({ account })
.then(response => {
if (response.code === 0) {
this.$message({ type: 'success', message: '验证码发送成功' })
} else {
// 停止计时
this.$refs.countdown.stop()
this.$message({ type: 'error', message: response.msg })
}
})
.catch(this.$refs.countdown.stop)
},
// 获取邮箱验证码
handleEmailCode() {
const account = this.user.email
if (!account) {
this.$message({ message: '该账号尚未绑定邮箱', type: 'error' })
return
}
this.isEmail = true
this.ruleForm.account = account
this.sendEmailCode(account)
},
// 发送邮箱验证码
sendEmailCode(account) {
api.sendCode({ account }).then(response => {
this.$message({ message: '验证码发送成功,请前往邮箱获取验证码', type: 'success' })
})
},
// 提交
handleSubmit() {
this.$refs.ruleForm.validate().then(this.checkCode)
},
// 校验验证码
checkCode() {
api.checkCode(this.ruleForm).then(response => {
this.$emit('success')
})
}
}
}
</script>
<style lang="scss" scoped>
.form {
max-width: 400px;
}
.button-change {
position: absolute;
right: -90px;
}
</style>
<template>
<app-container title="个人信息">
<el-form :model="ruleForm" :rules="rules" label-width="100px" ref="ruleForm" class="form" hide-required-asterisk>
<el-form-item label="所在院校" prop="school_name">
<el-input v-model="info.school_name" disabled></el-input>
</el-form-item>
<el-form-item label="院系名称" prop="college">
<el-input v-model="info.college" disabled></el-input>
</el-form-item>
<el-form-item label="专业名称" prop="major">
<el-input v-model="info.major" disabled></el-input>
</el-form-item>
<el-form-item label="考证等级" prop="grade">
<el-input v-model="info.grade" disabled></el-input>
</el-form-item>
<el-form-item label="用户真实姓名" prop="realname">
<el-input v-model="ruleForm.realname" disabled></el-input>
</el-form-item>
<el-form-item label="身份证号码" prop="id_number">
<el-input v-model="info.id_number" disabled></el-input>
</el-form-item>
<el-form-item label="身份" prop="role">
<el-select v-model="ruleForm.role" disabled style="width: 100%">
<el-option :value="1" label="学生"></el-option>
<el-option :value="2" label="老师"></el-option>
</el-select>
</el-form-item>
<el-form-item label="登录帐号" prop="mobile">
<el-input v-model="ruleForm.mobile" disabled></el-input>
</el-form-item>
<!-- <el-form-item label="账号名" prop="username">
<el-input v-model="ruleForm.username"></el-input>
</el-form-item> -->
<el-form-item label="常用邮箱" prop="email">
<el-input v-model="ruleForm.email"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="app-container-ft">
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</el-button>
</div>
</template>
</app-container>
</template>
<script>
import * as api from '../api.js'
export default {
data() {
return {
ruleForm: { username: '', email: '' },
rules: {
username: [{ required: true, message: '请输入账号名', trigger: 'blur' }],
email: [{ required: true, type: 'email', message: '请输入邮箱', trigger: 'blur' }]
},
submitLoading: false
}
},
computed: {
user() {
return this.$store.state.user
},
info() {
return this.user.role === 2 ? this.user.staff_info : this.user.student_info
}
},
watch: {
user: {
immediate: true,
handler(value) {
this.ruleForm = value
}
}
},
methods: {
handleSubmit() {
this.$refs.ruleForm.validate().then(this.handleSubmitRequest)
},
handleSubmitRequest() {
this.submitLoading = true
const params = {
// username: this.ruleForm.username,
email: this.ruleForm.email
}
api
.updateUser(params)
.then(response => {
this.ruleForm = response.data
this.$store.dispatch('getUser')
this.$message({ message: '修改成功', type: 'success' })
})
.finally(() => {
this.submitLoading = false
})
}
}
}
</script>
<style lang="scss" scoped>
.form {
max-width: 340px;
}
</style>
<template>
<app-container title="修改密码">
<el-form :model="ruleForm" :rules="rules" label-width="90px" ref="ruleForm" class="form" hide-required-asterisk>
<el-form-item label="旧密码" prop="old_password">
<el-input type="password" v-model="ruleForm.old_password" placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item label="新密码" prop="password">
<el-input type="password" v-model="ruleForm.password" placeholder="请输入新密码"></el-input>
</el-form-item>
<el-form-item label="重复新密码" prop="passwordR">
<el-input type="password" v-model="ruleForm.passwordR" placeholder="请重复新密码"></el-input>
</el-form-item>
</el-form>
<template #footer>
<div class="app-container-ft">
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存</el-button>
</div>
</template>
</app-container>
</template>
<script>
import * as api from '../api.js'
export default {
data() {
const validatePass = (rule, value, callback) => {
if (value === '') {
callback(new Error('请再次输入密码'))
} else if (value !== this.ruleForm.password) {
callback(new Error('两次输入密码不一致'))
} else {
callback()
}
}
return {
ruleForm: {
old_password: '',
password: '',
passwordR: ''
},
rules: {
old_password: { required: true, message: '请输入密码', trigger: 'blur' },
password: [
{ required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, max: 20, message: '长度为6-20个字符', trigger: 'blur' }
],
passwordR: [
{ required: true, message: '请重复新密码', trigger: 'blur' },
{ validator: validatePass, trigger: 'blur' }
]
},
submitLoading: false
}
},
methods: {
handleSubmit() {
this.$refs.ruleForm.validate().then(this.handleSubmitRequest)
},
handleSubmitRequest() {
this.submitLoading = true
api
.updatePassword(this.ruleForm)
.then(response => {
this.$message({ message: '密码修改成功', type: 'success' })
// 重置表单
this.$refs.ruleForm.resetFields()
})
.finally(() => {
this.submitLoading = false
})
}
}
}
</script>
<style lang="scss" scoped>
.form {
max-width: 340px;
}
</style>
<template>
<check-phone v-if="active === 1" @success="active = 2" />
<bind-phone v-else @success="active = 1" />
</template>
<script>
import CheckPhone from './checkPhone.vue'
import BindPhone from './bindPhone.vue'
export default {
components: { CheckPhone, BindPhone },
data() {
return { active: 1 }
}
}
</script>
import httpRequest from '@/utils/axios'
/* 意见反馈 */
export function submitFeedback(data) {
return httpRequest.post('/api/zy/v2/feedback/commit', data)
}
const routes = [
{
path: '/',
component: () => import('@/components/layout/index.vue'),
children: [{ path: '/cert', component: () => import('./views/Index.vue') }]
}
]
export { routes }
<template>
<div>11</div>
</template>
<script>
export default {}
</script>
<style lang="scss" scoped>
</style>
import httpRequest from '@/utils/axios'
/**
* 获取课程列表
*/
export function getCourseModule() {
return httpRequest.get('/api/zy/v2/education/mokuai')
}
/**
* 获取课程列表
*/
export function getCourseList() {
return httpRequest.get('/api/zy/v2/education/courses/list')
}
/**
* 获取课程详情
* @param {string} courseId 课程ID
*/
export function getCourse(courseId) {
return httpRequest.get(`/api/zy/v2/education/courses/${courseId}`).then(response => {
// response.chapters = response.chapters.filter(item => {
// item.children = item.children.filter(child => child.type === 2)
// return item.children.length
// })
return response
})
}
/**
* 获取课程知识点
* @param {string} courseId 课程ID
*/
export function getCourseTagList(courseId) {
return httpRequest.get(`/api/zy/v2/education/tag/tree/${courseId}`)
}
/**
* 知识点详情
* @param {string} tagId 知识点ID
*/
export function getCourseTag(tagId) {
return httpRequest.get(`/api/zy/v2/education/tag/${tagId}`)
}
<template>
<el-collapse v-model="activeNames">
<el-collapse-item :title="item.name" :name="item.id" v-for="item in currentList" :key="item.id">
<ul v-if="item.id === '1'">
<li class="meterial-item" v-for="subItem in item.children" :key="subItem.id">
<p>
<a :href="subItem.file_url" target="_blank">{{ subItem.file_name }}</a>
</p>
<i class="el-icon-download" @click="handleDownload(subItem)"></i>
</li>
</ul>
<ul v-else>
<li v-for="subItem in item.children" :key="subItem.id" @click="handleClick(subItem)">
<div class="name">{{ subItem.name }}</div>
<div class="duration">{{ subItem.duration }}</div>
<div class="progress" v-if="showProgress && subItem.type === 2">
{{ progressText(subItem.video_progress) }}
</div>
<div class="buttons" v-if="subItem.type === 9">
<el-button round size="mini" v-if="subItem.status === 100" @click="toExamPage(subItem, 1)">
测试
</el-button>
<el-button round size="mini" v-if="[0, 3].includes(subItem.status)" @click="toExamPage(subItem, 2)">
继续测试
</el-button>
<template v-if="[1, 2].includes(subItem.status)">
<!-- <el-button round size="mini" @click="toExamPage(subItem, 1)">重新测试</el-button> -->
<el-button round size="mini" @click="toExamPage(subItem, 3)">报告</el-button>
</template>
</div>
</li>
</ul>
</el-collapse-item>
</el-collapse>
</template>
<script>
export default {
props: {
courseId: { type: String },
data: { type: Array, required: true, default: () => [] },
showProgress: { type: Boolean, default: false }
},
data() {
return {
activeNames: []
}
},
computed: {
role() {
return this.$store.state.user.role
},
currentList() {
// if (this.role === 2) {
// return this.data.map(item => {
// item.children = item.children.filter(item => item.type === 2)
// return item
// })
// }
return this.data
}
},
methods: {
progressText(value) {
value = parseInt(value)
if (value === 0) {
return '未开始'
}
if (value === 100) {
return '已学完'
}
return `已学${value}%`
},
handleClick(data) {
if (data.type === 2 || data.type === 4) {
this.$router.push({ name: 'viewerCourseChapter', params: { cid: this.courseId, id: data.id } })
}
},
toExamPage(data, type) {
const path = type === 3 ? '/exam/test/result' : '/exam/test/exam'
this.$router.push({
path: path,
query: { course_id: this.courseId, chapter_id: data.id, type, exam_id: data.resource_id }
})
},
handleDownload(item) {
window.location.href = item.file_url
}
}
}
</script>
<style lang="scss" scoped>
.el-collapse {
border: 0;
}
::v-deep .el-collapse-item__header {
font-size: 14px;
font-weight: 600;
color: #222;
}
::v-deep .el-collapse-item__content {
padding-bottom: 10px;
}
li {
display: flex;
padding: 5px 0;
cursor: pointer;
color: #666;
&:hover {
color: #c01540;
}
.name {
flex: 1;
overflow: hidden;
}
.progress {
margin-left: 20px;
color: #999;
}
}
li.meterial-item {
display: flex;
p {
flex: 1;
}
i {
padding: 10px;
font-size: 20px;
}
}
</style>
<!--课程考核-学习进度及成绩-->
<template>
<div class="course-assess-progress">
<!-- 视频 -->
<div class="item">
<div class="item-title">
课程“音视频”观看统计( 累计学习时长:"00:00,完成率:0/24 )
</div>
<div class="table">
<div class="table-row table-row-th">
<div class="table-cell">章节</div>
<div class="table-cell w100">学习时长</div>
<div class="table-cell w100">百分比</div>
</div>
<div class="table-row-group" v-for="(item, index) in videoList" :key="index">
<div class="table-row-group-th">{{ item.title }}</div>
<div class="table-row" v-for="(subItem, index) in item.sections" :key="index">
<div class="table-cell">{{ subItem.title }}</div>
<div class="table-cell w100">{{ formatDuration(subItem.duration) }}</div>
<div class="table-cell w100">{{ subItem.progress }}%</div>
</div>
</div>
<div class="empty-data" v-if="!videoList.length">暂无数据</div>
</div>
</div>
<!-- 作业 -->
<!-- <div class="item">
<div class="item-title">
{{ $t('pages.learn.courseDetail.subjectivequestions') }}
</div>
<div class="table">
<div class="table-row table-row-th">
<div class="table-cell">{{ $t('pages.learn.courseDetail.chapter') }}</div>
<div class="table-cell w100">{{ $t('pages.learn.courseDetail.Submissiontime') }}</div>
<div class="table-cell w100">{{ $t('pages.learn.courseDetail.score') }}</div>
</div>
<div class="table-row-group" v-for="(item, index) in homeWorkList" :key="index">
<div class="table-row-group-th">{{ item.title }}</div>
<div class="table-row" v-for="(subItem, index) in item.sections" :key="index">
<div class="table-cell">{{ subItem.title }}</div>
<div class="table-cell w100">{{ subItem.created_time || '暂无提交' }}</div>
<div class="table-cell w100">{{ showCore(subItem.score) }}</div>
</div>
</div>
<div class="empty-data" v-if="!homeWorkList.length">{{ $t('pages.learn.courseDetail.Nodataavailable') }}</div>
</div>
</div>
<div class="item">
<div class="item-title">{{ $t('pages.learn.courseDetail.Bighomework') }}</div>
<div class="status-text">
{{ $t('pages.learn.courseDetail.Status') }}{{ essay.status || $t('action.courseAction.none') }}
</div>
<div class="status-text" v-if="essay.created_time">
{{ $t('pages.learn.courseDetail.Submissiontime') }}{{ essay.created_time }}
</div>
<div class="status-text">{{ $t('pages.learn.courseDetail.score2') }}{{ showCore(essay.score) }}</div>
</div> -->
</div>
</template>
<script>
export default {
props: { data: { type: Object, default: () => {} } },
computed: {
videoList() {
return this.data.video_evaluation || []
},
homeWorkList() {
return this.data.homework_evaluation || []
},
essay() {
return this.data.essay_evaluation || {}
}
},
methods: {
formatDuration(duration) {
const h = Math.floor(duration / 3600)
const m = Math.floor((duration - h * 3600) / 60)
const s = (duration - h * 3600 - m * 60) % 60
function tenify(a) {
return a >= 10 ? a : '0' + a
}
const to = { h: tenify(h), m: tenify(m), s: tenify(s) }
const format = 'h:m:s'
return format.replace(/h|m|s/g, k => to[k]).replace(/^00:/, '')
},
showCore(value) {
return value !== null ? value : this.$t('action.courseAction.none')
}
}
}
</script>
<style lang="scss">
.course-assess-progress {
.item {
padding: 0.1rem 0 0.2rem;
}
.item + .item {
border-top: 1px solid #c9c9c9;
}
.item-title {
padding: 0.1rem 0;
font-size: 0.16rem;
font-weight: 700;
color: #b49441;
}
.table {
margin: 10px 0;
}
.table-row {
display: flex;
}
.table-row-th {
font-weight: bold;
border-bottom: 1px solid #c9c9c9;
}
.table-row-group {
.table-row {
font-size: 12px;
&:hover {
background-color: #efefef;
}
}
}
.table-row-group-th {
font-weight: bold;
padding: 10px;
}
.table-cell {
padding: 5px 10px;
flex: 1;
&.w100 {
flex: 0 0 120px;
text-align: center;
}
}
.empty-data {
padding: 20px 0;
text-align: center;
}
.status-text {
padding-left: 0.1rem;
font-size: 0.14rem;
color: #000000;
line-height: 1.5;
}
}
</style>
<template>
<div class="teacher">
<div class="teacher-item" v-for="item in data" :key="item.id">
<img :src="item.lecturer_avatar" class="teacher-item-pic" />
<div class="teacher-item-content">
<p class="t1">{{ item.lecturer_name }}</p>
<p class="t2">{{ item.lecturer_education }}</p>
<p class="t2">{{ item.lecturer_title }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
data: { type: Array, default: () => [] }
}
}
</script>
<style lang="scss" scoped>
.teacher-item {
display: flex;
margin-bottom: 10px;
}
.teacher-item-pic {
width: 90px;
height: 110px;
object-fit: cover;
margin-right: 10px;
}
.teacher-item-content {
flex: 1;
.t1 {
font-weight: bold;
}
.t2 {
font-size: 13px;
margin-top: 5px;
color: #707070;
}
}
</style>
<template>
<div class="app-tag" element-loading-text="加载中..." v-loading="!loaded">
<div class="app-tag-main">
<!-- 顶部区域 -->
<div class="app-tag-main-hd">
<router-link :to="`/course/learn/${courseId}`">
<i class="el-icon-arrow-left"></i>
</router-link>
<h1 class="app-tag-main-hd__title">{{ detail.title }}</h1>
<div class="menu" @click="menuVisible = !menuVisible">
<i class="el-icon-s-unfold" v-if="menuVisible"></i>
<i class="el-icon-s-fold" v-else></i>
</div>
</div>
<!-- 主体区域 -->
<div class="app-tag-main-bd">
<router-view :key="$route.fullPath" @ready="onReady" />
</div>
</div>
<div class="app-tag-aside" v-show="menuVisible">
<div class="app-tag-aside-hd">考点列表</div>
<div class="app-tag-aside-bd">
<course-tag :activeId="$route.params.id" :courseId="courseId" @on-click="onTagClick"></course-tag>
</div>
</div>
</div>
</template>
<script>
import CourseTag from '@/components/CourseTag.vue'
export default {
components: { CourseTag },
data() {
return {
menuVisible: true,
loaded: true,
detail: {}
}
},
computed: {
courseId() {
return this.$route.params.courseId
}
},
methods: {
onReady(response) {
this.detail = response
},
onTagClick(data) {
this.$router.push({ name: 'courseTagItem', params: { courseId: this.courseId, id: data.id } })
}
}
}
</script>
<style lang="scss">
.app-tag {
display: flex;
height: 100vh;
overflow: hidden;
}
.app-tag-main {
flex: 1;
display: flex;
flex-direction: column;
}
.app-tag-main-hd {
display: flex;
align-items: center;
background-color: #3f3f3f;
height: 56px;
a {
color: #fff;
padding: 10px;
}
i {
font-size: 24px;
color: #fff;
}
}
.app-tag-main-hd__title {
flex: 1;
font-size: 1.5em;
// text-align: center;
color: #a0a0a0;
}
.app-tag-main-bd {
flex: 1;
height: calc(100vh - 56px);
overflow-y: auto;
}
.app-tag .menu {
width: 24px;
height: 24px;
padding: 12px;
margin-right: 10px;
color: #fff;
text-align: center;
border-radius: 50%;
cursor: pointer;
&:hover {
background-color: rgba(255, 255, 255, 0.08);
}
}
.app-tag-aside {
display: flex;
flex-direction: column;
width: 350px;
min-height: 100vh;
background-color: #232323;
}
.app-tag-aside-hd {
height: 56px;
font-size: 16px;
font-weight: 600;
line-height: 56px;
color: #909090;
text-align: center;
}
.app-tag-aside-bd {
flex: 1;
overflow-x: hidden;
overflow-y: auto;
padding: 0 10px 0 20px;
color: #b0b0b0;
.el-collapse-item__header {
font-size: 15px;
color: #b0b0b0;
background-color: transparent;
border: 0;
}
.el-collapse-item__wrap {
background-color: transparent;
border: 0;
}
.el-collapse-item__content {
font-size: 14px;
}
}
</style>
const routes = [
{
path: '/',
component: () => import('@/components/layout/index.vue'),
children: [
{ path: '/course/learn', component: () => import('./views/Index.vue') },
{ path: '/course/learn/:id', name: 'courseLearnItem', component: () => import('./views/Item.vue') }
]
}
]
export { routes }
<template>
<div>
<div style="margin-bottom: 20px">
<el-button type="primary">最近学习<i class="el-icon-caret-bottom el-icon--right"></i></el-button>
<el-button type="primary">最近更新<i class="el-icon-caret-bottom el-icon--right"></i></el-button>
</div>
<app-container>
<course-list @on-click="handleClick" :searchValue="searchValue" />
</app-container>
</div>
</template>
<script>
import CourseList from '@/components/CourseList.vue'
export default {
components: { CourseList },
data() {
return {
searchValue: ''
}
},
computed: {
role() {
// 1 学生 2 老师
return this.$store.state.user.role
},
title() {
return this.role === 2 ? '课程库' : ''
}
},
methods: {
handleClick(data) {
this.$router.push({ name: 'courseLearnItem', params: { id: data.course_id } })
},
// 搜索
handleSearch() {}
}
}
</script>
<style lang="scss" scoped>
.search {
display: flex;
width: 300px;
.el-button {
margin-left: 10px;
}
}
</style>
<template>
<app-container title="课程详情" element-loading-text="加载中..." v-loading="!loaded">
<div class="course-top" v-if="loaded">
<div class="course-top-hd">
<div class="course-top-hd-left">
<div class="course-top__title">{{ detail.curriculum.curriculum_name }}</div>
<div class="course-top__tips">第x次重修</div>
<div class="course-top__progress">
视频观看进度 <el-progress :percentage="detail.video_progress"></el-progress>
</div>
</div>
<div class="course-top-hd-right">
<el-button type="primary" size="small" @click="onChapterClick(latestVideo)" v-if="latestVideo">
{{ buttonText }}
</el-button>
</div>
</div>
<div class="course-top-bd">
<div class="course-top__pic"><img :src="detail.curriculum.curriculum_picture" /></div>
<div class="course-top__content" v-html="detail.curriculum.curriculum_represent"></div>
</div>
</div>
<div class="course-bottom">
<div class="course-bottom-left">
<el-tabs v-model="tabActive">
<el-tab-pane lazy label="课程内容">
<course-chapter :courseId="courseId" :showProgress="true" :data="detail.chapters"></course-chapter>
</el-tab-pane>
<el-tab-pane lazy label="学习进度">
<course-progress :data="{}"></course-progress>
</el-tab-pane>
</el-tabs>
</div>
<div class="course-bottom-right">
<el-tabs>
<el-tab-pane label="课程讲师">
<course-teacher :data="detail.lecturers"></course-teacher>
</el-tab-pane>
</el-tabs>
</div>
</div>
</app-container>
</template>
<script>
import CourseChapter from '../components/CourseChapter.vue'
import CourseTeacher from '../components/CourseTeacher.vue'
import CourseProgress from '../components/CourseProgress.vue'
import * as api from '@/api/course.js'
export default {
components: { CourseChapter, CourseTeacher, CourseProgress },
metaInfo() {
return {
title: this.detail.course_name || ''
}
},
data() {
return {
tabActive: 0,
loaded: false,
detail: {}
}
},
computed: {
courseId() {
return this.$route.params.id
},
buttonText() {
return this.detail.latest_play ? '继续学习' : '开始学习'
},
// 扁平化章节数据
flatChapters() {
if (this.detail.chapters && this.detail.chapters.length) {
return this.detail.chapters.reduce((result, item) => {
return result.concat(item.children)
}, [])
} else {
return []
}
},
// 最新的视频
latestVideo() {
if (this.detail.latest_play && this.flatChapters.length) {
return this.flatChapters.find(item => item.resource_id === this.detail.latest_play)
} else {
return this.flatChapters.length ? this.flatChapters[0] : null
}
},
examList() {
return this.flatChapters.filter(item => item.type === 9)
}
},
methods: {
// 课程学习
getCourse() {
this.loaded = false
api
.getCourse(this.courseId)
.then(response => {
if (response.files && response.files.length) {
response.chapters.push({ id: '1', name: '补充阅读材料', children: response.files })
}
this.detail = response
})
.finally(() => {
this.loaded = true
})
},
onChapterClick(data) {
this.$router.push({ name: 'viewerCourseChapter', params: { cid: this.courseId, id: data.id } })
},
toExamPage(data, type) {
const path = type === 3 ? '/exam/test/result' : '/exam/test/exam'
this.$router.push({
path: path,
query: { course_id: this.courseId, chapter_id: data.id, type, exam_id: data.resource_id }
})
}
},
beforeMount() {
this.getCourse()
}
}
</script>
<style lang="scss" scoped>
.main-container {
height: 100%;
padding: 30px;
background-color: #fff;
border-radius: 8px;
box-sizing: border-box;
}
.course-top {
padding-bottom: 20px;
}
.course-top-hd {
display: flex;
justify-content: space-between;
padding-bottom: 10px;
}
.course-top-hd-left {
flex: 1;
}
.course-top__title {
font-size: 18px;
font-weight: bold;
line-height: 1;
}
.course-top__progress {
margin-top: 10px;
display: flex;
align-items: center;
.el-progress {
width: 50%;
margin: 0 10px;
}
}
.course-top-bd {
display: flex;
}
.course-top__pic {
width: 160px;
height: 90px;
margin-right: 20px;
border-radius: 2px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.course-top__content {
flex: 1;
line-height: 24px;
overflow: hidden;
}
.course-bottom {
display: flex;
}
.course-bottom-left {
flex: 1;
}
.course-bottom-right {
margin-left: 20px;
width: 300px;
}
.exam-list {
li {
display: flex;
padding: 5px 0;
cursor: pointer;
color: #666;
&:hover {
color: #c01540;
}
.name {
flex: 1;
overflow: hidden;
}
}
}
.course-top__tips{
margin-top: 15px
}
</style>
import httpRequest from '@/utils/axios'
/**
* 获取课程详情
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/
export function getCourse(semesterId, courseId) {
return httpRequest.get(`/api/zy/v2/education/courses/${courseId}`).then(response => {
response.chapters = response.chapters.filter(item => {
item.children = item.children.filter(child => child.type === 2 || child.type === 4)
return item.children.length
})
return response
})
}
/**
* 获取章节资源详情
* @param {string} vid 资源ID
*/
export function getChapterVideo(vid) {
return httpRequest.post(
'/api/zy/v2/education/video-streaming',
{ vid },
{ headers: { 'Content-Type': 'application/json' } }
)
}
/**
* 获取章节资源详情
* @param {string} vid 章节的资源ID
*/
export function getChapterVideoAliyun(vid) {
return httpRequest.post(
'/api/zy/v2/education/aliyun-video-streaming',
{ vid },
{ headers: { 'Content-Type': 'application/json' } }
)
}
/**
* 获取章节视频播放进度
* @param {string} semesterId 学期ID
* @param {string} resourseId 章节的资源ID
* @param {Object} params
*/
export function getChapterVideoProgress(semesterId, resourseId, params) {
return httpRequest.get(`/api/zy/v2/education/video/${resourseId}/device`, { params })
}
/**
* 更新章节视频播放进度
* @param {Object} params
*/
export function updateChapterVideoProgress(params) {
return httpRequest.get('/api/zy/v2/analytics/upload-video', { params })
}
/**
* 获取章节作业
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
* @param {string} resourseId 章节的资源ID
*/
export function getChapterHomework(semesterId, courseId, resourseId) {
return httpRequest.get(`/api/zy/v2/education/homeworks/${courseId}/${resourseId}`)
}
/**
* 获取提交作业截止时间
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
* @param {string} chapterId 章节ID
*/
export function getChapterHomeworkDeadline(semesterId, courseId, chapterId) {
return httpRequest.get(`/api/zy/v2/education/homeworks/${courseId}/${chapterId}/deadline`)
}
/**
* 提交考试
*/
export function sbumitChapterHomework(data) {
return httpRequest.post('/api/zy/v2/education/homeworks', data, {
headers: { 'Content-Type': 'application/json' }
})
}
/**
* 上传文件
*/
export function uploadFile(data) {
return httpRequest.post('/api/zy/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(`/api/zy/v2/education/courses/${courseId}/essay`)
}
/**
* 提交课程大作业
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/
export function updateCourseWork(semesterId, courseId, data) {
return httpRequest.post(`/api/zy/v2/education/courses/${courseId}/essay`, data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
/**
* 获取课程考试试题
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
*/
export function getCourseExam(semesterId, courseId) {
return httpRequest.get(`/api/zy/v2/education/${courseId}/examination`)
}
/**
* 获取课程考试状态
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
* @param {string} examId 试题ID
*/
export function getCourseExamStatus(semesterId, courseId, examId) {
return httpRequest.get(`/api/zy/v2/education/${courseId}/examination/${examId}/status`)
}
/**
* 提交课程考试
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
* @param {string} examId 试题ID
*/
export function submitCourseExam(semesterId, courseId, examId, data) {
return httpRequest.post(`/api/zy/v2/education/${courseId}/examination/${examId}/sheet`, data, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
})
}
/**
* 获取课程考试结果
* @param {string} semesterId 学期ID
* @param {string} courseId 课程ID
* @param {string} examId 试题ID
*/
export function getCourseExamResult(semesterId, courseId, examId, params) {
return httpRequest.get(`/api/zy/v2/education/${courseId}/examination/${examId}/sheet`, { params })
}
<template>
<ul class="chapter-list">
<li class="chapter-item" v-for="item in chapters" :key="item.id">
<h4>{{ item.name }}</h4>
<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) }}</span>
<i class="el-icon" :class="genIconClass(subItem.type)"></i>
</li>
</ul>
</li>
</ul>
</template>
<script>
export default {
props: {
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
},
chapters: { type: Array, default: () => [] },
// 当前选中的章节
active: {
type: Object,
default() {
return {}
}
}
},
data() {
return {}
},
filters: {
showName(name, data) {
if ([5, 8].includes(data.type) && data.live) {
return `${name}(${data.live.start_time})`
}
return name
}
},
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) {
if (data.type === 1) {
return
}
// zoom直播
if (data.type === 8) {
const live = data.live
const hasRecordUrl = live.enable_record && live.record_url
if ([3, 5].includes(live.live_status) && !hasRecordUrl) {
this.$message.error('直播结束')
return
}
window.open(live.record_url || live.join_url)
return
}
// 课程大作业
if (data.id === 'course_work' && !this.data.survey) {
this.$message('请先填写教学评估,然后完成大作业。')
return
}
// 教学评估
if (data.id === 'teach_evaluation') {
const { sid, cid } = this.$route.params
this.$router.push({ name: 'survey', params: { sid, cid } })
return
}
this.$router.push({ name: 'viewerCourseChapter', params: { id: data.id } })
}
}
}
</script>
<style lang="scss" scoped>
/* 章列表样式 */
.chapter-list {
margin: 0;
padding: 0;
line-height: 1.6;
overflow: hidden;
.chapter-item {
h4 {
padding: 10px 22px;
margin: 0;
font-size: 15px;
color: #b0b0b0;
background-color: #2f2f2f;
}
/* 节列表样式 */
.chapter-item-list {
margin: 0;
padding: 0;
line-height: 1.6;
overflow: hidden;
li {
position: relative;
&.is-active {
background: #3c3c3c;
.chapter-item-list__name {
color: #c01540;
}
}
&:hover {
background: #3c3c3c;
}
&:before {
display: block;
content: '';
position: absolute;
left: 13px;
top: 16px;
z-index: 10;
width: 18px;
height: 18px;
background: #5b5b5b;
border: 2px solid #5b5b5b;
border-radius: 50%;
}
&:after {
display: block;
content: '';
position: absolute;
left: 22px;
top: 0;
z-index: 5;
width: 1px;
height: 100px;
background: #616161;
}
}
.chapter-item-list__name {
display: block;
padding: 15px 35px 15px 40px;
font-size: 14px;
color: #909090;
text-decoration: none;
cursor: pointer;
}
}
/* 章节后面小图标的样式 */
.el-icon {
position: absolute;
font-size: 16px;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #a0a0a0;
}
}
}
</style>
<template>
<aside class="course-viewer-aside">
<el-tabs v-model="activeName">
<el-tab-pane label="章节列表" name="0">
<div class="tab-pane">
<aside-chapter :data="data" :chapters="chapters" :active="active"></aside-chapter>
</div>
</el-tab-pane>
<el-tab-pane label="讲义" name="1" v-if="active && active.type === 2">
<div class="tab-pane">
<aside-lecture :ppts="ppts" :pptIndex="pptIndex" v-on="$listeners"></aside-lecture>
</div>
</el-tab-pane>
</el-tabs>
</aside>
</template>
<script>
import AsideChapter from './chapter.vue'
import AsideLecture from './lecture.vue'
export default {
props: {
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
},
// 章节
chapters: { type: Array, default: () => [] },
// 讲义
ppts: { type: Array, default: () => [] },
// 当前选中的章节
active: {
type: Object,
default() {
return {}
}
},
// 当前选择的PPT
pptIndex: { type: Number, default: 0 }
},
components: { AsideChapter, AsideLecture },
data() {
return {
activeName: '0'
}
}
}
</script>
<style lang="scss" scoped>
.course-viewer-aside {
width: 350px;
min-height: 100vh;
background-color: #232323;
}
.tab-pane {
height: calc(100vh - 56px);
overflow-y: auto;
}
::v-deep .el-tabs__header {
margin: 0;
}
::v-deep .el-tabs__nav {
float: none;
display: flex;
}
::v-deep .el-tabs__item {
flex: 1;
height: 56px;
font-size: 16px;
line-height: 56px;
color: #909090;
text-align: center;
&.is-active {
color: #c01540;
}
}
::v-deep .el-tabs__active-bar,
::v-deep .el-tabs__nav-wrap::after {
display: none;
}
</style>
<template>
<ul class="lecture-list">
<li
v-for="(item, index) in ppts"
:key="item.id"
@click="onClick(index)"
:class="{'is-active': index === activeIndex}"
>
<img :src="item.ppt_url" />
</li>
</ul>
</template>
<script>
export default {
props: {
// 当前选择的PPT
pptIndex: { type: Number, default: 0 },
ppts: { type: Array, default: () => [] }
},
data() {
return {
activeIndex: this.pptIndex
}
},
watch: {
pptIndex(index) {
this.activeIndex = index
}
},
methods: {
// 点击PPT
onClick(index) {
this.activeIndex = index
this.$emit('change-ppt', index)
}
}
}
</script>
<style lang="scss" scoped>
.lecture-list {
padding: 0 16px;
li {
padding: 8px 16px;
cursor: pointer;
&.is-active {
background: #888;
}
img {
width: 100%;
}
}
}
</style>
<template>
<div class="course-viewer-content">
<div class="course-viewer-content-hd">
<slot name="header">
<h3 class="course-viewer-content-hd__title">
<slot name="title">{{title}}</slot>
</h3>
<div class="course-viewer-content-hd__aside">
<slot name="header-aside"></slot>
</div>
</slot>
</div>
<div class="course-viewer-content-bd">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'Continaer',
props: { title: String }
}
</script>
<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
}
},
watch: {
value(val) {
if (this.ckEditor && this.ckEditor.getData() !== val) {
this.ckEditor.setData(val)
}
},
disabled(val) {
if (this.ckEditor && this.ckEditor.instanceReady) {
this.ckEditor.setReadOnly(val)
}
}
},
methods: {
createEditor() {
const config = {
height: 400,
uiColor: '#eeeeee',
filebrowserImageUploadUrl: '/api/ck/form/ckeditor-upload',
fileTools_requestHeaders: { tenant: 'sofia' },
// 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'] }
]
}
// if (this.disabled !== null) {
// console.log(this.disabled)
// config.readOnly = this.disabled
// }
const editor = (this.ckEditor = CKEDITOR.replace(this.textareaElementId, config))
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')
}
})
editor.setReadOnly(this.disabled)
})
},
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>
<template>
<div class="upload">
<el-upload action :disabled="disabled" :show-file-list="false" :http-request="httpRequest">
<slot></slot>
<el-button type="text" 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="fileList.length">
<div class="file-list-item" v-for="(item, index) in fileList" :key="index">
<a :href="item.url" :download="item.name" target="_blank">
<i class="el-icon-document"></i>
{{ item.name }}
</a>
<div>
<a href="javascript:;" @click="handleRemove(index)" style="margin-right: 10px" v-if="!disabled">
<el-tooltip effect="dark" content="删除">
<i class="el-icon-delete"></i>
</el-tooltip>
</a>
<a :href="item.url" :download="item.name" target="_blank">
<el-tooltip effect="dark" content="下载">
<i class="el-icon-download"></i>
</el-tooltip>
</a>
</div>
</div>
</div>
</div>
</template>
<script>
import * as api from '../../api'
export default {
name: 'VUpload',
props: {
value: { type: [String, Array] },
disabled: { type: Boolean, default: false }
},
data() {
return {
fileList: []
}
},
watch: {
value: {
immediate: true,
handler(value) {
if (!value) {
return
}
let fileList = []
if (Array.isArray(value)) {
fileList = value.map(item => {
return { name: item.name || item, url: item.url || item }
})
} else {
fileList.push({ name: '附件下载', url: value })
}
this.fileList = fileList
}
}
},
methods: {
httpRequest(xhr) {
api
.uploadFile({ file: xhr.file })
.then(response => {
if (response.success) {
if (Array.isArray(this.value)) {
this.fileList.push({ name: xhr.file.name, url: response.url })
this.$emit('input', this.fileList)
} else {
this.fileList = [response.url]
this.$emit('input', response.url)
}
}
})
.catch(error => {
this.$message.error(error.message)
})
},
handleRemove(index) {
this.fileList.splice(index, 1)
this.$emit('input', Array.isArray(this.value) ? this.fileList : '')
}
}
}
</script>
<style lang="scss" scoped>
.file-list-item {
display: flex;
margin-bottom: 10px;
padding: 0 10px;
justify-content: space-between;
line-height: 30px;
background-color: #fff;
border-radius: 4px;
a {
text-decoration: none;
color: #333;
&:hover {
color: #c01540;
}
}
}
</style>
<template>
<component :is="currentCompoent" :chapter="chapter" v-bind="$attrs" v-on="$listeners" v-if="chapter" :key="pid" />
</template>
<script>
// components
import ChapterPlayer from './player/chapterPlayer.vue' // 章节视频
import ChapterWork from './work/index.vue' // 章节作业
import ChapterExam from './work/chapterExam.vue' // 章节考试
import ChapterRead from './read/chapterRead.vue' // 章节资料
import ChapterLive from './live/chapterLive.vue' // 章节直播
import CourseWork from './work/courseWork.vue' // 课程大作业
import CourseRead from './read/courseRead.vue' // 课程资料
import CourseExam from './work/courseExam.vue' // 课程考试
export default {
name: 'ViewerLayout',
components: {
ChapterPlayer,
ChapterWork,
ChapterRead,
ChapterExam,
ChapterLive,
CourseWork,
CourseRead,
CourseExam
},
props: {
chapter: {
type: Object,
default() {
return {}
}
}
},
computed: {
currentCompoent() {
const componentNames = {
2: 'ChapterPlayer', // 视频
3: 'ChapterWork', // 作业
4: 'ChapterRead', // 资料
5: 'ChapterLive', // CC直播
8: 'ChapterLive', // CC直播
9: 'ChapterExam', // 考试
99: 'CourseWork', // 课程大作业
100: 'CourseRead', // 课程资料
101: 'CourseExam' // 课程考试
}
return this.chapter ? componentNames[this.chapter.type] || '' : ''
},
pid() {
return this.$route.params.id
}
}
}
</script>
<template>
<div style="width: 100%; height: 100%">
<div class="course-viewer-content" v-if="isLiveEnd && !hasRecord">
<div class="empty">直播已结束</div>
</div>
<iframe
:src="iframeUrl"
frameborder="0"
width="100%"
height="100%"
allow="autoplay;geolocation;microphone;camera;midi;encrypted-media;"
v-else
></iframe>
</div>
</template>
<script>
// 章节视频
export default {
name: 'ChapterLive',
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
computed: {
user() {
return this.$store.state.user
},
nickName() {
return this.user.personal_name || '匿名'
},
live() {
const live = this.chapter.live || {}
live.live_status = parseInt(live.live_status)
return live
},
// 是否直播结束
isLiveEnd() {
return [3, 5].includes(this.live.live_status)
},
// 是否有回放
hasRecord() {
// enable_record 0:不启用回放 1:开启回放
return this.live.enable_record === 1 && this.live.record_url
},
iframeUrl() {
if (this.live.type === 5) {
return this.ccUrl
}
if (this.live.type === 8) {
return this.zoomUrl
}
return ''
},
// cc直播
ccUrl() {
const live = this.live
if (this.isLiveEnd && this.hasRecord) {
// 查看回放
return live.record_url.replace(/^http:|^https:/, '')
} else {
// 直播
live.user_name = live.user_name || this.nickName
return `https://view.csslcloud.net/api/view/index?roomid=${live.room_id}&userid=${live.account_id}&autoLogin=true&viewername=${live.user_name}&viewertoken=${live.play_pass}`
}
},
// zoom直播
zoomUrl() {
return this.live.record_url || this.live.join_url
}
}
}
</script>
<style scoped>
.empty {
padding: 100px;
font-size: 30px;
text-align: center;
}
</style>
<template>
<div class="player" v-if="chatperResources">
<div class="player-main">
<div class="player-column" v-show="videoVisible">
<!-- 视频 -->
<video-player
:isSkip="isSkip"
:skipTime="skipTime"
:video="chatperResources.video"
@timeupdate="onTimeupdate"
@ready="onReady"
ref="videoPlayer"
></video-player>
</div>
<div class="player-column" v-if="pptVisible">
<!-- ppt -->
<ppt-player
:index="pptIndex"
:ppts="chatperResources.ppts"
@close="onPPTClose"
@fullscreen="onPPTFullscreen"
@videoSyncTime="onVideoSyncTime"
></ppt-player>
</div>
</div>
<div class="player-footer">
<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="chatperResources.ppts.length">同步显示PPT</em>
<em :class="skipClass" @click="toggleSkip">始终跳过片头</em>
</div>
</div>
</template>
<script>
import { throttle } from 'lodash'
// api
import * as api from '../../api'
// components
import videoPlayer from './videoPlayer.vue'
import pptPlayer from './pptPlayer.vue'
export default {
name: 'ChapterPlayer',
components: { videoPlayer, pptPlayer },
props: {
// 当前章节
chapter: { type: Object },
// 是否是PPT播放跳转
isSeek: { type: Boolean, default: false },
// PPT当前选中的索引
pptIndex: { type: Number, default: 0 }
},
data() {
// 是否跳过片头
const isSkip = window.localStorage.getItem('isSkip') === 'true'
return {
deviceId: 'jjhz92fn0.le2a6c06c9g0.thhg7ekb1f8',
videoVisible: true,
pptVisible: false,
isSkip,
skipTime: 6,
chatperResources: null,
throttled: null,
throttleWait: 5, // 秒
progress: {
cpt: 0, // 当前播放时间
mpt: 0, // 当前播放最大时间
progress: 0, // 进度
pt: 0 // 累计观看时间
},
player: null,
watchedTime: 0,
watchedTimePoint: [] // 视频观看的时间点
}
},
watch: {
pptIndex(index) {
this.isSeek && this.updateVideoCurrentTime(index)
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 视频资源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,
'player-button-ppt': !this.pptVisible,
'player-button-ppt__active': this.pptVisible
}
},
skipClass() {
return {
'player-button': true,
'player-button-skip': !this.isSkip,
'player-button-skip__active': this.isSkip
}
}
},
methods: {
// 同步显示PPT
togglePPTVisible() {
this.videoVisible = true
this.pptVisible = !this.pptVisible
},
// 始终跳过片头
toggleSkip() {
this.isSkip = !this.isSkip
window.localStorage.setItem('isSkip', this.isSkip)
},
// 关闭PPT
onPPTClose() {
this.pptVisible = false
this.videoVisible = true
},
// PPT全屏
onPPTFullscreen(value) {
this.videoVisible = !value
},
// 设置视频时间为当前PPT时间
onVideoSyncTime(time) {
this.player.seek(time)
},
// 播放器ready
onReady(player) {
this.player = player
// 跳转播放进度
if (this.progress.cpt) {
this.player.seek(this.progress.cpt)
}
},
// 当前播放时间更新
onTimeupdate(time) {
time = Math.floor(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)
const durations = this.player.getDuration()
// 更新当前播放时间
this.progress.cpt = time
// 观看的最大点
this.progress.mpt = Math.max(time, this.progress.mpt)
const hasTimePoint = this.watchedTimePoint.includes(this.progress.cpt)
if (!hasTimePoint) {
this.watchedTimePoint.push(this.progress.cpt)
}
// 更新视频观看总时长
this.updateWatchTime(time)
// 更新视频进度,10秒更新一次
if (this.throttled) {
this.throttled(time, durations)
} else {
this.throttled = throttle(this.updateChapterVideoProgress, this.throttleWait * 1000, { leading: false })
}
},
// 更新视频当前播放时间
updateVideoCurrentTime() {
const ppt = this.chatperResources.ppts[this.pptIndex]
ppt && this.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 => {
let { video, audio, ppts } = response
video = video.reduce(
(result, item) => {
if (item.quality === '10') {
result.LD = item.playurl
}
if (item.quality === '20') {
result.SD = item.playurl
}
return result
},
{ LD: '', SD: '' }
)
this.chatperResources = { video, audio, ppts }
Array.isArray(ppts) && this.$emit('pptupdate', ppts)
})
}
},
// 获取章节视频进度
getChapterVideoProgress() {
api
.getChapterVideoProgress(this.sid, this.resourceId, {
device_id: this.deviceId
})
.then(response => {
this.progress = response
// 跳转播放进度
if (this.player && response.cpt) {
this.player.seek(response.cpt)
}
})
},
// 更新章节视频进度
updateChapterVideoProgress(time, durations) {
// 登录用户信息
const user = this.$store.state.user
const info = user.role === 2 ? user.staff_info : user.student_info
const params = {
sid: info.id,
uid: user.id,
d: this.deviceId,
i: this.deviceId,
c: this.cid, // 课程ID
s: this.sid, // 学期ID
v: this.resourceId, // 视频资源ID
_p: this.progress.pt, // 累计时间
_m: this.progress.mpt, // 当前播放最大时间
_c: this.progress.cpt, // 当前播放位置
ps: this.watchedTimePoint.join(',') // 播放时,统计帧
}
api.updateChapterVideoProgress(params)
// 清空已经上传过的观看时间点
this.watchedTimePoint = []
},
// 更新观看总时长
updateWatchTime(time) {
if (time === this.watchedTime) {
return
}
this.watchedTime = time
// 增加跳过片头时间
if (this.isSkip && !this.progress.pt) {
this.progress.pt = this.skipTime + 20
}
// 默认增加时间
this.progress.pt = this.progress.pt || 20
this.progress.pt++
}
},
beforeMount() {
// 获取视频
this.getChapterVideo()
// 获取视频进度
this.getChapterVideoProgress()
},
beforeDestroy() {
this.throttled && this.throttled.cancel()
}
}
</script>
<style lang="scss" scoped>
.player {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background-color: #3f3f3f;
}
.player-main {
display: flex;
flex: 1;
overflow: hidden;
}
.player-column {
flex: 1;
height: 100%;
}
.player-footer {
display: flex;
align-items: center;
height: 54px;
padding: 0 20px;
font-size: 14px;
color: #a0a0a0;
a {
color: #a0a0a0;
text-decoration: none;
}
em {
margin-right: 40px;
cursor: pointer;
}
}
.player-button {
display: inline-block;
color: #a0a0a0;
padding-left: 25px;
font-size: 14px;
line-height: 18px;
margin: 0 20px;
background: url(../../assets/play-icons.png) no-repeat 0 0;
cursor: pointer;
}
.player-button-download {
background-position: 0 -240px;
}
.player-button-ppt {
background-position: 0 -240px;
}
.player-button-ppt__active {
background-position: 0 -280px;
color: #b19241;
}
.player-button-skip {
background-position: 0 -160px;
}
.player-button-skip__active {
background-position: 0 -200px;
color: #b19241;
}
</style>
<template>
<div class="ppt-player">
<template v-if="ppts.length">
<div class="ppt-player-preview">
<img :src="pptUrl" v-if="pptUrl" />
</div>
<div class="ppt-player-controls">
<div class="ppt-player-controls__page">
<template v-if="currentIndex >= 0">
<i class="el-icon-arrow-left" @click="prev"></i>
</template>
<template v-if="currentIndex + 1 < ppts.length">
<i class="el-icon-arrow-right" @click="next"></i>
</template>
</div>
<div class="ppt-player-controls__pages">
<span class="is-active">{{currentIndex + 1}}</span>
/
<span>{{ppts.length}}</span>
</div>
<div class="ppt-player-controls__tools">
<el-tooltip content="PPT同步视频播放">
<i :class="['el-icon-self-xuexiao', (isSync ? 'active' : '')]" @click="onToggleSync"></i>
</el-tooltip>
<el-tooltip content="放大PPT">
<i class="el-icon-self-quanping" @click="fullscreen"></i>
</el-tooltip>
<el-tooltip content="切换视频到当前PPT页">
<i class="el-icon-self-shipin" @click="setVideoTime"></i>
</el-tooltip>
<el-tooltip content="关闭PPT">
<i class="el-icon-self-guanbi" @click="$emit('close')"></i>
</el-tooltip>
</div>
</div>
</template>
</div>
</template>
<script>
export default {
name: 'ppt-player',
props: {
ppts: { type: Array },
index: { type: Number, default: 0 }
},
data() {
return {
currentIndex: this.index,
isSync: true,
isFullscreen: false
}
},
watch: {
index: {
handler(value) {
if (this.isSync) {
this.currentIndex = value
}
}
}
},
computed: {
pptUrl() {
return this.ppts[this.currentIndex]
? this.ppts[this.currentIndex].ppt_url
: ''
}
},
methods: {
gotoIndex(index) {
this.currentIndex = index
},
getIndex(index) {
return Math.min(this.ppts.length - 1, Math.max(0, index))
},
prev() {
this.currentIndex = this.getIndex(this.currentIndex - 1)
this.isSync = false
},
next(e) {
this.currentIndex = this.getIndex(this.currentIndex + 1)
this.isSync = false
},
onToggleSync(e) {
this.isSync = !this.isSync
},
setVideoTime(e) {
this.isSync = true
this.$emit('videoSyncTime', this.ppts[this.currentIndex].ppt_point)
},
// 全屏
fullscreen() {
this.isFullscreen = !this.isFullscreen
this.$emit('fullscreen', this.isFullscreen)
}
}
}
</script>
<style lang="scss" scoped>
.ppt-player {
position: relative;
width: 100%;
height: 100%;
background-color: #000;
}
.ppt-player-preview {
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: contain;
}
}
.ppt-player-controls {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 44px;
line-height: 44px;
padding: 0 14px;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
}
.ppt-player-controls__page {
width: 90px;
color: #fff;
i {
padding: 0 10px;
font-size: 18px;
cursor: pointer;
}
}
.ppt-player-controls__pages {
flex: 1;
color: #fff;
text-align: center;
}
.ppt-player-controls__pages .is-active {
color: #d29f29;
}
.ppt-player-controls__tools {
float: right;
}
.ppt-player-controls__tools i {
color: #fff;
margin: 0 10px;
cursor: pointer;
}
.ppt-player-controls__tools i.active,
.ppt-player-controls__tools i:hover {
color: #d29f29;
}
.ppt-player-controls__tools .icon-rotate {
font-size: 1.125em;
}
</style>
<template>
<div class="video-player" id="player"></div>
</template>
<script>
export default {
name: 'VideoPlayer',
props: {
isSkip: Boolean,
video: Object,
autoplay: { type: Boolean, default: true }
},
data() {
return { player: null }
},
methods: {
createPlayer() {
const _this = this
const { FD, LD, SD } = this.video
/*
"OD" : "原画"
"FD" : "流畅"
"LD" : "标清"
"SD" : "高清"
"HD" : "超清"
"2K" : "2K"
"4K" : "4K"
*/
this.player = new window.Aliplayer(
{
id: 'player',
source: JSON.stringify({ SD, LD, FD }),
width: '100%',
height: '100%',
autoplay: this.autoplay,
isLive: false,
controlBarVisibility: 'always',
useHlsPluginForSafari: true
},
function(player) {
player.on('ready', function() {
// 跳过片头
_this.isSkip && player.seek(6)
_this.$emit('ready', player)
})
player.on('timeupdate', function(event) {
_this.$emit('timeupdate', player.getCurrentTime())
})
player.on('error', function(event) {
console.log(event)
})
}
)
}
},
mounted() {
this.createPlayer()
},
beforeDestroy() {
this.player && this.player.dispose()
}
}
</script>
<style lang="scss" scoped>
.video-player {
width: 100%;
height: 100%;
}
</style>
<template>
<container :title="chapter.name">
<file-list :files="files"></file-list>
</container>
</template>
<script>
// components
import Container from '../common/container.vue'
import FileList from './fileList.vue'
// 章节阅读资料
export default {
name: 'ChapterRead',
components: { Container, FileList },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
computed: {
files() {
return this.chapter.reading ? this.chapter.reading.reading_attachment : []
}
}
}
</script>
<template>
<container :title="chapter.name">
<file-list :files="files"></file-list>
</container>
</template>
<script>
// components
import Container from '../common/container.vue'
import FileList from './fileList.vue'
// 课程阅读资料
export default {
name: 'CourseRead',
components: { Container, FileList },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
computed: {
files() {
return this.data.files || []
}
}
}
</script>
<template>
<div>
<ul class="file-list" v-if="currentFiles.length">
<li class="file-list-item" v-for="file in currentFiles" :key="file.id">
<a :href="file.file_url" target="_blank">
<i class="el-icon-document"></i>
<div class="file-list-item__inner" v-html="file.file_name"></div>
</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>
</template>
<script>
export default {
name: 'FilePanel',
props: {
// 标题
title: { type: String, default: '课程资料' },
// 文件列表
files: { type: Array, default: () => [] }
},
computed: {
currentFiles() {
return this.files.map(file => {
file.file_url = file.file_url || file.url
return file
})
}
}
}
</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 {
display: flex;
align-items: center;
text-decoration: none;
color: #333;
&:hover {
color: #c01540;
}
::v-deep * {
margin: 0;
padding: 0;
}
}
}
.empty {
font-size: 18px;
line-height: 80px;
background-color: #fff;
text-align: center;
border-radius: 40px;
}
.file-list-item__inner {
margin: 0 10px !important;
}
</style>
<template>
<container :title="detail.paper_title" v-loading="loading">
<template v-slot:header-aside v-if="isExamComplete">分数:{{exam.score.total}}</template>
<div class="exam">
<template v-if="isSubmited && !isExamComplete">
<div class="no-exam">试卷批改中,请耐心等待</div>
</template>
<template v-else>
<!-- 考试期间,未开始考试 -->
<div class="exam-welcome" v-if="!isStartExam">
<div v-if="detail.paper_deadline">考试截止时间:{{detail.paper_deadline}}</div>
<el-button
type="primary"
:disabled="!isExamTime"
@click="onStartExam"
>{{startExamButtonText}}</el-button>
</div>
<!-- 考试试题 -->
<div class="exam-form" v-if="isStartExam">
<el-form :disabled="isSubmited">
<template v-for="items in questions">
<exam-item
v-for="(item, index) in items"
:index="index"
:type="item.type"
:data="item"
:value="item.formModel"
:disabled="isSubmited"
:key="item.id"
></exam-item>
</template>
<div class="exam-buttons">
<el-tooltip effect="dark" content="提交之后就不能修改了哦" placement="right">
<el-button type="primary" :loading="submitLoading" @click="onSubmit">{{submitText}}</el-button>
</el-tooltip>
</div>
</el-form>
</div>
</template>
</div>
</container>
</template>
<script>
import { Base64 } from 'js-base64'
// components
import Container from '../common/container.vue'
import ExamItem from './examItem.vue'
// api
import * as api from '../../api'
// 章节测试题
export default {
name: 'ChapterExam',
components: { Container, ExamItem },
props: {
// 当前选中的章节
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
loading: false,
detail: {},
questions: [],
messageInstance: null,
exam: {},
isStartExam: false, // 是否开始考试
autoSubmitTimer: null, // 自动提交定时器
submitLoading: false
}
},
watch: {
chapter: {
immediate: true,
handler(data) {
this.detail = data.paper
this.questions = data.paper
? this.genQuestions(data.paper.examination)
: []
}
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 当前页面的ID
pid() {
return this.$route.params.id
},
// 是否是考试时间
isExamTime() {
if (!this.detail.paper_deadline) {
return true
}
// 大于开始时间,小于结束时间
const endTime = +new Date(this.exam.paper_deadline)
const currentTime = new Date().getTime()
return currentTime < endTime
},
// 考试按钮
startExamButtonText() {
return this.isExamTime ? '开始考试' : '考试结束'
},
// 考试完成
isExamComplete() {
// 考试完成,批改完成并且公布成绩
return this.exam.is_published === 1 && this.exam.type === 2
},
// 是否提交
isSubmited() {
return this.exam.type === 1 || this.exam.type === 2
},
// 提交按钮文本
submitText() {
return this.isSubmited ? '已提交' : '提交'
}
},
methods: {
// 开始考试
onStartExam() {
this.isStartExam = true
// 自动提交答题
this.autoSubmit()
},
// 组装问题数据
genQuestions(list) {
if (!list) {
return []
}
return list.map(data => {
let { radioList, checkboxList, shortAnswerList } = data
// 单选
radioList = radioList.map(item => {
const temp = {
type: 1,
formModel: { id: item.id, user_answer: item.user_answer || '' }
}
return Object.assign({}, item, temp)
})
// 多选
checkboxList = checkboxList.map(item => {
const temp = {
type: 2,
formModel: { id: item.id, user_answer: item.user_answer || [] }
}
return Object.assign({}, item, temp)
})
// 问答
shortAnswerList = shortAnswerList.map(item => {
const temp = {
type: 3,
formModel: {
id: item.id,
user_answer: item.user_answer
? Base64.decode(item.user_answer.replace(/ /gi, '+'))
: '',
attachments: item.attachments || []
}
}
return Object.assign({}, item, temp)
})
return [...radioList, ...checkboxList, ...shortAnswerList]
})
},
// 获取考试结果
getExamResult() {
api
.getCourseExamResult(this.sid, this.cid, this.pid, { paper_type: 0 })
.then(response => {
// 设置问题列表数据
if (response.code !== 8001) {
this.isStartExam = true
this.exam = response
this.questions = this.genQuestions(response.sheet)
// 自动提交
if (this.isStartExam && !this.isSubmited && !this.isExamComplete) {
this.autoSubmit()
}
}
})
},
// 提交校验
checkSubmit() {
for (let i = 0; i < this.questions.length; i++) {
const questions = this.questions[i]
for (let k = 0; k < questions.length; k++) {
const value = questions[k].formModel.user_answer
if (Array.isArray(value) ? !value.length : !value) {
return false
}
}
}
return true
},
// 提交
onSubmit() {
// 校验
if (!this.checkSubmit()) {
this.messageInstance && this.messageInstance.close()
this.messageInstance = this.$message.error('还有题目未做,不能提交')
return
}
// 提交的答案数据
const answers = this.handleSubmitData()
// 提交参数
const params = { answers: JSON.stringify(answers), type: 1 }
// 请求接口
this.submitLoading = true
this.handleSubmitRequest(params)
},
// 自动提交
autoSubmit() {
// 10秒提交一次
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
this.autoSubmitTimer = setInterval(() => {
// 提交的答案数据
const answers = this.handleSubmitData()
const params = { answers: JSON.stringify(answers), type: 0 }
// 请求接口
this.handleSubmitRequest(params)
}, 3000)
},
// 处理请求接口答案数据
handleSubmitData() {
return this.questions.map(questions => {
return questions.reduce(
(result, item) => {
// 单选题
if (item.type === 1) {
result.radioList.push(item.formModel)
}
// 多选题
if (item.type === 2) {
result.checkboxList.push(item.formModel)
}
// 简答题
if (item.type === 3) {
const formModel = Object.assign({}, item.formModel, {
user_answer: Base64.encode(item.formModel.user_answer)
})
result.shortAnswerList.push(formModel)
}
return result
},
{ radioList: [], checkboxList: [], shortAnswerList: [] }
)
})
},
// 请求提交接口
handleSubmitRequest(params) {
params.paper_type = 0
api
.submitCourseExam(this.sid, this.cid, this.pid, params)
.then(response => {
if (params.type === 0) {
console.log('暂存成功')
return
}
if (response.code === 200) {
this.$message.success('考试答卷提交成功')
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
this.getExamResult()
} else {
this.$message.error(response.data.error)
}
})
.catch(error => {
this.$message.error(error.message)
})
.finally(() => {
this.submitLoading = false
})
}
},
beforeMount() {
// 获取考试结果
this.getExamResult()
},
destroyed() {
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
}
}
</script>
<style lang="scss" scoped>
.exam-buttons {
padding: 40px 0;
text-align: center;
.el-button {
width: 240px;
margin: 40px auto;
}
}
.no-exam {
padding: 100px;
font-size: 30px;
text-align: center;
}
.exam-welcome {
padding: 40px;
line-height: 30px;
text-align: center;
::v-deep .el-button {
margin-top: 30px;
}
}
</style>
<template>
<container :title="chapter.name" v-loading="loading">
<template v-slot:header-aside v-if="isSubmited">正确率:{{ detail.score }}%</template>
<div class="exam">
<div class="exam-form">
<el-form :disabled="isSubmited">
<exam-item
v-for="(item, index) in questions"
:index="index"
:type="item.question_type"
:data="item"
:value="item.formModel"
:disabled="isSubmited"
:key="item.id"
></exam-item>
<div class="exam-buttons">
<el-tooltip effect="dark" content="提交之后就不能修改了哦" placement="right">
<el-button type="primary" :loading="submitLoading" @click="onSubmit">{{ submitText }}</el-button>
</el-tooltip>
</div>
</el-form>
</div>
</div>
</container>
</template>
<script>
// libs
import { shuffle } from 'lodash'
// components
import Container from '../common/container.vue'
import ExamItem from './examItem.vue'
// api
import * as api from '../../api'
// 章节测试题
export default {
name: 'ChapterTest',
components: { Container, ExamItem },
props: {
// 当前选中的章节
chapter: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
loading: false,
detail: null,
questions: [], // 问题列表
startTime: new Date().getTime(), // 进入时间
messageInstance: null,
submitLoading: false
}
},
watch: {
chapter: {
immediate: true,
handler(data) {
this.questions = data.homework ? this.genQuenstions(data.homework.questions) : []
}
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 当前页面的ID
pid() {
return this.$route.params.id
},
// 资源ID
resourceId() {
return this.chapter.resource_id
},
// 打乱顺序的问题列表
unorderedQuestions() {
const ids = this.questions.map(item => item.id)
const sortIds = shuffle(ids)
return sortIds.map(id => this.questions.find(item => item.id === id))
},
// 是否提交
isSubmited() {
return this.detail ? !!this.detail.work_contents : false
},
// 提交按钮文本
submitText() {
return this.isSubmited ? '已提交' : '提交'
}
},
methods: {
// 获取测试答题详情
getDetail() {
this.loading = true
api
.getChapterHomework(this.sid, this.cid, this.resourceId)
.then(response => {
this.detail = Array.isArray(response) ? null : response
if (this.detail) {
const parseAnswers = JSON.parse(this.detail.work_contents)
// 设置答案
this.questions = this.questions.map(item => {
const found = parseAnswers.find(answer => answer.question_id === item.id)
if (found) {
const selectedIds = found.options.reduce((result, item) => {
item.selected && result.push(item.id)
return result
}, [])
item.user_answer = item.question_type === 2 ? selectedIds : selectedIds[0]
}
return item
})
this.questions = this.genQuenstions(this.questions)
}
})
.finally(() => {
this.loading = false
})
},
// 组装问题数据
genQuenstions(list) {
if (!list) {
return []
}
return list.map(item => {
let temp = null
if (item.question_type === 1) {
// 单选
temp = {
formModel: { id: item.id, user_answer: item.user_answer || '' }
}
} else if (item.question_type === 2) {
// 多选
temp = {
formModel: { id: item.id, user_answer: item.user_answer || [] }
}
} else if (item.question_type === 3) {
// 简答
temp = {
formModel: {
id: item.id,
user_answer: item.user_answer ? Base64.decode(item.user_answer) : '',
attachments: item.attachments || ''
}
}
}
return Object.assign(
{},
item,
{
content: item.question_content,
options: item.question_options ? JSON.parse(item.question_options) : []
},
temp
)
})
},
// 提交校验
checkSubmit() {
const quenstions = this.questions
for (let i = 0; i < quenstions.length; i++) {
const value = quenstions[i].formModel.user_answer
if (Array.isArray(value) ? !value.length : !value) {
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 => {
// 设置提交选中状态
let isCorrect = true
const options = item.options.map(option => {
// 选择的项
const answers = item.formModel.user_answer
// 是否选中该项
const selected = Array.isArray(answers) ? answers.includes(option.id) : option.id === answers
// 是否选择正确
if (option.checked !== selected && isCorrect) {
isCorrect = false
}
return {
id: option.id,
checked: option.checked,
option: option.option,
selected
}
})
return {
question_id: item.id,
is_correct: isCorrect ? 1 : 0,
options
}
})
return result
},
// 请求提交接口
handleSubmitRequest(params) {
this.submitLoading = true
api
.sbumitChapterHomework(params)
.then(response => {
if (response.status) {
this.getDetail()
} else {
this.$message.error(response.data.error)
}
})
.catch(error => {
this.$message.error(error.message)
})
.finally(() => {
this.submitLoading = false
})
}
},
beforeMount() {
this.getDetail()
}
}
</script>
<style lang="scss" scoped>
.exam-buttons {
padding: 40px 0;
text-align: center;
.el-button {
width: 240px;
margin: 40px auto;
}
}
</style>
<template>
<container :title="chapter.name" v-loading="loading">
<div class="exam-form">
<el-form :disabled="disabled || !isWorkTime">
<exam-item
v-for="(item, index) in questions"
:index="index"
:type="item.question_type"
:data="item"
:value="item.formModel"
:disabled="disabled || !isWorkTime"
:key="item.id"
></exam-item>
</el-form>
</div>
<p style="color:red;" v-if="deadline">请于截止日期 {{ deadline }} 前提交</p>
<!-- 驳回状态 -->
<template v-if="detail && detail.status === 1">
<div class="work-bottom">
<div class="info">
<div class="paper-check">
<h4>作业被驳回,点击“重新编辑”按钮重新编辑内容再次提交</h4>
<div class="paper-check-item">
<b>驳回时间:</b>
{{ detail.checker_time }}
</div>
<div class="paper-check-item">
<b>驳回说明:</b>
<div class="edit_html" v-html="detail.check_comments"></div>
</div>
</div>
</div>
</div>
<div class="buttons">
<el-button type="primary" @click="onReEdit" :disabled="!isWorkTime">重新编辑</el-button>
</div>
</template>
<!-- 正常状态 -->
<template v-else>
<div class="work-bottom" v-if="detail">
<div class="info">
<template v-if="isRevised">
<div class="paper-check">
<p>批改时间:{{ detail.checker_time }}</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.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" :disabled="disabled || !isWorkTime" :loading="submitLoading" @click="onSubmit">{{
submitText
}}</el-button>
</el-tooltip>
</div>
</template>
</container>
</template>
<script>
import { Base64 } from 'js-base64'
// componets
import Container from '../common/container.vue'
import ExamItem from './examItem.vue'
// api
import * as api from '../../api'
// 章节作业
export default {
name: 'ChapterWork',
components: { Container, ExamItem },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
loading: false,
detail: null,
questions: [], // 问题列表
startTime: new Date().getTime(), // 进入时间
messageInstance: null,
deadline: '', // 截止时间
disabled: false,
submitLoading: false
}
},
watch: {
chapter: {
immediate: true,
handler(data) {
this.questions = data.homework ? this.genQuenstions(data.homework.questions) : []
}
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 当前页面的ID
pid() {
return this.$route.params.id
},
// 资源ID
resourceId() {
return this.chapter.resource_id
},
// 是否批改
isRevised() {
return this.detail ? this.detail.status === 0 : false
},
// 提交按钮文本
submitText() {
return this.isRevised ? '已批改' : '提交'
},
// 是否是提交作业时间
isWorkTime() {
if (!this.deadline) {
return true
}
// 大于开始时间,小于结束时间
const endTime = +new Date(this.deadline)
const currentTime = new Date().getTime()
return currentTime < endTime
}
},
methods: {
// 获取作业截止时间
getDeadline() {
api.getChapterHomeworkDeadline(this.sid, this.cid, this.pid).then(response => {
this.deadline = response.dead_line
})
},
// 获取详情
getDetail() {
this.loading = true
api
.getChapterHomework(this.sid, this.cid, this.resourceId)
.then(response => {
this.detail = Array.isArray(response) ? null : response
if (this.detail) {
// -1未处理 0已处理 1驳回
this.disabled = [0, 1].includes(this.detail.status)
const parseAnswers = JSON.parse(this.detail.work_contents)
// 设置答案
this.questions = this.questions.map(item => {
const found = parseAnswers.find(answer => answer.question_id === item.id)
if (found) {
item.user_answer = found.descreption
item.attachments = found.file_url
}
return item
})
this.questions = this.genQuenstions(this.questions)
}
})
.finally(() => {
this.loading = false
})
},
// 组装问题数据
genQuenstions(list) {
if (!list) {
return []
}
return list.map(item => {
let temp = null
if (item.question_type === 1) {
// 单选
temp = {
formModel: { id: item.id, user_answer: item.user_answer || '' }
}
} else if (item.question_type === 2) {
// 多选
temp = {
formModel: { id: item.id, user_answer: item.user_answer || [] }
}
} else if (item.question_type === 3) {
// 简答
temp = {
formModel: {
id: item.id,
user_answer: item.user_answer ? Base64.decode(item.user_answer) : '',
attachments: item.attachments || ''
}
}
}
return Object.assign(
{},
item,
{
content: item.question_content,
options: item.question_options ? JSON.parse(item.question_options) : []
},
temp
)
})
},
// 提交校验
checkSubmit() {
const quenstions = this.questions
for (let i = 0; i < quenstions.length; i++) {
const value = quenstions[i].formModel.user_answer
if (Array.isArray(value) ? !value.length : !value) {
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 answers = this.questions.map(item => {
return {
question_id: item.id,
descreption:
item.question_type === 3 ? Base64.encode(item.formModel.user_answer) : item.formModel.user_answer,
file_url: item.formModel.attachments,
is_encoded: 1
}
})
// 提交参数
const params = {
semester_id: this.sid,
course_id: this.cid,
chapter_id: this.pid,
work_id: this.resourceId,
work_contents: JSON.stringify(answers),
duration
}
// 请求接口
this.handleSubmitRequest(params)
},
// 请求提交接口
handleSubmitRequest(params) {
this.submitLoading = true
api
.sbumitChapterHomework(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)
})
.finally(() => {
this.submitLoading = false
})
},
// 重新编辑
onReEdit() {
this.disabled = false
this.detail.status = -1
}
},
beforeMount() {
this.getDetail()
this.getDeadline()
}
}
</script>
<style lang="scss" scoped>
.work-bottom {
margin-top: 20px;
.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;
h4 {
margin: 0 0 10px;
}
}
.paper-check-item {
display: flex;
b {
white-space: nowrap;
}
}
</style>
<template>
<container :title="exam.title" v-loading="!loaded">
<template v-slot:header-aside>
<template v-if="isCompleted">分数:{{ exam.score.total }}</template>
<template v-else>考试时间:{{ status.start_time }} ~ {{ status.terminate_time }}</template>
</template>
<div class="exam">
<template v-if="status.examination_status === '00'">
<div class="no-exam">暂无考试</div>
</template>
<template v-else-if="isSubmited && !isCompleted && !isMultipleExams">
<div class="no-exam">试卷批改中,请耐心等待</div>
</template>
<template v-else>
<!-- 考试试题 -->
<div class="exam-form" v-if="loaded">
<el-form :disabled="!canEditable">
<template v-for="items in questions">
<exam-item
v-for="(item, index) in items"
:index="index"
:type="item.type"
:data="item"
:value="item.formModel"
:disabled="!canEditable"
:showResult="isCompleted"
:key="item.id"
></exam-item>
</template>
</el-form>
<div class="exam-buttons">
<!-- 允许多次提交 -->
<template v-if="isMultipleExams">
<el-button type="primary" @click="handlePrev" v-if="hasPrev">上一套试卷</el-button>
<el-button type="primary" @click="handleNext" v-if="hasNext">下一套试卷</el-button>
<el-button type="primary" @click="handleNewExam" v-if="hasResubmit">再考一次</el-button>
</template>
<template v-if="!(isSubmited && isMultipleExams)">
<el-tooltip effect="dark" content="提交之后就不能修改了哦" placement="right">
<el-button type="primary" :disabled="!canEditable" :loading="submitLoading" @click="onSubmit">{{
submitText
}}</el-button>
</el-tooltip>
</template>
</div>
</div>
</template>
</div>
</container>
</template>
<script>
import { Base64 } from 'js-base64'
// components
import Container from '../common/container.vue'
import ExamItem from './examItem.vue'
// api
import * as api from '../../api'
// 章节测试题
export default {
name: 'CourseExam',
components: { Container, ExamItem },
props: {
// 当前选中的章节
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
loaded: false,
detail: {},
status: {},
questions: [],
messageInstance: null,
exam: {},
autoSubmitTimer: null, // 自动提交定时器
checkStatusTimer: null, // 考试状态定时器
submitLoading: false,
isMultipleExams: false, // 是否可以多次考试
maxExams: 3, // 最多考试几次
examCount: this.data.exist_examination.length || 0 // 试卷数量
}
},
watch: {
offset: {
immediate: true,
handler() {
this.init()
}
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 当前页面的ID
pid() {
return this.data.course_examination
},
// 是否是考试时间
isExamTime() {
// 大于开始时间,小于结束时间
return this.status.examination_status === '20'
},
// 是否提交
isSubmited() {
return this.exam.type === 1 || this.exam.type === 2
},
// 考试完成
isCompleted() {
// 考试完成,批改完成并且公布成绩
return this.exam.is_published === 1 && this.exam.type === 2
},
// 可以编辑
canEditable() {
return !this.isSubmited && this.isExamTime
},
// 提交按钮文本
submitText() {
return this.isSubmited ? '已提交' : '提交'
},
// 试卷页码
offset() {
const { query } = this.$route
return parseInt(query.offset) || 0
},
// 是否显示上一套试题
hasPrev() {
return !!this.offset
},
// 是否显示下一套试题
hasNext() {
return this.offset < this.examCount - 1
},
// 是否显示再考一次
hasResubmit() {
if (this.examList.length >= this.maxExams) {
return false
}
// 判断状态是否还有未提交的试题
for (const exam of this.examList) {
if (!['1', '2'].includes(exam.status)) {
return false
}
}
return true
// return this.isSubmited && this.isExamTime && this.examCount < this.maxExams
},
// 已存在的试题列表
examList() {
return this.data.exist_examination
}
},
methods: {
// 初始化
async init() {
this.clearTimer()
// 自动获取考试状态
await this.autoCheckExamStatus()
// 获取试题
this.getExam()
},
// 获取考试状态
async getExamStatus() {
await api.getCourseExamStatus(this.sid, this.cid, this.pid).then(response => {
this.status = response
if (this.isSubmited || response.examination_status === '90') {
this.checkStatusTimer && clearInterval(this.checkStatusTimer)
}
})
},
// 自动获取考试状态
async autoCheckExamStatus() {
// 获取试题状态
await this.getExamStatus()
this.checkStatusTimer && clearInterval(this.checkStatusTimer)
this.checkStatusTimer = setInterval(this.getExamStatus, 5000)
},
// 获取试题
getExam() {
this.loaded = false
api
.getCourseExamResult(this.sid, this.cid, this.pid, { offset: this.offset })
.then(response => {
this.exam = response
this.questions = this.genQuestions(response.sheet)
// 自动提交
this.canEditable && this.autoSubmit()
// 更新菜单
this.isMultipleExams && this.$emit('update')
})
.finally(() => {
this.loaded = true
})
},
// 组装问题数据
genQuestions(list) {
if (!list) {
return []
}
return list.map(data => {
let { radioList, checkboxList, shortAnswerList } = data
// 单选
radioList = radioList.map(item => {
const temp = {
type: 1,
formModel: { id: item.id, user_answer: item.user_answer || '' }
}
return Object.assign({}, item, temp)
})
// 多选
checkboxList = checkboxList.map(item => {
const temp = {
type: 2,
formModel: { id: item.id, user_answer: item.user_answer || [] }
}
return Object.assign({}, item, temp)
})
// 问答
shortAnswerList = shortAnswerList.map(item => {
const temp = {
type: 3,
formModel: {
id: item.id,
user_answer: item.user_answer ? Base64.decode(item.user_answer.replace(/ /gi, '+')) : '',
attachments: item.attachments || []
}
}
return Object.assign({}, item, temp)
})
return [...radioList, ...checkboxList, ...shortAnswerList]
})
},
// 提交校验
checkSubmit() {
for (let i = 0; i < this.questions.length; i++) {
const questions = this.questions[i]
for (let k = 0; k < questions.length; k++) {
const value = questions[k].formModel.user_answer
if (Array.isArray(value) ? !value.length : !value) {
return false
}
}
}
return true
},
// 提交
onSubmit() {
// 校验
if (!this.checkSubmit()) {
this.messageInstance && this.messageInstance.close()
this.messageInstance = this.$message.error('还有题目未做,不能提交')
return
}
// 提交的答案数据
const answers = this.handleSubmitData()
// 提交参数
const params = { answers: JSON.stringify(answers), type: 1 }
// 请求接口
this.submitLoading = true
this.handleSubmitRequest(params)
},
// 自动提交
autoSubmit() {
// 10秒提交一次
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
this.autoSubmitTimer = setInterval(() => {
// 提交的答案数据
const answers = this.handleSubmitData()
const params = { answers: JSON.stringify(answers), type: 0 }
// 请求接口
this.handleSubmitRequest(params)
}, 3000)
},
// 处理请求接口答案数据
handleSubmitData() {
return this.questions.map(questions => {
return questions.reduce(
(result, item) => {
// 单选题
if (item.type === 1) {
result.radioList.push(item.formModel)
}
// 多选题
if (item.type === 2) {
result.checkboxList.push(item.formModel)
}
// 简答题
if (item.type === 3) {
const formModel = Object.assign({}, item.formModel, {
user_answer: Base64.encode(item.formModel.user_answer)
})
result.shortAnswerList.push(formModel)
}
return result
},
{ radioList: [], checkboxList: [], shortAnswerList: [] }
)
})
},
// 请求提交接口
handleSubmitRequest(params) {
params.offset = this.offset
api
.submitCourseExam(this.sid, this.cid, this.pid, params)
.then(response => {
if (params.type === 0) {
console.log('暂存成功')
return
}
if (response.code === 200) {
this.$message.success('考试答卷提交成功')
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
this.getExam()
} else {
this.$message.error(response.data.error)
}
})
.catch(error => {
this.$message.error(error.message)
})
.finally(() => {
this.submitLoading = false
})
},
// 上一套试卷
handlePrev() {
const offset = this.offset - 1
this.$router.push({ query: { offset } })
},
handleNext() {
const offset = this.offset + 1
this.$router.push({ query: { offset } })
},
handleNewExam() {
this.$router.push({ query: { offset: this.examCount } })
this.examCount++
},
// 清除定时器
clearTimer() {
this.autoSubmitTimer && clearInterval(this.autoSubmitTimer)
this.checkStatusTimer && clearInterval(this.checkStatusTimer)
}
},
beforeMount() {
// // 自动获取考试状态
// this.autoCheckExamStatus()
// // 获取试题
// this.getExam()
},
destroyed() {
this.clearTimer()
}
}
</script>
<style lang="scss" scoped>
.exam-buttons {
padding: 40px 0;
text-align: center;
.el-button {
min-width: 160px;
margin: 40px auto;
}
}
.no-exam {
padding: 100px;
font-size: 30px;
text-align: center;
}
.exam-welcome {
padding: 40px;
line-height: 30px;
text-align: center;
::v-deep .el-button {
margin-top: 30px;
}
}
</style>
<template>
<container :title="chapter.name" v-loading="loading">
<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="主题" maxlength="50"></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 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.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"
:disabled="isRevised"
:loading="submitLoading"
@click="onSubmit"
>{{submitText}}</el-button>
</el-tooltip>
</div>
</template>
</el-step>
</el-steps>
</container>
</template>
<script>
// componets
import Container from '../common/container.vue'
import VEditor from '../common/editor.vue'
import VUpload from '../common/upload.vue'
// api
import * as api from '../../api'
// 课程大作业
export default {
name: 'CourseWork',
components: { Container, VEditor, VUpload },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
data() {
return {
ruleForm: {
essay_name: '',
essay_description: '',
url: ''
},
rules: {
essay_name: [
{ required: true, message: '请输入主题', trigger: 'blur' }
],
essay_description: [
{ required: true, message: '请输入正文', trigger: 'change' }
],
url: [{ required: true, message: '请上传附件', trigger: 'change' }]
},
detail: null,
loading: false,
messageInstance: null,
submitLoading: false
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 是否批改
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.messageInstance && this.messageInstance.close()
if (!this.ruleForm.essay_name) {
this.messageInstance = this.$message.error('请输入主题')
return
}
if (!this.ruleForm.essay_description) {
this.messageInstance = this.$message.error('请输入正文')
return
}
if (!this.ruleForm.url) {
this.messageInstance = this.$message.error('请上传附件')
return
}
const params = Object.assign(this.ruleForm, {
semester_id: this.sid,
course_id: this.cid
})
this.handleSubmitRequest(params)
},
// 请求提交接口
handleSubmitRequest(params) {
this.submitLoading = true
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)
})
.finally(() => {
this.submitLoading = false
})
}
},
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;
b {
white-space: nowrap;
}
}
</style>
<template>
<div class="q-item">
<div class="q-item-hd">
<div class="q-item-num">{{ index + 1 }}.</div>
<div class="q-item-title" v-html="data.content"></div>
<div class="q-item-aside">
<template v-if="typeText">({{ typeText }})</template>
<template v-if="data.hasOwnProperty('score')">({{ data.score }}分)</template>
</div>
</div>
<div class="q-item-bd">
<!-- 单选 -->
<el-radio-group v-model="currentValue.user_answer" v-if="type === 1">
<div class="q-option-item" v-for="item in currentOptions" :key="item.id">
<el-radio :class="genClass(item)" :label="item.id">
<div class="q-option-item__answer" v-html="item.abc_option"></div>
</el-radio>
</div>
</el-radio-group>
<!-- 多选 -->
<el-checkbox-group v-model="currentValue.user_answer" v-if="type === 2">
<div class="q-option-item" v-for="item in currentOptions" :key="item.id">
<el-checkbox :class="genClass(item)" :label="item.id">
<div class="q-option-item__answer" v-html="item.abc_option"></div>
</el-checkbox>
</div>
</el-checkbox-group>
<!-- 简答题 -->
<template v-if="type === 3">
<v-editor v-model="currentValue.user_answer" :disabled="disabled"></v-editor>
<v-upload :disabled="disabled" v-model="currentValue.attachments">请上传对应的文件附件:</v-upload>
</template>
</div>
<div class="q-item-ft" v-if="disabled && showResult">
<template v-if="type === 3">
<p v-if="data.check_comment">
<span>评语:</span>
<span>{{ data.check_comment }}</span>
</p>
</template>
<template v-else>
<div class="result">
<p>
<span>学生答案:</span>
<span :class="isCorrect ? 'is-success' : 'is-error'">{{ submitAnswerText }}</span>
</p>
<p>
<span>正确答案:</span>
<span>{{ correctAnswerText }}</span>
</p>
</div>
</template>
<p v-if="data.hasOwnProperty('get_score')">
<span>评分:</span>
<span>{{ data.get_score }}分</span>
</p>
<div class="analyze" v-if="data.analysis">
<span>解析:</span>
<div class="analyze-main">
<span style="color: blue; cursor: pointer" @click="showAnalyze = !showAnalyze">查看解析</span>
<div v-html="data.analysis" v-if="data.analysis" v-show="showAnalyze" class="analyze-content"></div>
</div>
</div>
</div>
</div>
</template>
<script>
// components
import VEditor from '../common/editor.vue'
import VUpload from '../common/upload.vue'
export default {
name: 'ExamItem',
components: { VEditor, VUpload },
props: {
// 索引
index: { type: Number },
// 问题类型
type: { type: Number },
// 单条数据
data: {
type: Object,
default() {
return {}
}
},
// 提交的答案
value: {
type: Object,
default() {
return {}
}
},
// 是否禁用,提交过的是禁用状态
disabled: { type: Boolean, default: false },
showResult: { type: Boolean, default: true }
},
data() {
return {
currentValue: {},
showAnalyze: false
}
},
watch: {
value: {
immediate: true,
handler(value) {
this.currentValue = value
}
}
},
computed: {
// 26个英文字母
A_Z() {
const result = []
for (let i = 0; i < 26; i++) {
result.push(String.fromCharCode(65 + i))
}
return result
},
// 选项类型
typeText() {
const map = { 1: '单选题', 2: '多选题' }
return map[this.type]
},
// 处理后的options数据
currentOptions() {
if (!this.data.options) {
return []
}
return this.data.options.map((item, index) => {
// 英文字母 + 名称
item.abc = this.A_Z[index]
item.abc_option = `${this.A_Z[index]}. ${item.option}`
// 提交时的选中状态
const value = this.value.user_answer || ''
item.selected = Array.isArray(value) ? value.includes(item.id) : value === item.id
// 处理正确的选中状态
const hasChecked = Object.prototype.hasOwnProperty.call(item, 'checked')
const rightAnswer = this.data.right_answer || ''
if (!hasChecked && rightAnswer) {
item.checked = Array.isArray(rightAnswer) ? rightAnswer.includes(item.id) : rightAnswer === 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 || !this.showResult) {
return null
}
return {
'is-error': !this.isCorrect && item.selected,
'is-success': this.isCorrect && item.selected
}
}
}
}
</script>
<style lang="scss" scoped>
.q-item {
font-size: 16px;
padding: 10px 0;
border-bottom: 1px solid #c9c9c97a;
.upload {
font-size: 14px;
}
}
.q-item-hd {
display: flex;
padding: 10px 0 20px;
::v-deep p {
margin: 0;
padding: 0;
}
::v-deep ul {
margin: 0;
padding: 0;
list-style: none;
}
}
.q-item-num {
width: 20px;
text-align: center;
}
.q-item-title {
flex: 1;
::v-deep img {
max-width: 100%;
}
}
.q-item-aside {
padding-left: 20px;
// align-self: flex-end;
}
.q-option-item {
padding-left: 20px;
margin-bottom: 14px;
}
.q-option-item__answer {
display: inline;
::v-deep * {
display: inline;
}
}
.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 {
padding: 10px 0;
p {
font-size: 14px;
margin: 0 0 10px 0;
}
.result {
display: flex;
justify-content: flex-end;
p {
padding-left: 20px;
}
}
.analyze {
display: flex;
font-size: 14px;
}
.analyze-main {
flex: 1;
overflow: hidden;
}
.analyze-content {
margin-top: 10px;
background-color: #c9c9c97a;
border: 1px solid #c9c9c97a;
padding: 10px;
::v-deep * {
margin: 0;
padding: 0;
max-width: 100%;
}
}
}
</style>
<template>
<component :is="currentCompoent" :chapter="chapter" :data="data" v-bind="$attrs" v-on="$listeners" v-if="chapter" />
</template>
<script>
// componets
import ChapterWork from './chapterWork.vue'
import ChapterTest from './chapterTest.vue'
export default {
name: 'ViewerWork',
components: { ChapterWork, ChapterTest },
props: {
// 当前选中的
chapter: {
type: Object,
default() {
return {}
}
},
// 课程详情接口返回的数据
data: {
type: Object,
default() {
return {}
}
}
},
computed: {
currentCompoent() {
const componentNames = {
1: 'ChapterTest', // 课后测验
2: 'ChapterWork' // 作业
}
const homework = this.chapter.homework
return homework ? componentNames[homework.work_type] : ''
}
}
}
</script>
const routes = [
{
path: '/viewer/:cid',
component: () => import('./views/Index.vue'),
children: [{ name: 'viewerCourseChapter', path: ':id', component: () => import('./components/layout.vue') }]
}
]
export { routes }
<template>
<div class="course-viewer" element-loading-text="加载中..." v-loading="!loaded">
<div class="course-viewer-main">
<!-- 顶部区域 -->
<div class="course-viewer-main-hd">
<router-link :to="`/course/learn/${cid}`">
<i class="el-icon-arrow-left"></i>
</router-link>
<h1 class="course-viewer-main-hd__title">{{ detail.course_name }}</h1>
<div class="course-menu" @click="menuVisible = !menuVisible">
<i class="el-icon-s-unfold" v-if="menuVisible"></i>
<i class="el-icon-s-fold" v-else></i>
</div>
</div>
<!-- 主体区域 -->
<div class="course-viewer-main-bd">
<router-view
:data="detail"
:chapter="activeChapter"
:pptIndex="pptIndex"
:isSeek="isSeek"
:key="pid"
@pptupdate="handlePPTupdate"
@change-ppt="handleChangePPT(...arguments, false)"
@update="getCourse"
/>
</div>
</div>
<!-- 侧边栏 -->
<v-aside
:data="detail"
:chapters="chapters"
:active="activeChapter"
:ppts="ppts"
:pptIndex="pptIndex"
@change-ppt="handleChangePPT(...arguments, true)"
v-if="detail.chapters"
v-show="menuVisible"
></v-aside>
</div>
</template>
<script>
// api
import * as api from '../api'
// components
import VAside from '../components/aside/index.vue'
export default {
name: 'CourseViewer',
components: { VAside },
data() {
return {
detail: {},
ppts: [],
pptIndex: 0,
isSeek: false,
menuVisible: true,
loaded: false
}
},
watch: {
activeChapter() {
this.ppts = []
this.pptIndex = 0
},
isLive(value) {
if (value) {
this.menuVisible = false
}
},
isCourseExam(value) {
if (value) {
this.menuVisible = false
}
}
},
computed: {
// 学期ID
sid() {
return this.$route.params.sid
},
// 课程ID
cid() {
return this.$route.params.cid
},
// 当前页面的ID
pid() {
return this.$route.params.id
},
// 章节列表
chapters() {
const chapters = this.detail.chapters || []
if (!chapters.length) {
return []
}
const customeChapter = {
name: '大作业及资料',
children: [
{ name: '课程大作业', id: 'course_work', type: 99 },
{ name: '课程资料', id: 'course_info', type: 100 },
{ name: '教学评估', id: 'teach_evaluation', type: 102 }
]
}
// 课程考试
if (this.detail.course_examination) {
customeChapter.children.push({
name: '课程考试',
id: 'course_exam',
type: 101
})
}
// chapters.push(customeChapter)
return chapters
},
// 当前选中的章节
activeChapter() {
const id = this.pid
const list = this.chapters
return this.findChapter(id, list)
},
// 直播
isLive() {
return this.activeChapter ? [5, 8].includes(this.activeChapter.type) : false
},
// 课程考试
isCourseExam() {
return this.activeChapter ? this.activeChapter.type === 101 : false
}
},
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() {
this.loaded = false
api
.getCourse(this.sid, this.cid)
.then(response => {
this.detail = response
})
.finally(() => {
this.loaded = true
})
},
// PPT列表更新
handlePPTupdate(list) {
this.ppts = list
},
// 右侧菜单选中的PPT修改
handleChangePPT(index, isSeek) {
this.pptIndex = index
this.isSeek = isSeek
}
},
beforeMount() {
this.getCourse()
}
}
</script>
<style lang="scss">
.course-viewer {
display: flex;
height: 100vh;
overflow: hidden;
}
.course-viewer-main {
flex: 1;
display: flex;
flex-direction: column;
}
.course-viewer-main-hd {
display: flex;
align-items: center;
background-color: #3f3f3f;
height: 56px;
a {
color: #fff;
padding: 10px;
}
i {
font-size: 24px;
}
}
.course-viewer-main-hd__title {
flex: 1;
font-size: 1.5em;
// text-align: center;
color: #a0a0a0;
}
.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;
}
.course-menu {
width: 24px;
height: 24px;
padding: 12px;
margin-right: 10px;
color: #fff;
text-align: center;
border-radius: 50%;
cursor: pointer;
&:hover {
background-color: rgba(255, 255, 255, 0.08);
}
}
</style>
const routes = [
{
path: '/',
component: () => import('@/components/layout/index.vue'),
children: [
{ path: '/exam/exam', component: () => import('./views/Index.vue') },
{ path: '/exam/exam/result', component: () => import('./views/Result.vue') }
]
},
{ path: '/exam/exam/exam', component: () => import('./views/Exam.vue') }
]
export { routes }
<template>
<div>
<exam-card
title="模拟考试"
:hasMark="hasMark"
:status="status"
:hasSubmitBtn="!!!this.$route.query.id"
:hasCountdown="!!!this.$route.query.id"
:data="data"
@submit="submitExam"
@back="handleBack"
ref="exam"
v-if="Object.keys(data).length"
></exam-card>
</div>
</template>
<script>
import * as api from '@/api/exam.js'
import ExamCard from '@/components/exam/examCard.vue'
export default {
components: { ExamCard },
data() {
return {
status: 1, // 考试状态
hasMark: true,
data: {},
cacheAnswerTime: null // 缓存题计时器
}
},
computed: {
examId() {
return this.$route.query.exam_id
}
},
beforeMount() {
// 获取考卷
this.getTopic()
},
beforeDestroy() {
clearInterval(this.cacheAnswerTime) // 停止缓存
},
methods: {
// 获取考卷
getTopic() {
const isCreate =
this.$route.query.id || this.$route.query.is_create === undefined ? 0 : this.$route.query.is_create
const param = {
type: 2, // 1:能力自测, 2:模拟考试
paper_id: this.examId,
is_create: isCreate // 是否重新测试: 1:是,否则不是
}
api.getExamQuestion(param).then(response => {
this.data = JSON.parse(response.data).sheet
// 已提交
const isSubmited = ['1', '2'].includes(this.data.status)
// 缓存答题
if (!isSubmited) {
clearInterval(this.cacheAnswerTime)
this.cacheAnswerTime = setInterval(() => {
this.submitExam(0)
}, 3000)
} else {
this.status = 2
this.hasMark = false
}
})
},
// 返回
handleBack() {
this.$router.push('/exam/exam')
},
// 提交考卷 isCache:0缓存,1提交
submitExam(isCache) {
const refData = this.$refs.exam
const id = this.data.id
const answer = {}
let answerNum = 0
refData.questionGroups.forEach(item => {
if (!answer[item.question_item_id]) answer[item.question_item_id] = {}
item.question_list.forEach(cItem => {
if (!cItem.user_answer.length) {
answerNum++
}
answer[item.question_item_id][cItem.id] = {
sign: cItem.sign ? cItem.sign : false,
answer: cItem.user_answer
}
})
})
if (isCache) {
if (answerNum !== 0 && !this.$refs.exam.isCountDownEnd) {
this.$confirm(`您还有${answerNum}道题没有作答`, '请确认', {
confirmButtonText: '确认提交',
cancelButtonText: '继续作答',
type: 'warning'
})
.then(() => {
this.submitApi(id, isCache, answer, refData.duration)
})
.catch(() => {})
} else {
this.submitApi(id, isCache, answer, refData.duration)
}
} else {
this.submitApi(id, isCache, answer, refData.duration)
}
},
submitApi(id, isCache, answer, duration) {
const param = {
sheet_id: id,
status: isCache !== 0 ? 1 : 0, // 0缓存,1提交
answers: JSON.stringify(answer),
duration: duration
}
api.setCache(param).then(response => {
if (isCache) {
clearInterval(this.cacheAnswerTime)
this.$router.replace({ path: '/exam/exam/result', query: { exam_id: this.examId } })
}
})
}
}
}
</script>
<template>
<app-container title="试卷管理">
<div class="exam-card">
<div class="exam-card__head">
2022年第一期<br />
金融数据合规管理证书考试
</div>
<div class="card-body">
<div class="card-body__data">
试卷总分:<span>100</span>
</div>
<div class="card-body__data">
及格分数:<span>100</span>
</div>
<div class="card-body__data">
考试时长:<span>100</span>分钟
</div>
<!-- <div class="card-body__data red">
剩余时长:<span>100</span>分钟
</div> -->
<!-- <div class="card-body__btn">开始考试</div> -->
<!-- <div class="card-body__btn2">继续考试</div> -->
<div class="card-body__btn3">已完成</div>
</div>
</div>
</app-container>
</template>
<script>
export default {}
</script>
<style lang="scss" scoped>
.exam-card {
width: 311px;
// height: 408px;
padding-bottom: 40px;
margin: 50px auto;
background: rgba(248, 248, 248, 1);
border-radius: 10px;
overflow: hidden;
.exam-card__head {
padding-top: 22px;
box-sizing: border-box;
height: 99px;
background: rgba(170, 25, 65, 1);
font-size: 18px;
line-height: 26px;
color: #ffffff;
text-align: center;
}
.card-body{
.card-body__data{
padding-left: 87px;
box-sizing: border-box;
margin-top: 35px;
font-size: 16px;
color: rgba(102, 102, 102, 1);
line-height: 100%;
span{
font-size: 22px;
font-weight: bold;
}
&.red{
color: rgba(170, 25, 65, 1);
}
}
.card-body__btn{
width: 150px;
height: 41px;
background: rgba(255, 245, 248, 0.39);
border: 1px solid #AA1941;
border-radius: 6px;
text-align: center;
line-height: 41px;
box-sizing: border-box;
color: #AA1941;
font-size: 16px;
margin: 60px auto 0;
cursor: pointer;
}
.card-body__btn3{
width: 150px;
height: 41px;
background: rgba(221, 221, 221, 1);
border-radius: 6px;
text-align: center;
line-height: 41px;
box-sizing: border-box;
color: rgba(102, 102, 102, 1);
font-size: 16px;
margin: 60px auto 0;
cursor: pointer;
}
}
}
</style>
<template>
<div class="result-box" v-if="data.sheet">
<div class="card-left">
<div class="title">
<span>成绩报告</span>
<span class="time" v-if="data.sheet">
{{ data.sheet.created_time }}
</span>
</div>
<div class="chart-box">
<div class="chart-item">
<div class="chart-title">成绩</div>
<chart :accuracy="objectQuestionScore">
<template v-slot:tips>
<div class="num">{{ objectQuestionScore }}</div>
</template>
</chart>
</div>
</div>
<p class="new__text">恭喜考试通过,您已获得xxxx证书,请前往我的证书页面查看</p>
</div>
<!-- <div class="card-right">
<card v-if="data.sheet" :data="data.sheet" @goQuestion="goPage">
<template v-slot:btnBox>
<div class="btn-box">
<div class="btn" @click="goPage('all')">全部解析</div>
</div>
</template>
</card>
</div> -->
</div>
</template>
<script>
import chart from '@/components/exam/result/pieChart.vue'
import * as api from '@/api/exam.js'
export default {
components: {
chart
},
data() {
return {
data: {
sheet: {}
},
accuracy: 0,
accuracScore: 0,
accuracText: 0,
subjectQuestionTotal: 0,
subjectQuestionScore: 0,
objectQuestionTotal: 0,
objectQuestionScore: 0,
status: 0
}
},
created() {
this.getExamPapers()
},
computed: {
examId() {
return this.$route.query.exam_id
},
percent() {
return (
(this.subjectQuestionScore + this.objectQuestionScore) / (this.subjectQuestionTotal + this.objectQuestionTotal)
)
},
setStyle() {
return `width: ${100 * this.percent}%`
}
},
methods: {
goPage(param) {
this.$router.push({
path: '/exam/exam/exam',
query: { exam_id: this.examId, id: param }
})
},
getExamPapers() {
const param = {
type: 2,
paper_id: this.examId,
is_create: 0
}
api.getExamQuestion(param).then(response => {
const data = JSON.parse(response.data)
// let rightNum = 0
// let totalNum = 0
// data.sheet.questions.question_items.forEach(list => {
// list.question_list = list.question_list.reduce((a, b) => {
// return a.concat(b)
// }, [])
// list.question_list.forEach(item => {
// const currentItem = data.sheet.score_items[list.question_item_id][item.id]
// if (currentItem.checked_flag) {
// totalNum++
// if (currentItem.is_right) rightNum++
// } else {
// if (item.question_options) {
// totalNum++
// if (currentItem.is_right) rightNum++
// }
// }
// })
// })
// this.accuracy = parseInt(rightNum)
// if (parseInt(rightNum) === 0 && parseInt(totalNum) === 0) {
// this.accuracText = '-'
// } else {
// this.accuracText = parseInt((rightNum / totalNum) * 100)
// }
this.data = data
// this.accuracScore = parseInt(totalNum)
this.status = data.sheet.status
let subjectQuestionTotal = 0
let subjectQuestionScore = 0
let objectQuestionTotal = 0
let objectQuestionScore = 0
data.sheet.questions.question_items.forEach(item => {
item.question_list = item.question_list.reduce((a, b) => {
return a.concat(b)
}, [])
const currentQuestionScore = data.sheet.score_items[item.question_item_id]
// console.log(item)
item.question_list.forEach(it => {
const currentItem = currentQuestionScore[it.id]
if (Array.isArray(it.question_options) && it.question_options.length) {
// if (currentItem.is_right) objectQuestionScore += parseFloat(currentItem.score)
objectQuestionScore += parseFloat(currentItem.score)
objectQuestionTotal += parseFloat(it.score)
} else {
// if (currentItem.is_right) subjectQuestionScore += parseFloat(currentItem.score)
subjectQuestionScore += parseFloat(currentItem.score)
subjectQuestionTotal += parseFloat(it.score)
}
})
})
this.subjectQuestionTotal = subjectQuestionTotal
this.subjectQuestionScore = subjectQuestionScore
// this.subjectQuestionScore = 20
this.objectQuestionTotal = objectQuestionTotal
this.objectQuestionScore = objectQuestionScore
// this.objectQuestionScore = 65
})
}
}
}
</script>
<style lang="scss" scoped>
.new__text{
text-align: center;
padding:30px;
font-size: 22px;
}
.result-box {
width: 100%;
// height: 100%;
display: flex;
.card-left {
box-sizing: border-box;
padding: 10px 30px 20px;
flex: 1;
background: #fff;
margin-right: 10px;
// height: 560px;
border-radius: 8px;
.title {
font-size: 18px;
color: #222222;
line-height: 45px;
border-bottom: 1px solid #ccc;
display: flex;
}
.time {
font-size: 14px;
color: #222222;
line-height: 45px;
margin-left: auto;
}
.chart-box {
// width: 148px;
margin: 26px 0 0;
display: flex;
justify-content: center;
.chart-item {
display: flex;
align-items: center;
.chart-title {
font-size: 30px;
color: #333;
margin-right: 20px;
}
}
.chart-item:nth-child(2) {
margin-left: 60px;
}
}
.assess {
font-size: 18px;
color: #222222;
line-height: 45px;
border-bottom: 1px solid #ccc;
}
.assess-box {
padding: 27px 0;
.prog {
width: 350px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
.line-box {
width: 300px;
width: 300px;
height: 10px;
background: #f9f9f9;
border-radius: 5px;
.line {
width: 80%;
height: 10px;
background: linear-gradient(90deg, #f47c46 0%, #f22f48 100%);
border-radius: 5px;
}
}
.icon {
width: 41px;
height: 38px;
background: url(@/assets/images/res-icon.png);
background-size: 100% 100%;
}
}
.text {
font-size: 14px;
color: #222222;
line-height: 20px;
text-align: center;
margin: 50px 0 68px 0;
}
.btn {
cursor: pointer;
text-align: center;
line-height: 40px;
width: 144px;
height: 40px;
background: #c01540;
border-radius: 4px;
font-size: 14px;
font-weight: bold;
color: #ffffff;
margin: 0 auto;
}
}
}
.card-right {
box-sizing: border-box;
flex: 1;
background: #fff;
// height: 560px;
border-radius: 8px;
margin-left: 10px;
padding: 10px 30px 0;
}
}
</style>
import httpRequest from '@/utils/axios'
/**
* 获取我的试题
*/
export function getMyQuestions(params) {
return httpRequest.get('/api/zy/v2/examination/my-question', { params })
}
/**
* 删除试题
*/
export function deleteQuestion(data) {
return httpRequest.post('/api/zy/v2/examination/delete-my-question', data)
}
/**
* 收藏试题
*/
export function addCollection(data) {
return httpRequest.post('/api/zy/v2/examination/add-collection', data)
}
/**
* 取消收藏试题
*/
export function deleteCollection(data) {
return httpRequest.post('/api/zy/v2/examination/delete-my-question', data)
}
<template>
<div class="my-question-list" element-loading-text="加载中..." v-loading="!loaded">
<div class="my-question-list-hd">
<div class="select">
<el-select v-model="selectId" placeholder="选择课程" clearable @change="handleTypeChange">
<el-option v-for="item in selectList" :key="item.id" :label="item.category_name" :value="item.id"></el-option>
</el-select>
</div>
<div class="title"><slot name="title"></slot></div>
<div class="tools">
<span>选择题型</span>
<el-select v-model="questionType" placeholder="选择课程" clearable @change="handleTypeChange">
<el-option v-for="item in typeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</div>
</div>
<div class="my-question-list-bd">
<template v-if="list.length">
<question-list-item
v-for="item in list"
:data="item"
:key="item.id"
v-bind="$attrs"
v-on="$listeners"
@on-remove="handleRemove"
@on-addCollection="handleAddCollection"
@on-removeCollection="handleRemoveCollection"
@on-click="handleClick(item.num)"
></question-list-item>
</template>
<div class="empty" v-else>暂无试题</div>
</div>
<div class="my-question-list-ft" v-if="list.length">
<div class="buttons">
<el-button size="medium" @click="toggleSelectAll(!isAllChecked)">
{{ isAllChecked ? '取消全选' : '全选' }}
</el-button>
<el-button size="medium" @click="handleBatchRemove" v-if="hasRemove">删除</el-button>
</div>
<div class="pagations">
<el-pagination
@current-change="handleCurrentChange"
:current-page.sync="currentPage"
layout="prev, pager, next"
:total="total"
>
</el-pagination>
</div>
</div>
</div>
</template>
<script>
import * as api from '../api.js'
import QuestionListItem from './QuestionListItem.vue'
export default {
components: { QuestionListItem },
props: {
requestCallback: Function,
requestParams: { type: Object, default: () => ({}) }
},
data() {
return {
loaded: false,
questionType: 0,
list: [],
total: 0,
currentPage: 1,
selectList: [],
selectId: '',
typeList: [
{ value: 0, label: '全部' },
{ value: 1, label: '单选题' },
{ value: 2, label: '多选题' },
{ value: 3, label: '问答题' },
{ value: 5, label: '案例题' },
{ value: 6, label: '判断题' },
{ value: 7, label: '实操题' },
{ value: 8, label: '情景题' }
]
}
},
computed: {
// 已选中
checkedList() {
return this.list.filter(item => item.checked)
},
// 是否全选
isAllChecked() {
return this.checkedList.length === this.list.length
},
// 是否显示删除按钮
hasRemove() {
return !!this.checkedList.length
}
},
methods: {
// 获取试题列表
getMyQuestions() {
this.loaded = false
const params = Object.assign({}, this.requestParams, {
question_type: this.questionType,
page: this.currentPage,
page_size: 10,
category_id: this.selectId
})
api
.getMyQuestions(params)
.then(response => {
const { list = [], current_total: total } = this.requestCallback ? this.requestCallback(response) : response
this.total = total
this.list = list.flat().map(item => {
item.checked = false
return item
})
this.list = this.changeArr(this.list)
this.selectList = response.all_category
this.$emit('request-success', response)
})
.finally(() => {
this.loaded = true
})
},
changeArr(arr) {
const obj = {}
arr = arr.reduce((item, next) => {
if (parseInt(next.group_id) === 0) {
item.push(next)
return item
} else {
if (!obj[next.group_id]) {
obj[next.group_id] = true
item.push(next)
}
return item
}
}, [])
return arr
},
// 类型改变
handleTypeChange(value) {
this.currentPage = 1
this.getMyQuestions()
},
// 页码改变
handleCurrentChange(value) {
this.getMyQuestions()
},
// 全选、取消全选
toggleSelectAll(checked) {
this.list = this.list.map(item => {
item.checked = checked
return item
})
},
// 收藏
handleAddCollection(data) {
api.addCollection({ question_id: data.question_id }).then(response => {
data.is_collection = true
})
},
// 取消收藏
handleRemoveCollection(data) {
api.deleteCollection({ question_id: data.question_id, type: 2 }).then(response => {
data.is_collection = false
})
},
// 删除
handleRemove(data) {
this.handleRemoveReqeust({ question_id: data.question_id })
},
// 批量删除
handleBatchRemove() {
const ids = this.checkedList.map(item => {
return item.question_id
})
this.handleRemoveReqeust({ question_id: ids.join(',') })
},
// 删除接口
handleRemoveReqeust(params) {
const requestParams = Object.assign({}, params, this.requestParams)
api.deleteQuestion(requestParams).then(response => {
this.$message({ message: '删除成功', type: 'success' })
this.getMyQuestions()
})
},
// 跳详情
handleClick(index) {
window.localStorage.removeItem('answerRecord')
this.$router.push({
path: '/exam/questions/exam',
query: { type: this.requestParams.type, qType: this.questionType, index }
})
}
},
beforeMount() {
this.getMyQuestions()
}
}
</script>
<style lang="scss" scoped>
.my-question-list-hd {
display: flex;
align-items: center;
padding-bottom: 8px;
font-size: 18px;
border-bottom: 1px solid #ccc;
.title {
flex: 1;
padding: 0 20px;
}
::v-deep .el-radio {
font-size: 18px;
}
::v-deep .el-radio__label {
font-size: 18px;
font-weight: 400;
}
::v-deep .el-radio__inner {
width: 18px;
height: 18px;
}
}
.my-question-list-ft {
padding-top: 20px;
display: flex;
justify-content: space-between;
align-items: center;
::v-deep .el-button {
width: 100px;
}
}
</style>
<template>
<div class="my-question-list-item">
<el-checkbox v-model="data.checked"></el-checkbox>
<div class="badge" :class="`questiont-type_${data.question_type}`">{{ questionTypeName }}</div>
<div class="name" v-html="data.question_content || data.common_content" @click="$emit('on-click')"></div>
<div class="tools">
<i class="el-icon-delete" @click="$emit('on-remove', data)"></i>
<i
class="el-icon-star-on"
:class="{ 'is-active': data.is_collection }"
@click="toggleCollection"
v-if="hasCollection"
></i>
</div>
</div>
</template>
<script>
export default {
props: {
data: { type: Object, default: () => ({}) },
hasCollection: { type: Boolean, default: true }
},
data() {
return {
radio: false
}
},
computed: {
questionTypeName() {
const map = { 1: '单选题', 2: '多选题', 3: '问答题', 5: '案例题', 6: '判断题', 7: '实操题', 8: '情景题' }
return map[this.data.question_type]
}
},
methods: {
toggleCollection() {
this.data.is_collection ? this.$emit('on-removeCollection', this.data) : this.$emit('on-addCollection', this.data)
}
}
}
</script>
<style lang="scss" scoped>
.my-question-list-item {
display: flex;
align-items: center;
padding: 22px 0;
border-bottom: 1px solid #eee;
&:hover {
.name {
color: #c01540;
}
}
.badge {
margin: 0 8px;
width: 60px;
height: 18px;
font-size: 12px;
line-height: 18px;
color: #fff;
text-align: center;
background: #ffbe44;
border-radius: 9px;
}
.questiont-type_1 {
background: #f47c46;
}
.questiont-type_2 {
background: #ffbe44;
}
.questiont-type_5 {
background: #7ed6e8;
}
.questiont-type_6 {
background: #83da60;
}
.name {
flex: 1;
padding: 0 10px;
font-size: 18px;
}
.tools {
padding-left: 20px;
display: flex;
font-size: 18px;
color: #d4d4d4;
i {
margin-left: 10px;
cursor: pointer;
}
.is-active {
color: #ffcd39;
}
}
}
</style>
const routes = [
{
path: '/',
component: () => import('@/components/layout/index.vue'),
children: [
{ path: '/exam/questions/all', component: () => import('./views/All.vue') },
{ path: '/exam/questions/wrong', component: () => import('./views/Wrong.vue') },
{ path: '/exam/questions/collection', component: () => import('./views/Collection.vue') }
]
},
{ path: '/exam/questions/exam', component: () => import('./views/Exam.vue') }
]
export { routes }
<template>
<app-container>
<question-list :requestParams="{ type: 3 }" @request-success="handleRequestSuccess">
<template #title>做题总数:{{ total }}</template>
</question-list>
</app-container>
</template>
<script>
import QuestionList from '../components/QuestionList.vue'
export default {
components: { QuestionList },
data() {
return {
total: 0
}
},
methods: {
handleRequestSuccess(response) {
this.total = response.total
}
}
}
</script>
<template>
<app-container>
<question-list :hasCollection="false" :requestParams="{ type: 2 }" @request-success="handleRequestSuccess">
<template #title>收藏总数:{{ total }}</template>
</question-list>
</app-container>
</template>
<script>
import QuestionList from '../components/QuestionList.vue'
export default {
components: { QuestionList },
data() {
return {
total: 0
}
},
methods: {
handleRequestSuccess(response) {
this.total = response.current_total
}
}
}
</script>
<template>
<exam-card
:title="title"
:status="2"
:groups="questionGroups"
:numberGroups="numberGroups"
:hasMark="false"
:hasDeleteBtn="true"
:hasCountDown="false"
:hasShowResultBtn="true"
:groupPage="bigNum"
:groupPageSize="pageSize"
:groupPageCount="total"
submitButtonText="清空记录,重新答题"
@back="handleBack"
@submit="handleSubmit"
@page-change="handlePageChange"
@delete="deleteQuestion"
@primary="handlePrimary"
ref="exam"
v-loading="loading"
>
</exam-card>
</template>
<script>
import * as api from '@/api/exam.js'
import ExamCard from '@/components/exam/examCard.vue'
export default {
components: { ExamCard },
data() {
return {
loading: false,
bigNum: 0,
page: 0,
pageSize: 10,
list: [], // 试题组
total: 0,
allQuestionList: [] // 所有试题
}
},
computed: {
title() {
return this.$route.query.type === '1' ? '错题集合' : '收藏试题'
},
questionGroups() {
return this.list.map(list => {
const [first = {}] = list
return { question_item_id: '', question_type: first.question_type, question_list: list, hasResult: false }
})
},
numberGroups() {
return this.allQuestionList.map(list => {
const [first = {}] = list
return { question_item_id: '', question_type: first.question_type, question_list: list }
})
}
},
beforeMount() {
this.init()
},
methods: {
init() {
// 获取所有小题
this.getAllQuestion().then(() => {
const flatList = this.allQuestionList.reduce((result, list) => {
return result.concat(list)
}, [])
const index = parseInt(this.$route.query.index) || 0
// // 通过小题编号查找大题
const found = flatList.find(item => item.num === index)
if (found) {
this.bigNum = found.big_num
this.page = parseInt((found.big_num - 1) / this.pageSize)
}
// 获取考卷
this.getTopic()
})
},
// 删除题目
deleteQuestion(data) {
const param = {
question_id: data.question_list[0].question_id,
type: this.$route.query.type
}
api.deleteQuestion(param).then(response => {
this.page = 0
this.getAllQuestion()
this.getTopic().then(() => {
// 重置
this.$refs.exam.reset()
})
})
},
// 返回
handleBack() {
if (this.$route.query.type === '1') {
this.$router.push('/exam/questions/wrong')
} else {
this.$router.push('/exam/questions/collection')
}
},
// 获取考卷
getTopic() {
const query = this.$route.query
const param = {
type: query.type,
question_type: query.qType,
page: this.page + 1,
page_size: this.pageSize
}
this.loading = true
return api.getMyQuestion(param).then(response => {
this.list = response.list
this.total = response.collection_total || response.error_total
this.loading = false
})
},
getAllQuestion() {
const query = this.$route.query
const param = {
type: query.type,
question_type: query.qType
}
return api.getAllQuestion(param).then(response => {
this.allQuestionList = response.list
if (!this.allQuestionList.length) {
this.handleBack()
}
})
},
// 清空
handleSubmit(data) {
const param = {
type: this.$route.query.type,
question_type: this.$route.query.qType,
clear: 1
}
this.cacheQuestion(param, () => {
this.page = 0
this.getAllQuestion()
this.getTopic().then(() => {
// 重置
this.$refs.exam.reset()
})
})
},
// 确认答案
handlePrimary(data, groups) {
this.cacheQuestion(this.genSubmitData(groups)).then(() => {
this.getAllQuestion()
})
},
// 翻页
handlePageChange(index, data, groups) {
const page = parseInt((index - 1) / this.pageSize)
if (this.page !== page) {
this.page = page
this.getTopic()
}
this.cacheQuestion(this.genSubmitData(groups))
},
cacheQuestion(param, callback) {
return api.setMyCache(param).then(response => {
callback && callback()
})
},
// 组装提交数据
genSubmitData(questionGroups) {
const answers = {}
questionGroups.forEach(group => {
group.question_list.forEach(item => {
if (item.user_answer.length) {
answers[item.question_id] = item.user_answer
}
})
})
return {
type: this.$route.query.type,
question_type: this.$route.query.qType,
answer: JSON.stringify(answers)
}
}
}
}
</script>
<template>
<app-container>
<question-list :requestParams="{ type: 1 }" @request-success="handleRequestSuccess">
<template #title>错题总数:{{ total }}</template>
</question-list>
</app-container>
</template>
<script>
import QuestionList from '../components/QuestionList.vue'
export default {
components: { QuestionList },
data() {
return {
total: 0
}
},
methods: {
handleRequestSuccess(response) {
this.total = response.current_total
}
}
}
</script>
import httpRequest from '@/utils/axios'
/* 意见反馈 */
export function submitFeedback(data) {
return httpRequest.post('/api/zy/v2/feedback/commit', data)
}
const routes = [
{
path: '/',
component: () => import('@/components/layout/index.vue'),
children: [{ path: '/exam/record', component: () => import('./views/Index.vue') }]
}
]
export { routes }
<template>
<app-container title="学员须知">
<app-list v-bind="tableOptions" ref="tabList">
</app-list>
</app-container>
</template>
<script>
import AppList from '@/components/base/AppList.vue'
export default {
components: {
AppList
},
data() {
return {
tableOptions: {
remote: {
},
columns: [
{ prop: 'name', label: '序号' },
{ prop: 'user_count', label: '考卷名称' },
{ prop: 'operate2', label: '分数' },
{ prop: 'operate3', label: '考试时间' }
]
}
}
}
}
</script>
<style lang="scss" scoped>
.notice-title {
font-size: 16px;
line-height: 27px;
color: #666666;
margin-left: 35px;
}
.apply {
margin-top: 67px;
.apply__title {
font-size: 16px;
color: #424242;
margin-bottom: 24px;
line-height: 100%;
}
img {
width: 910px;
display: block;
}
.apply__tips {
font-size: 14px;
color: #898989;
margin-left: 242px;
}
}
.notice {
margin-top: 67px;
.notice__title {
font-size: 16px;
color: #424242;
margin-bottom: 28px;
line-height: 100%;
}
.notice__content{
font-size: 16px;
line-height: 34px;
color: rgba(137, 137, 137, 1);
margin-left: 46px;
span{
color: rgba(102, 102, 102, 1);
font-weight: bold;
}
}
}
</style>
const routes = [
{
path: '/',
component: () => import('@/components/layout/index.vue'),
children: [{ path: '/exam/test/result', component: () => import('./views/Result.vue') }]
},
{ path: '/exam/test/exam', component: () => import('./views/Exam.vue') }
]
export { routes }
<template>
<exam-card
title="课后练习"
:hasMark="hasMark"
:status="status"
:data="data"
:hasCountdown="false"
@submit="handleSubmit"
@back="handleBack"
ref="exam"
v-if="Object.keys(data).length"
></exam-card>
</template>
<script>
import * as api from '@/api/exam.js'
import ExamCard from '@/components/exam/examCard.vue'
export default {
components: { ExamCard },
data() {
return {
status: 1, // 考试状态
hasMark: true,
data: {},
cacheAnswerTime: null
}
},
beforeMount() {
// 获取考卷
this.getTopic()
},
beforeDestroy() {
this.cacheAnswerTime && clearInterval(this.cacheAnswerTime) // 停止缓存
},
methods: {
// 获取考卷
getTopic() {
const query = this.$route.query
const param = {
type: 1,
is_create: parseInt(query.type) === 2 ? 0 : 1,
course_id: query.course_id,
chapter_id: query.chapter_id,
paper_id: query.exam_id
}
api.getCourseQuestion(param).then(response => {
this.data = JSON.parse(response.data).sheet
// 已提交
const isSubmited = ['1', '2'].includes(this.data.status)
// 缓存答题
if (isSubmited) {
this.status = 2
this.hasMark = false
} else {
clearInterval(this.cacheAnswerTime)
this.cacheAnswerTime = setInterval(this.handleCache, 3000)
}
})
},
// 返回
handleBack() {
this.$router.push(`/course/learn/${this.$route.query.course_id}`)
},
// 提交答案
handleSubmit(data) {
const params = this.genSubmitData(data).params
const answerNum = this.genSubmitData(data).answerNum
params.status = 1
if (answerNum !== 0) {
this.$confirm(`您还有${answerNum}道题没有作答`, '请确认', {
confirmButtonText: '确认提交',
cancelButtonText: '继续作答',
type: 'warning'
})
.then(() => {
this.submitApi(params)
})
.catch(() => {})
} else {
this.submitApi(params)
}
},
submitApi(params) {
this.cacheAnswerTime && clearInterval(this.cacheAnswerTime) // 停止缓存
api.setCourseCache(params).then(res => {
this.$router.replace({
path: '/exam/test/result',
query: Object.assign({}, this.$route.query, { type: 2 })
})
})
},
// 缓存答案
handleCache() {
const refData = this.$refs.exam
const params = this.genSubmitData(refData.questionGroups).params
params.status = 0
api.setCourseCache(params)
},
// 组装提交数据
genSubmitData(questionGroups) {
const answers = {}
let answerNum = 0
questionGroups.forEach(group => {
if (!answers[group.question_item_id]) answers[group.question_item_id] = {}
group.question_list.forEach(item => {
if (!item.user_answer.length) {
answerNum++
}
answers[group.question_item_id][item.id] = {
sign: item.sign ? item.sign : false,
answer: item.user_answer
}
})
})
const data = {
params: { type: 1, sheet_id: this.data.id, status: 0, answers: JSON.stringify(answers), duration: 0 },
answerNum: answerNum
}
return data
}
}
}
</script>
<template>
<div class="result-box" v-if="data.sheet">
<div class="card-left">
<div class="title">
<span>成绩报告</span>
<span class="time" v-if="data.sheet">
{{ data.sheet.created_time }}
</span>
</div>
<div class="chart-box">
<!-- <chart :accuracy="accuracy" :accuracScore="accuracScore">
<template v-slot:tips>
<div class="num">{{ accuracText }}%</div>
<div class="t">正确率</div>
</template>
</chart> -->
<div class="chart-item" v-if="objectQuestionTotal">
<div class="chart-title">客观题</div>
<chart :accuracy="objectQuestionScore" :accuracScore="objectQuestionTotal">
<template v-slot:tips>
<div class="num">{{ objectQuestionScore }}</div>
</template>
</chart>
</div>
<div class="chart-item" v-if="subjectQuestionTotal">
<div class="chart-title">主观题</div>
<chart :accuracy="subjectQuestionScore" :accuracScore="subjectQuestionTotal">
<template v-slot:tips>
<div class="num" v-if="status === '2'">{{ subjectQuestionScore }}</div>
<div class="num" v-else>-分</div>
</template>
</chart>
</div>
</div>
<div class="assess">测试评估</div>
<div class="assess-box">
<div class="prog">
<div class="line-box">
<div class="line" :style="setStyle"></div>
</div>
<div class="icon"></div>
</div>
<!-- <div class="text" v-if="accuracText < 100">
{{ accuracText < 80 ? '您离成功还有一段距离,继续努力!' : '成功近在眼前,再接再厉!' }}
</div> -->
<template v-if="status !== '2'"> <p class="text">请等待老师评分~</p></template>
<template v-else>
<p class="text" v-if="percent < 0.8">您离成功还有一段距离,继续努力!</p>
<div class="chart-box">
<div class="chart-item">
<div class="chart-title">总分</div>
<chart
:accuracy="subjectQuestionScore + objectQuestionScore"
:accuracScore="subjectQuestionTotal + objectQuestionTotal"
>
<template v-slot:tips>
<div class="num">{{ subjectQuestionScore + objectQuestionScore }}</div>
</template>
</chart>
</div>
</div>
</template>
<!-- <div class="btn">全部考试服务</div> -->
</div>
</div>
<div class="card-right">
<card v-if="Object.keys(data.sheet).length" :data="data.sheet" @goQuestion="goPage">
<template v-slot:btnBox>
<div class="btn-box">
<div class="btn" @click="goPage('all')">全部解析</div>
<!-- <div class="btn" @click="goPage('err')">错题解析</div> -->
</div>
</template>
</card>
</div>
</div>
</template>
<script>
import chart from '@/components/exam/result/pieChart.vue'
import card from '@/components/exam/result/resultCard.vue'
import * as api from '@/api/exam.js'
export default {
components: {
chart,
card
},
data() {
return {
data: {},
accuracy: 0,
accuracScore: 0,
accuracText: 0,
subjectQuestionTotal: 0,
subjectQuestionScore: 0,
objectQuestionTotal: 0,
objectQuestionScore: 0,
status: 0
}
},
created() {
this.getExamPapers()
},
computed: {
examId() {
return this.$route.query.exam_id
},
percent() {
return (
(this.subjectQuestionScore + this.objectQuestionScore) / (this.subjectQuestionTotal + this.objectQuestionTotal)
)
},
setStyle() {
return `width: ${100 * this.percent}%`
}
},
methods: {
goPage(param) {
const urlParam = this.$route.query
urlParam.id = param
urlParam.type = 2
this.$router.push({ path: '/exam/test/exam', query: urlParam })
},
getExamPapers() {
const param = {
type: 1,
is_create: 0,
course_id: this.$route.query.course_id,
chapter_id: this.$route.query.chapter_id
}
api.getCourseQuestion(param).then(response => {
const data = JSON.parse(response.data)
// let rightNum = 0
// let totalNum = 0
// data.sheet.questions.question_items.forEach(list => {
// list.question_list = list.question_list.reduce((a, b) => {
// return a.concat(b)
// }, [])
// list.question_list.forEach(item => {
// const currentItem = data.sheet.score_items[list.question_item_id][item.id]
// if (currentItem.checked_flag) {
// totalNum++
// if (currentItem.is_right) rightNum++
// } else {
// if (item.question_options) {
// totalNum++
// if (currentItem.is_right) rightNum++
// }
// }
// })
// })
// this.accuracy = parseInt(rightNum)
// if (parseInt(rightNum) === 0 && parseInt(totalNum) === 0) {
// this.accuracText = '-'
// } else {
// this.accuracText = parseInt((rightNum / totalNum) * 100)
// }
this.data = data
// this.accuracScore = parseInt(totalNum)
this.status = data.sheet.status
let subjectQuestionTotal = 0
let subjectQuestionScore = 0
let objectQuestionTotal = 0
let objectQuestionScore = 0
data.sheet.questions.question_items.forEach(item => {
item.question_list = item.question_list.reduce((a, b) => {
return a.concat(b)
}, [])
const currentQuestionScore = data.sheet.score_items[item.question_item_id]
item.question_list.forEach(it => {
const currentItem = currentQuestionScore[it.id]
if (Array.isArray(it.question_options) && it.question_options.length) {
if (currentItem.is_right) objectQuestionScore += parseFloat(currentItem.score)
objectQuestionTotal += parseFloat(it.score)
} else {
if (currentItem.is_right) subjectQuestionScore += parseFloat(currentItem.score)
subjectQuestionTotal += parseFloat(it.score)
}
})
})
this.subjectQuestionTotal = subjectQuestionTotal
this.subjectQuestionScore = subjectQuestionScore
// this.subjectQuestionScore = 7
this.objectQuestionTotal = objectQuestionTotal
this.objectQuestionScore = objectQuestionScore
// this.objectQuestionScore = 29
})
}
}
}
</script>
<style lang="scss" scoped>
.result-box {
width: 100%;
// height: 100%;
display: flex;
.card-left {
box-sizing: border-box;
padding: 10px 30px 20px;
flex: 1;
background: #fff;
margin-right: 10px;
// height: 560px;
border-radius: 8px;
.title {
font-size: 18px;
color: #222222;
line-height: 45px;
border-bottom: 1px solid #ccc;
display: flex;
}
.time {
font-size: 14px;
color: #222222;
line-height: 45px;
margin-left: auto;
}
.chart-box {
// width: 148px;
margin: 26px 0 0;
display: flex;
justify-content: center;
.chart-item {
display: flex;
align-items: center;
.chart-title {
font-size: 30px;
color: #333;
margin-right: 20px;
}
}
.chart-item:nth-child(2) {
margin-left: 60px;
}
}
.assess {
font-size: 18px;
color: #222222;
line-height: 45px;
border-bottom: 1px solid #ccc;
}
.assess-box {
padding: 27px 0;
.prog {
width: 350px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
.line-box {
width: 300px;
width: 300px;
height: 10px;
background: #f9f9f9;
border-radius: 5px;
.line {
width: 80%;
height: 10px;
background: linear-gradient(90deg, #f47c46 0%, #f22f48 100%);
border-radius: 5px;
}
}
.icon {
width: 41px;
height: 38px;
background: url(@/assets/images/res-icon.png);
background-size: 100% 100%;
}
}
.text {
font-size: 14px;
color: #222222;
line-height: 20px;
text-align: center;
margin: 50px 0 68px 0;
}
.btn {
cursor: pointer;
text-align: center;
line-height: 40px;
width: 144px;
height: 40px;
background: #c01540;
border-radius: 4px;
font-size: 14px;
font-weight: bold;
color: #ffffff;
margin: 0 auto;
}
}
}
.card-right {
box-sizing: border-box;
flex: 1;
background: #fff;
// height: 560px;
border-radius: 8px;
margin-left: 10px;
padding: 10px 30px 0;
}
}
</style>
const routes = [
{
path: '/',
component: () => import('@/components/layout/index.vue'),
children: [{ path: '/index', component: () => import('./views/Index.vue') }]
}
]
export { routes }
<template>
<div></div>
</template>
const modules = Object.values(import.meta.globEager('./**/index.js'))
export default function({ router }) {
modules.forEach(({ routes }) => {
// 注册路由
routes &&
routes.forEach(route => {
router.addRoute(route)
})
})
}
import httpRequest from '@/utils/axios'
/* 意见反馈 */
export function submitFeedback(data) {
return httpRequest.post('/api/zy/v2/feedback/commit', data)
}
const routes = [
{
path: '/',
component: () => import('@/components/layout/index.vue'),
children: [{ path: '/notice', component: () => import('./views/Index.vue') }]
}
]
export { routes }
<template>
<app-container title="学员须知">
<div class="notice-title">
尊敬的学员:<br />
您好!欢迎参加金融数据合规管理课程及考试。请认真阅读以下须知。预祝大家学习愉快,考试顺利!
</div>
<div class="apply">
<div class="apply__title">申请流程</div>
<img src="https://webapp-pub.ezijing.com/project/cert/notice-img1.png" alt="" />
<div class="apply__tips">(提示:请在个人中心核对姓名信息,需真实准确,与证书保持一致)</div>
</div>
<div class="notice">
<div class="notice__title">考试须知</div>
<div class="notice__content">
考试条件:<span>完成所有课程观看,进度均达到100%后即可开启考试</span><br/>
考试时间:<span>60分钟</span><br/>
考试形式:<span>仅限电脑登录,保持网络稳定</span><br/>
考试规则:<span>进入考试系统后系统随机抓屏,离开或退出考试页面超过5次后系统将强制交卷</span><br/>
考试题型:<span>题库随机组题,单选+多选+判断</span><br/>
考试成绩:<span>提交试卷后即显示成绩,80分为通过,可获得本年度证书,低于80分则需重学重考</span><br/>
证书发放:<span>电子证书,考试通过后3个工作日内进入我的证书查看下载</span>
</div>
</div>
</app-container>
</template>
<script>
export default {}
</script>
<style lang="scss" scoped>
.notice-title {
font-size: 16px;
line-height: 27px;
color: #666666;
margin-left: 35px;
}
.apply {
margin-top: 67px;
.apply__title {
font-size: 16px;
color: #424242;
margin-bottom: 24px;
line-height: 100%;
}
img {
width: 910px;
display: block;
}
.apply__tips {
font-size: 14px;
color: #898989;
margin-left: 242px;
}
}
.notice {
margin-top: 67px;
.notice__title {
font-size: 16px;
color: #424242;
margin-bottom: 28px;
line-height: 100%;
}
.notice__content{
font-size: 16px;
line-height: 34px;
color: rgba(137, 137, 137, 1);
margin-left: 46px;
span{
color: rgba(102, 102, 102, 1);
font-weight: bold;
}
}
}
</style>
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location, onResolve, onReject) {
if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
return originalPush.call(this, location).catch(err => err)
}
const routes = [
{ path: '*', redirect: '/index' },
{ path: '/', redirect: '/course/learn' }
]
const router = new VueRouter({
mode: 'history',
routes
})
export default router
import Vue from 'vue'
import Vuex from 'vuex'
import { getUser, logout, getIsVip, createGuestUser, getPermissions } from '@/api/account'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
user: {},
isVip: false,
isLogin: false,
isIos: /iphone|ipad|ipod/i.test(navigator.userAgent),
isAndroid: /android/i.test(navigator.userAgent),
isWeapp: /miniProgram/.test(navigator.userAgent),
guestUser: { user_id: '', student_id: '' },
permissions: []
},
mutations: {
setUser(state, user) {
state.user = user
},
setIsWeapp(state, isWeapp) {
state.isWeapp = isWeapp
},
setIsLogin(state, isLogin) {
state.isLogin = isLogin
},
setIsVip(state, isVip) {
state.isVip = isVip
},
setGuestUser(state, user) {
state.guestUser = user
window.localStorage.setItem('guestUser', JSON.stringify(user))
},
setPermissions(state, permissions) {
state.permissions = permissions
}
},
actions: {
getUser({ commit }) {
getUser().then(response => {
commit('setUser', response)
})
},
// 退出登录
logout({ commit }) {
return logout().then(response => {
commit('setUser', {})
commit('setIsLogin', false)
return response
})
},
// 检测登录状态
async checkLogin({ commit }) {
const isLogin = await getUser()
.then(response => {
commit('setUser', response)
return true
})
.catch(() => {
commit('setUser', {})
return false
})
commit('setIsLogin', isLogin)
return isLogin
},
// 检测是否付费
async checkIsVip({ commit, state }) {
if (!state.isVip) {
await getIsVip().then(response => commit('setIsVip', response.is_vip))
}
return state.isVip
},
// 创建游客用户
async createGuestUser({ commit, state }) {
const { user_id: userId, student_id: studentId } = state.guestUser
if (!userId || !studentId) {
await createGuestUser().then(response => commit('setGuestUser', response))
}
return state.guestUser
},
// 加载本地游客信息
loadGuestUser({ commit, state }) {
const localGuestUser = window.localStorage.getItem('guestUser')
let guestUser = { user_id: '', student_id: '' }
if (localGuestUser) {
try {
guestUser = JSON.parse(localGuestUser)
} catch (error) {
console.log(error)
}
}
commit('setGuestUser', guestUser)
},
// 获取所有权限列表
getPermissions({ commit }) {
getPermissions({ type: 1 }).then(res => {
if (res.data && res.data.items) {
commit('setPermissions', res.data.items)
}
})
}
}
})
store.dispatch('getPermissions')
// 加载本地游客用户
// store.dispatch('loadGuestUser')
// 检测是否付费
// store.dispatch('checkIsVip')
export default store
import axios from 'axios'
import qs from 'qs'
import { Message } from 'element-ui'
import router from '@/router'
const httpRequest = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
timeout: 60000,
withCredentials: true,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
tenant: 'x1'
}
})
// 请求拦截
httpRequest.interceptors.request.use(
function (config) {
if (config.headers['Content-Type'] === 'application/x-www-form-urlencoded') {
config.data = qs.stringify(config.data)
}
return config
},
function (error) {
return Promise.reject(error)
}
)
// 响应拦截
httpRequest.interceptors.response.use(
function (response) {
const { data } = response
if (data.code) {
Message.error(data.msg || data.message)
return Promise.reject(data)
}
return data
},
function (error) {
if (error.response) {
const { status, message, code } = error.response.data
// 未登录
if (status === 403) {
window.location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(window.location.href)}`
} else if (status === 400 && code === 401) {
// router.push('/role')
} else if (status === 402) {
router.push('/index')
} else {
Message.error(message || error.response.data)
}
return Promise.reject(error.response)
} else {
Message.error(error)
}
return Promise.reject(error)
}
)
export default httpRequest
import store from '@/store'
export default async function (to, from, next) {
const { user_id: userId, student_id: studentId } = to.query
if (userId && studentId) {
store.commit('setGuestUser', { user_id: userId, student_id: studentId })
}
// 创建游客用户
// await store.dispatch('createGuestUser')
// 登录白名单
const whiteList = ['/index']
if (whiteList.includes(to.path)) {
next()
return
}
const isLogin = store.state.isLogin || (await store.dispatch('checkLogin'))
if (to.meta.requiredLogin && !isLogin) {
window.location.href = `${import.meta.env.VITE_LOGIN_URL}?rd=${encodeURIComponent(window.location.href)}`
return
}
next()
}
import fs from 'fs'
import path from 'path'
import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
import eslint from '@rollup/plugin-eslint'
export default defineConfig({
base: process.env.BUILD_ENV === 'prod' ? 'https://webapp-pub.ezijing.com/website/prod/x-learn/' : '/',
plugins: [eslint({ include: '**/*.+(vue|js|jsx|ts|tsx)' }), createVuePlugin()],
server: {
open: true,
host: 'dev.ezijing.com',
https: {
key: fs.readFileSync(path.join(__dirname, './https/dev.ezijing.com.key')),
cert: fs.readFileSync(path.join(__dirname, './https/dev.ezijing.com.pem'))
},
proxy: {
// '/api/fd': 'http://localhost-financial-api.ezijing.com'
'/api/fd': {
target: 'http://localhost-financial-api.ezijing.com',
changeOrigin: true,
rewrite: path => path.replace(/^\/api\/zy/, '')
}
}
},
resolve: {
alias: [
{
find: '@',
replacement: path.resolve(__dirname, 'src')
}
]
},
css: {
// 禁用SASS警告提醒
preprocessorOptions: { scss: { quietDeps: true } }
}
})
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论