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

bug fixes

上级 9adacf96
......@@ -21,6 +21,7 @@
"lodash-es": "^4.17.21",
"pinia": "^2.0.21",
"qs": "^6.11.0",
"ua-parser-js": "^1.0.2",
"video.js": "^7.20.2",
"vue": "^3.2.38",
"vue-router": "^4.1.5"
......@@ -32,6 +33,7 @@
"@types/file-saver": "^2.0.5",
"@types/node": "^16.11.56",
"@types/qs": "^6.9.7",
"@types/ua-parser-js": "^0.7.36",
"@types/video.js": "^7.3.46",
"@vitejs/plugin-vue": "^3.0.3",
"@vue/eslint-config-typescript": "^11.0.0",
......@@ -400,6 +402,12 @@
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
},
"node_modules/@types/ua-parser-js": {
"version": "0.7.36",
"resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz",
"integrity": "sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==",
"dev": true
},
"node_modules/@types/video.js": {
"version": "7.3.46",
"resolved": "https://registry.npmmirror.com/@types/video.js/-/video.js-7.3.46.tgz",
......@@ -4163,6 +4171,24 @@
"node": ">=4.2.0"
}
},
"node_modules/ua-parser-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.2.tgz",
"integrity": "sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/ua-parser-js"
},
{
"type": "paypal",
"url": "https://paypal.me/faisalman"
}
],
"engines": {
"node": "*"
}
},
"node_modules/ufo": {
"version": "0.8.5",
"resolved": "https://registry.npmmirror.com/ufo/-/ufo-0.8.5.tgz",
......@@ -5083,6 +5109,12 @@
"integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==",
"dev": true
},
"@types/ua-parser-js": {
"version": "0.7.36",
"resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz",
"integrity": "sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==",
"dev": true
},
"@types/video.js": {
"version": "7.3.46",
"resolved": "https://registry.npmmirror.com/@types/video.js/-/video.js-7.3.46.tgz",
......@@ -7945,6 +7977,11 @@
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
"devOptional": true
},
"ua-parser-js": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.2.tgz",
"integrity": "sha512-00y/AXhx0/SsnI51fTc0rLRmafiGOM4/O+ny10Ps7f+j/b8p/ZY11ytMgznXkOVo4GQ+KwQG5UQLkLGirsACRg=="
},
"ufo": {
"version": "0.8.5",
"resolved": "https://registry.npmmirror.com/ufo/-/ufo-0.8.5.tgz",
......
......@@ -26,6 +26,7 @@
"lodash-es": "^4.17.21",
"pinia": "^2.0.21",
"qs": "^6.11.0",
"ua-parser-js": "^1.0.2",
"video.js": "^7.20.2",
"vue": "^3.2.38",
"vue-router": "^4.1.5"
......@@ -37,6 +38,7 @@
"@types/file-saver": "^2.0.5",
"@types/node": "^16.11.56",
"@types/qs": "^6.9.7",
"@types/ua-parser-js": "^0.7.36",
"@types/video.js": "^7.3.46",
"@vitejs/plugin-vue": "^3.0.3",
"@vue/eslint-config-typescript": "^11.0.0",
......
import httpRequest from '@/utils/axios'
// https://gitlab.ezijing.com/root/api-documents/-/tree/master/%E6%95%B0%E6%8D%AE%E5%9F%8B%E7%82%B9%E6%9C%8D%E5%8A%A1/api
// 获取Key
export function getPageKey() {
return httpRequest.get('/api/meta/api/v1/client/page-key')
}
/**
*
* @param data.event video_event | courseware_event | lesson_plan_event | file_event | suggestion_event | paper_event
* @param data.action stu_watch_action | stu_watch_courseware_action | stu_watch_lesson_plan_action | stu_download_file_action | stu_suggestion_action | stu_submit_paper_action
* @param data.data
*/
export function setMetaInfo(data: { event: string; action: string; data: string }) {
return httpRequest.post('/api/meta/api/v1/client/set-meta-info', data)
}
......@@ -32,13 +32,12 @@ const dataList = ref<any[]>([])
const page = reactive({ total: 0, size: props.limit, currentPage: 1 })
const params = reactive({ ...props.remote?.params })
watch(
() => props.data,
list => {
dataList.value = list || []
},
{ immediate: true }
)
watchEffect(() => {
Object.assign(params, props.remote?.params)
})
watchEffect(() => {
dataList.value = props.data || []
})
// 获取数据
const fetchList = (isReset = false) => {
......
import parser from 'ua-parser-js'
import { useUserStore } from '@/stores/user'
import { getPageKey, setMetaInfo } from '@/api/log'
const userStore = useUserStore()
export function useLog(options?: { hasKey?: boolean }) {
let key = ''
// UA
const userAgent = window.navigator.userAgent
// 解析后的UA
const parsedUserAgent = parser(userAgent)
// 调用时间,一般是页面加载时间
const startTime = Math.floor(Date.now() / 1000)
// 获取Key
const getKey = async () => {
const res = await getPageKey()
key = res.data.key
return key
}
// 上送日志
const upload = async (data: { event: string; action: string; data: Record<string, any> }) => {
if (options?.hasKey && !key) {
await getKey()
}
const OSName = parsedUserAgent.os.name || ''
const defaultData = {
key,
sso_id: userStore.user?.id,
student_id: userStore.user?.id,
open_browser_time: startTime,
close_browser_time: Math.floor(Date.now() / 1000),
device: ['iOS', 'Android'].includes(OSName) ? OSName : 'PC'
}
return setMetaInfo(Object.assign({}, data, { data: JSON.stringify(Object.assign(defaultData, data.data)) }))
}
return { userAgent, parsedUserAgent, key, upload, getKey }
}
......@@ -85,7 +85,7 @@ function handleUploadSuccess(file: any) {
form.name = file.name.split('.').shift()
form.size = (file.size / 1024 / 1024).toString()
form.url = file.raw.url
form.type = file.raw.type
form.type = file.raw.type || 'unknown'
}
// 提交
function handleSubmit() {
......
......@@ -3,6 +3,7 @@ import { getFilterList } from '../api'
export interface FilterItem {
id: string
name: string
specialty_id?: string
}
// 课程
const courses = ref<FilterItem[]>([])
......
......@@ -9,12 +9,28 @@ const DiscussDialog = defineAsyncComponent(() => import('../components/DiscussDi
const { courses, experiments, specialties, classes } = useFilterList()
const appList = $ref<InstanceType<typeof AppList> | null>(null)
const params = reactive({ student_name: '', course_id: '', experiment_id: '', specialty_id: '', class_id: '' })
const classList = $computed(() => {
const specialty = specialties.value.find(item => item.id === params.specialty_id)
if (specialty) {
return classes.value.filter(item => item.specialty_id === specialty.id)
}
return classes.value
})
// 列表配置
const listOptions = $computed(() => {
return {
remote: {
httpRequest: getDiscussList,
params: { student_name: '', course_id: '', experiment_id: '', specialty_id: '', class_id: '' }
params,
beforeRequest(requestParams: any) {
if (params.specialty_id !== requestParams.specialty_id) {
requestParams.class_id = ''
}
params.specialty_id = requestParams.specialty_id || ''
return requestParams
}
},
filterForm: { labelWidth: 100 },
filters: [
......@@ -50,7 +66,7 @@ const listOptions = $computed(() => {
prop: 'class_id',
label: '班级',
placeholder: '请选择班级',
options: classes.value,
options: classList,
labelKey: 'name',
valueKey: 'id'
},
......
<script setup lang="ts">
import { Upload } from '@element-plus/icons-vue'
import { useFileDialog } from '@vueuse/core'
import { uploadCheckExperimentRecord } from '../api'
const emit = defineEmits<{
(e: 'update'): void
}>()
// 批量导入
const { files, open } = useFileDialog()
function handleImport() {
open({
accept: '.csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel',
multiple: false
})
}
watchEffect(() => {
if (!files.value?.length) return
const [file] = files.value
uploadCheckExperimentRecord({ file }).then(() => {
emit('update')
})
})
</script>
<template>
<el-dialog
title="批量导入"
:close-on-click-modal="false"
width="400px"
@update:modelValue="$emit('update:modelValue')"
>
<div class="box">
<el-button type="primary" round :icon="Upload" @click="handleImport">本地上传</el-button>
<p>
<a
href="https://webapp-pub.ezijing.com/project/saas-lab/%E5%AE%9E%E9%AA%8C%E6%88%90%E7%BB%A9%E5%AF%BC%E5%85%A5%E6%A8%A1%E6%9D%BF.xlsx"
download
>下载模板</a
>
</p>
</div>
</el-dialog>
</template>
<style lang="scss" scoped>
.box {
padding: 20px 0;
display: flex;
align-items: center;
justify-content: center;
.el-button {
width: 220px;
}
p {
color: #999;
margin-left: 20px;
}
}
</style>
......@@ -3,6 +3,7 @@ import { getFilterList } from '../api'
export interface FilterItem {
id: string
name: string
specialty_id?: string
}
// 课程
const courses = ref<FilterItem[]>([])
......
......@@ -2,10 +2,11 @@
import type { RecordItem } from '../types'
import { Upload, Promotion } from '@element-plus/icons-vue'
import AppList from '@/components/base/AppList.vue'
import { getExperimentRecordList, uploadCheckExperimentRecord } from '../api'
import { getExperimentRecordList } from '../api'
import { useFilterList } from '../composables/useFilterList'
import { useFileDialog } from '@vueuse/core'
const ScoreDialog = defineAsyncComponent(() => import('../components/ScoreDialog.vue'))
const ImportDialog = defineAsyncComponent(() => import('../components/ImportDialog.vue'))
const { courses, experiments, specialties, classes } = useFilterList()
......@@ -15,17 +16,33 @@ const route = useRoute()
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listOptions = $computed(() => {
return {
remote: {
httpRequest: getExperimentRecordList,
params: {
const params = reactive({
student_name: '',
course_id: '',
experiment_id: (route.query.experiment_id as string) || '',
specialty_id: '',
class_id: ''
})
const classList = $computed(() => {
const specialty = specialties.value.find(item => item.id === params.specialty_id)
if (specialty) {
return classes.value.filter(item => item.specialty_id === specialty.id)
}
return classes.value
})
// 列表配置
const listOptions = $computed(() => {
return {
remote: {
httpRequest: getExperimentRecordList,
params,
beforeRequest(requestParams: any) {
if (params.specialty_id !== requestParams.specialty_id) {
requestParams.class_id = ''
}
params.specialty_id = requestParams.specialty_id || ''
return requestParams
}
},
filterForm: { labelWidth: 100 },
......@@ -62,7 +79,7 @@ const listOptions = $computed(() => {
prop: 'class_id',
label: '班级',
placeholder: '请选择班级',
options: classes.value,
options: classList,
labelKey: 'name',
valueKey: 'id'
},
......@@ -82,23 +99,7 @@ const listOptions = $computed(() => {
]
}
})
// 批量导入
const { files, open } = useFileDialog()
function handleImport() {
open({
accept: '.csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel',
multiple: false
})
}
watchEffect(() => {
if (!files.value?.length) return
const [file] = files.value
uploadCheckExperimentRecord({ file }).then(() => {
appList?.refetch()
})
})
const importVisible = $ref(false)
let dialogVisible = $ref(false)
const rowData = ref<RecordItem>()
// 评分
......@@ -116,7 +117,12 @@ function onUpdateSuccess() {
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
<el-row justify="space-between">
<el-button type="primary" round :icon="Upload" @click="handleImport" v-permission="'v1-teacher-record-upload'"
<el-button
type="primary"
round
:icon="Upload"
@click="importVisible = true"
v-permission="'v1-teacher-record-upload'"
>批量导入</el-button
>
<a :href="LAB_URL" target="_blank">
......@@ -142,12 +148,15 @@ function onUpdateSuccess() {
</template>
</AppList>
</AppCard>
<!-- 评分 -->
<ScoreDialog
v-model="dialogVisible"
:data="rowData"
@update="onUpdateSuccess"
v-if="dialogVisible && rowData"
></ScoreDialog>
<!-- 批量导入 -->
<ImportDialog v-model="importVisible" @update="onUpdateSuccess" v-if="importVisible"></ImportDialog>
</template>
<style lang="scss" scoped>
......
......@@ -26,7 +26,7 @@ function handleChange(id: string, type: number) {
<router-link to="/admin/lab/book" class="link1"></router-link>
<router-link to="/admin/lab/record" class="link2"></router-link>
<router-link to="/admin/lab/video" class="link3"></router-link>
<router-link to="/" class="link4"></router-link>
<router-link to="/admin/lab/discuss" class="link4"></router-link>
</div>
<div class="select-group">
<el-select size="large" placeholder="实验指导书" @change="handleChange($event, 1)">
......
......@@ -3,8 +3,12 @@ import type { ExperimentBookType } from '../types'
import Preview from '@/components/Preview.vue'
import { getExperimentBook } from '../api'
import { useLog } from '@/composables/useLog'
const log = useLog()
interface Props {
experiment_id?: string
course_id?: string
}
const props = defineProps<Props>()
......@@ -13,6 +17,16 @@ function fetchInfo() {
if (!props.experiment_id) return
getExperimentBook({ experiment_id: props.experiment_id }).then(res => {
detail = res.data.detail
// 日志上送
log.upload({
event: 'file_event',
action: 'experiment_book_stu_watch_action',
data: {
experiment_id: props.experiment_id,
course_id: props.course_id,
book_id: detail.id
}
})
})
}
watchEffect(() => {
......
......@@ -12,16 +12,10 @@ const score = $computed<number>(() => {
</script>
<template>
<el-dialog title="实验成绩详情" :close-on-click-modal="false">
<el-dialog title="实验成绩详情" width="600px" :close-on-click-modal="false">
<el-form label-width="120px" label-suffix=":" v-if="detail">
<el-row>
<el-col :span="12">
<el-form-item label="实验名称">{{ detail.experiment.name }}</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="实验课程名称">{{ detail.course.name }}</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="学生姓名">{{ detail.student.name }}</el-form-item>
......@@ -38,61 +32,29 @@ const score = $computed<number>(() => {
<el-form-item label="所属班级">{{ detail.student.specialty.name }}</el-form-item>
</el-col>
</el-row>
<el-form-item label="实验报告文件">
<div v-if="detail.file">
<a :href="detail.file.url" target="_blank" class="file-item">
<el-icon><Document /></el-icon>{{ detail.file.name }}
</a>
</div>
</el-form-item>
<el-form-item label="实验成绩" class="form-item-score">
<el-form ref="formRef" hide-required-asterisk inline label-position="top" style="padding: 5px 0 20px">
<el-form-item label="实验操作" prop="operate">
<el-input-number
:min="1"
:max="100"
:controls="false"
step-strictly
disabled
v-model="detail.score_details.operate"
/>
<el-form-item label="实验操作">
<el-input-number :controls="false" disabled v-model="detail.score_details.operate" />
</el-form-item>
<el-form-item label="实验结果" prop="result">
<el-input-number
:min="1"
:max="100"
:controls="false"
step-strictly
disabled
v-model="detail.score_details.result"
/>
<el-form-item label="实验结果">
<el-input-number :controls="false" disabled v-model="detail.score_details.result" />
</el-form-item>
<el-form-item label="实验报告" prop="file">
<el-input-number
:min="1"
:max="100"
:controls="false"
step-strictly
disabled
v-model="detail.score_details.file"
/>
<el-form-item label="实验报告">
<el-input-number :controls="false" disabled v-model="detail.score_details.file" />
</el-form-item>
<el-form-item label="综合实验成绩">
<el-input-number :min="0" :max="100" :controls="false" disabled v-model="score" />
<el-form-item label="综合实验成绩" style="width: 364px">
<el-input-number :controls="false" disabled v-model="score" style="width: 100%" />
</el-form-item>
</el-form>
</el-form-item>
<el-form-item label="实验报告文件">
<div v-if="detail.file">
<a :href="detail.file.url" target="_blank" class="file-item">
<el-icon><Document /></el-icon>{{ detail.file.name }}
</a>
</div>
</el-form-item>
<!-- <el-form-item label="实验过程截图">
<ul class="picture-list">
<li v-for="item in pictures" :key="item.url">
<p class="t1">
<a :href="item.url" target="_blank">{{ item.name }}</a>
</p>
<p class="t2">截图时间:{{ item.upload_time }}</p>
</li>
</ul>
</el-form-item> -->
</el-form>
</el-dialog>
</template>
......@@ -123,9 +85,6 @@ const score = $computed<number>(() => {
}
}
.form-item-score {
padding-top: 10px;
background-color: #f8f9fb;
border-radius: 16px;
:deep(.el-form-item__label) {
text-align: center;
}
......
......@@ -5,6 +5,7 @@ import { getExperimentVideoList } from '../api'
interface Props {
experiment_id?: string
course_id?: string
}
const props = defineProps<Props>()
......@@ -27,7 +28,13 @@ const isEmpty = $computed(() => {
<template>
<el-empty description="暂无数据" v-if="isEmpty" />
<template v-else>
<VideoItem v-for="item in list" :key="item.id" :data="item"></VideoItem>
<VideoItem
v-for="item in list"
:key="item.id"
:data="item"
:course_id="course_id"
:experiment_id="experiment_id"
></VideoItem>
</template>
</template>
......
......@@ -2,7 +2,13 @@
import type { ExperimentVideoType, PlayInfo } from '../types'
import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue'
import { getExperimentVideoPlayInfo } from '../api'
import { useLog } from '@/composables/useLog'
const log = useLog()
interface Props {
experiment_id?: string
course_id?: string
data: ExperimentVideoType
}
const props = defineProps<Props>()
......@@ -10,6 +16,16 @@ let playList = $ref<PlayInfo[]>([])
function fetchInfo() {
getExperimentVideoPlayInfo({ source_id: props.data.source_id }).then(res => {
playList = res.data.play_info_list
// 日志上送
log.upload({
event: 'file_event',
action: 'experiment_book_stu_watch_action',
data: {
experiment_id: props.experiment_id,
course_id: props.course_id,
book_id: props.data.id
}
})
})
}
const playUrl = $computed(() => {
......
......@@ -160,10 +160,10 @@ function handleSubmit() {
</el-form>
<el-tabs type="border-card" stretch>
<el-tab-pane label="实训指导" lazy>
<Book :experiment_id="form.experiment_id"></Book>
<Book :course_id="form.course_id" :experiment_id="form.experiment_id"></Book>
</el-tab-pane>
<el-tab-pane label="操作视频" lazy>
<Video :experiment_id="form.experiment_id"></Video>
<Video :course_id="form.course_id" :experiment_id="form.experiment_id"></Video>
</el-tab-pane>
<el-tab-pane label="讨论交流" lazy>
<Discuss :experiment_id="form.experiment_id"></Discuss>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论