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

chore: update

上级 43bde8c6
......@@ -42,9 +42,6 @@
"isReactive": true,
"isReadonly": true,
"isRef": true,
"logicAnd": true,
"logicNot": true,
"logicOr": true,
"makeDestructurable": true,
"markRaw": true,
"nextTick": true,
......@@ -105,6 +102,14 @@
"unrefElement": true,
"until": true,
"useActiveElement": true,
"useArrayEvery": true,
"useArrayFilter": true,
"useArrayFind": true,
"useArrayFindIndex": true,
"useArrayJoin": true,
"useArrayMap": true,
"useArrayReduce": true,
"useArraySome": true,
"useAsyncQueue": true,
"useAsyncState": true,
"useAttrs": true,
......@@ -115,7 +120,6 @@
"useBroadcastChannel": true,
"useBrowserLocation": true,
"useCached": true,
"useClamp": true,
"useClipboard": true,
"useColorMode": true,
"useConfirmDialog": true,
......@@ -191,6 +195,7 @@
"usePreferredColorScheme": true,
"usePreferredDark": true,
"usePreferredLanguages": true,
"usePreferredReducedMotion": true,
"useRafFn": true,
"useRefHistory": true,
"useResizeObserver": true,
......@@ -210,8 +215,10 @@
"useStorage": true,
"useStorageAsync": true,
"useStyleTag": true,
"useSupported": true,
"useSwipe": true,
"useTemplateRefsList": true,
"useTextDirection": true,
"useTextSelection": true,
"useTextareaAutosize": true,
"useThrottle": true,
......@@ -223,6 +230,8 @@
"useTimeoutPoll": true,
"useTimestamp": true,
"useTitle": true,
"useToNumber": true,
"useToString": true,
"useToggle": true,
"useTransition": true,
"useUrlSearchParams": true,
......
......@@ -43,9 +43,6 @@ declare global {
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const logicAnd: typeof import('@vueuse/core')['logicAnd']
const logicNot: typeof import('@vueuse/core')['logicNot']
const logicOr: typeof import('@vueuse/core')['logicOr']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
......@@ -106,6 +103,14 @@ declare global {
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
const useArraySome: typeof import('@vueuse/core')['useArraySome']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
......@@ -116,7 +121,6 @@ declare global {
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useCached: typeof import('@vueuse/core')['useCached']
const useClamp: typeof import('@vueuse/core')['useClamp']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
......@@ -192,6 +196,7 @@ declare global {
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
......@@ -211,8 +216,10 @@ declare global {
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSupported: typeof import('@vueuse/core')['useSupported']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
......@@ -224,6 +231,8 @@ declare global {
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToNumber: typeof import('@vueuse/core')['useToNumber']
const useToString: typeof import('@vueuse/core')['useToString']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
......
差异被折叠。
......@@ -17,10 +17,12 @@
"@vueuse/core": "^9.1.0",
"axios": "^0.27.2",
"blueimp-md5": "^2.19.0",
"dayjs": "^1.11.5",
"element-plus": "^2.2.12",
"lodash-es": "^4.17.21",
"pinia": "^2.0.17",
"qs": "^6.11.0",
"video.js": "^7.20.2",
"vue": "^3.2.37",
"vue-router": "^4.1.3"
},
......@@ -29,6 +31,7 @@
"@types/blueimp-md5": "^2.18.0",
"@types/node": "^16.11.45",
"@types/qs": "^6.9.7",
"@types/video.js": "^7.3.45",
"@vitejs/plugin-vue": "^3.0.1",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
......
......@@ -12,7 +12,7 @@ interface Props {
}
const props = withDefaults(defineProps<Props>(), {
prefix: 'upload/saas-learn/'
prefix: 'upload/saas-lab/'
})
const emit = defineEmits(['update:modelValue', 'success'])
......@@ -117,7 +117,7 @@ const handlePreview: UploadProps['onPreview'] = uploadFile => {
<el-icon><Plus /></el-icon>
</template>
<template v-else>
<el-button round>本地文件</el-button>
<el-button type="primary" plain round>本地文件</el-button>
</template>
</template>
<div class="avatar-uploader" v-else>
......
<script lang="ts">
const DEFAULT_OPTIONS = {
controls: true,
autoplay: false,
controlBar: {
pictureInPictureToggle: false
},
// fluid: true,
responsive: true
// playbackRates: [0.5, 1, 1.5, 2]
}
const DEFAULT_EVENTS = [
'abort',
'canplay',
'canplaythrough',
'durationchange',
'emptied',
'ended',
'error',
'loadeddata',
'loadedmetadata',
'pause',
'play',
'playing',
'progress',
'ratechange',
'resize',
'seeked',
'seeking',
'stalled',
'suspend',
'timeupdate',
'volumechange',
'waiting'
]
</script>
<script setup lang="ts">
import videojs from 'video.js'
import type { VideoJsPlayerOptions, VideoJsPlayer } from 'video.js'
import 'video.js/dist/video-js.css'
interface Props {
src?: string | { src: string; type?: string }
options?: VideoJsPlayerOptions
}
const props = defineProps<Props>()
const emit = defineEmits(['ready', ...DEFAULT_EVENTS])
let player = $ref<VideoJsPlayer | null>()
const videoRef = $ref<HTMLVideoElement>()
const videoOptions = $computed<VideoJsPlayerOptions>(() => {
return Object.assign({}, DEFAULT_OPTIONS, props.options)
})
watch(
() => props.src,
src => {
src && changeSrc(src)
},
{ deep: true, immediate: true }
)
// 初始化播放器
function initPlayer() {
if (!videoRef) return
if (player) {
player.dispose()
player = null
}
player = videojs(videoRef, videoOptions, function onPlayerReady() {
props.src && changeSrc(props.src)
// 注册事件
DEFAULT_EVENTS.forEach(eventName => {
this.on(eventName, (...arg) => {
// console.log(eventName, ...arg)
emit(eventName, ...arg)
})
})
emit('ready', this)
})
return player
}
function changeSrc(src: string | { src: string; type?: string }) {
if (!player || !src) return
// if (!player.paused()) {
// console.log(2)
// player.pause()
// }
player.src(src)
// player.load()
// player.play()
}
onMounted(() => {
initPlayer()
})
onUnmounted(() => {
player && player.dispose()
})
</script>
<template>
<video class="video-js vjs-default-skin vjs-big-play-centered" ref="videoRef"></video>
</template>
<style>
.video-js {
font-size: 12px;
}
</style>
......@@ -36,3 +36,23 @@ export function addExperimentDiscuss(data: { experiment_id: string; title: strin
export function addExperimentDiscussComment(data: { discussion_id: string; content: string }) {
return httpRequest.post('/api/student/v1/student/experiment-topic/comment', data)
}
// 获取实验记录
export function getExperimentRecord(params: { experiment_id: string }) {
return httpRequest.get('/api/student/v1/student/experiment-record/detail', { params })
}
// 截图
export function uploadExperimentPicture(data: { experiment_id: string; pictures: string }) {
return httpRequest.post('/api/student/v1/student/experiment-record/upload-pictures', data)
}
// 上传实验报告
export function uploadExperimentReport(data: { experiment_id: string; file: string }) {
return httpRequest.post('/api/student/v1/student/experiment-record/upload-report', data)
}
// 提交实验记录
export function submitExperimentRecord(data: { experiment_id: string }) {
return httpRequest.post('/api/student/v1/student/experiment-record/submit', data)
}
......@@ -10,7 +10,7 @@ interface Props {
experiment_id?: string
}
const props = defineProps<Props>()
const params = reactive({ tag: 3, page: 0, 'per-page': 10 })
const params = reactive({ tag: 1, page: 0, 'per-page': 10 })
let list = $ref<ExperimentDiscussType[]>([])
let hasMore = $ref(false)
let isLoading = $ref(false)
......@@ -26,9 +26,7 @@ function fetchInfo() {
isLoading = false
})
}
onMounted(() => {
fetchInfo()
})
watch(() => props.experiment_id, handleRefetch, { immediate: true })
const isEmpty = $computed(() => {
return !props.experiment_id || !list.length
......
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
// import { ElMessage } from 'element-plus'
import type { ExperimentRecord } from '../types'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
import { uploadExperimentReport } from '../api'
interface Props {
data?: any
experiment_id: string
}
const props = defineProps<Props>()
defineEmits<{
const detail = $ref<ExperimentRecord>(inject('detail'))
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
const formRef = $ref<FormInstance>()
const form = reactive({ files: [] })
const form = reactive<any>({ files: [] })
watchEffect(() => {
Object.assign(form, props.data)
if (detail?.file) {
form.files = [detail.file]
}
})
const rules = ref<FormRules>({
files: [{ required: true, message: '请输入话题描述', trigger: 'blur' }]
files: [{ required: true, message: '请上传实验报告文件', trigger: 'blur' }]
})
// 提交
function handleSubmit() {
......@@ -27,16 +34,25 @@ function handleSubmit() {
}
// 修改
const update = () => {
// submitSuggestion(form).then(() => {
// ElMessage({ message: '提交成功', type: 'success' })
// emit('update')
// formRef?.resetFields()
// })
const [file] = form.files
uploadExperimentReport({
experiment_id: props.experiment_id,
file: JSON.stringify({ name: file.name, url: file.url, upload_time: dayjs().format('YYYY-MM-DD HH:mm:ss') })
}).then(() => {
ElMessage({ message: '上传成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
</script>
<template>
<el-dialog title="上传实验报告" :close-on-click-modal="false" width="600px">
<el-dialog
title="上传实验报告"
:close-on-click-modal="false"
width="600px"
@update:modelValue="$emit('update:modelValue')"
>
<el-form ref="formRef" :model="form" :rules="rules">
<el-form-item label="实验报告文件" prop="files">
<AppUpload v-model="form.files">
......
<script setup lang="ts">
import type { ExperimentRecord } from '../types'
interface Props {
experiment_id?: string
}
defineProps<Props>()
const props = defineProps<Props>()
const detail = $ref<ExperimentRecord>(inject('detail'))
const isEmpty = $computed(() => {
return !props.experiment_id || !detail
})
</script>
<template>
<div>result</div>
<el-empty description="暂无数据" v-if="isEmpty" />
<template v-else>
<h2>我的成绩</h2>
<h2>实验过程</h2>
<ul class="picture-list">
<li v-for="item in detail.pictures" :key="item.url">
<img :src="item.url" />
<p>截图时间:{{ item.upload_time }}</p>
</li>
</ul>
</template>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.picture-list {
li {
position: relative;
height: 200px;
margin: 20px 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
p {
position: absolute;
left: 0;
right: 0;
bottom: 0;
padding: 0 10px;
font-size: 14px;
color: #fff;
line-height: 30px;
text-align: right;
background-color: rgba(0, 0, 0, 0.5);
}
}
}
</style>
<script setup lang="ts">
import type { ExperimentVideoType, PlayInfo } from '../types'
import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue'
import { getExperimentVideoPlayInfo } from '../api'
interface Props {
data: ExperimentVideoType
......@@ -11,7 +12,9 @@ function fetchInfo() {
playList = res.data.play_info_list
})
}
console.log(playList)
const playUrl = $computed(() => {
return playList[0]?.PlayURL || ''
})
onMounted(() => {
fetchInfo()
})
......@@ -19,16 +22,22 @@ onMounted(() => {
<template>
<div class="video-item">
<h2>{{ data.name }}</h2>
<img :src="data.cover" />
<!-- <img :src="data.cover" /> -->
<AppVideoPlayer
:options="{ sources: [{ src: playUrl, type: 'application/x-mpegURL' }] }"
style="width: 100%"
v-if="playUrl"
></AppVideoPlayer>
</div>
</template>
<style lang="scss" scoped>
.video-item {
h2 {
font-size: 16px;
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 10px;
padding: 10px 0;
text-align: center;
}
img {
......
......@@ -83,3 +83,45 @@ export interface UserType {
real_name: string
username: string
}
export interface ExperimentRecord {
experiment_id: string
student_id: string
commit_time: string
pictures: ExperimentRecordFile[]
file: ExperimentRecordFile
checker_id: string
check_time: string
score_details: string
score: string
status: 0 | 1 | 2
checker_user: UserType
experiment: {
id: string
name: string
score: number
length: number
}
course: CourseType
student: ExperimentRecordStudent
}
export interface ExperimentRecordFile {
url: string
name: string
upload_time: string
}
export interface ExperimentRecordStudent {
id: string
name: string
specialty: {
id: string
name: string
}
classes: [
{
id: string
name: string
}
]
}
<script setup lang="ts">
import type { CourseType } from '../types'
import type { CourseType, ExperimentRecord } from '../types'
import { HomeFilled, Select, UploadFilled, FullScreen } from '@element-plus/icons-vue'
import { useGetCourseList } from '../composables/useGetCourseList'
import { upload } from '@/utils/upload'
import { getExperimentRecord, uploadExperimentPicture, submitExperimentRecord } from '../api'
import dayjs from 'dayjs'
const Book = defineAsyncComponent(() => import('../components/Book.vue'))
const Video = defineAsyncComponent(() => import('../components/Video.vue'))
......@@ -20,6 +23,19 @@ const { courses } = useGetCourseList()
const experimentList = $computed(() => {
return form.course?.experiments || []
})
let detail = $ref<ExperimentRecord>()
provide('detail', $$(detail))
function fetchInfo() {
if (!form.experiment_id) return
getExperimentRecord({ experiment_id: form.experiment_id }).then(res => {
detail = Array.isArray(res.data.data) ? undefined : res.data.data
})
}
watchEffect(() => {
fetchInfo()
})
// 右侧
const LAB_URL = import.meta.env.VITE_LAB_URL
let iframeKey = $ref(Date.now())
......@@ -30,7 +46,13 @@ function handleBackHome() {
const reportDialogVisible = $ref(false)
// 是否已经提交
const submitted = $ref(false)
const submitted = $computed(() => {
return detail ? detail.status !== 0 : false
})
// 是否禁用
const disabled = $computed(() => {
return submitted || !form.experiment_id
})
const iframeRef = $ref<HTMLIFrameElement>()
let screenshotLoading = $ref(false)
......@@ -48,11 +70,9 @@ function handleCapture() {
function handleCaptureCallback(event: MessageEvent) {
const { data } = event
if (data.action === 'screenshot' && data.timestamp === screenshotTimestamp) {
console.log(data)
const img = new Image()
img.src = data.dataURL
document.body.appendChild(img)
screenshotLoading = false
upload(data.blob).then(url => {
uploadPicture(url)
})
}
}
onMounted(() => {
......@@ -61,6 +81,24 @@ onMounted(() => {
onUnmounted(() => {
window.removeEventListener('message', handleCaptureCallback, false)
})
// 上传截图
function uploadPicture(url: string) {
const pictures = detail?.pictures || []
pictures.unshift({ url, name: 'screenshot.png', upload_time: dayjs().format('YYYY-MM-DD HH:mm:ss') })
uploadExperimentPicture({ experiment_id: form.experiment_id, pictures: JSON.stringify(pictures) }).then(() => {
screenshotLoading = false
if (!detail) {
fetchInfo()
}
})
}
// 提交实验
function handleSubmit() {
submitExperimentRecord({ experiment_id: form.experiment_id }).then(() => {
fetchInfo()
})
}
</script>
<template>
......@@ -100,14 +138,14 @@ onUnmounted(() => {
>返回首页</el-button
>
<div>
<el-button type="primary" :icon="Select" :disabled="submitted">提交</el-button>
<el-button type="primary" :icon="UploadFilled" :disabled="submitted" @click="reportDialogVisible = true"
<el-button type="primary" :icon="Select" :disabled="disabled" @click="handleSubmit">提交</el-button>
<el-button type="primary" :icon="UploadFilled" :disabled="disabled" @click="reportDialogVisible = true"
>上传报告</el-button
>
<el-button
type="primary"
:icon="FullScreen"
:disabled="submitted"
:disabled="disabled"
:loading="screenshotLoading"
@click="handleCapture"
>截图</el-button
......@@ -122,7 +160,12 @@ onUnmounted(() => {
</div>
</section>
<!-- 上传报告 -->
<ReportDialog v-model="reportDialogVisible" v-if="reportDialogVisible"></ReportDialog>
<ReportDialog
v-model="reportDialogVisible"
:experiment_id="form.experiment_id"
@update="fetchInfo"
v-if="reportDialogVisible && form.experiment_id"
></ReportDialog>
</template>
<style lang="scss" scoped>
......
import md5 from 'blueimp-md5'
import { getSignature, uploadFile } from '@/api/base'
export async function upload(blob: Blob) {
const key = 'upload/saas-lab/' + md5(new Date().getTime() + Math.random().toString(36).slice(-8)) + '.png'
const response: any = await getSignature()
const params = {
key,
OSSAccessKeyId: response.accessid,
policy: response.policy,
signature: response.signature,
success_action_status: '200',
file: blob,
url: `${response.host}/${key}`
}
await uploadFile(params)
return params.url
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论