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

chore: update

上级 df07ff92
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -15,6 +15,7 @@
"@element-plus/icons-vue": "^2.0.9",
"@tinymce/tinymce-vue": "^5.0.0",
"@vueuse/core": "^9.1.0",
"ali-oss": "^6.17.1",
"axios": "^0.27.2",
"blueimp-md5": "^2.19.0",
"dayjs": "^1.11.5",
......@@ -28,6 +29,7 @@
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.1.0",
"@types/ali-oss": "^6.16.4",
"@types/blueimp-md5": "^2.18.0",
"@types/node": "^16.11.45",
"@types/qs": "^6.9.7",
......@@ -35,7 +37,6 @@
"@vitejs/plugin-vue": "^3.0.3",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
"ali-oss": "^6.17.1",
"chalk": "^5.0.1",
"eslint": "^8.5.0",
"eslint-plugin-vue": "^9.3.0",
......
......@@ -34,19 +34,14 @@ export function getMapList() {
return httpRequest.get('/api/resource/v1/util/get-data-dictionary-list')
}
// 上传视频
export function getCreateAuth(data: { title: string; file_name: string }) {
return httpRequest.post('/api/resource/v1/resource/video/create-auth', data)
// 获取上传视频凭证
export function getUploadVideoAuth(data: { title: string; file_name: string }) {
return httpRequest.post('/api/lab/v1/teacher/video/auth-create', data)
}
// 刷新上传视频的地址凭证
export function updateAuth(data: { source_id: string }) {
return httpRequest.post('/api/resource/v1/resource/video/create-auth', data)
}
// 获取分类列表
export function getCategoryList(params: { type: string; category_name?: string }) {
return httpRequest.get('/api/resource/v1/backend/category/list', { params })
export function updateUploadVideoAuth(data: { source_id: string }) {
return httpRequest.post('/api/lab/v1/teacher/video/create-auth', data)
}
// 获取项目列表
......
<script setup lang="ts">
interface Props {
url: string
}
const props = defineProps<Props>()
// 文件扩展名
const fileExtensionName = $computed(() => {
return props.url.split('.').pop() || ''
})
// office文件
const officeUrl = $computed(() => {
return ['pptx', 'doc', 'docx', 'xls', 'xlsx'].includes(fileExtensionName)
? `https://view.officeapps.live.com/op/view.aspx?src=${props.url}`
: ''
})
</script>
<template>
<iframe :src="officeUrl" frameborder="0" style="width: 100%; height: 100%" v-if="officeUrl"></iframe>
<object :data="url" style="width: 100%; height: 100%; object-fit: none" v-else></object>
</template>
<script setup lang="ts">
import * as OSS from 'ali-oss'
import '@/lib/aliyun-upload-sdk/aliyun-upload-sdk-1.5.3.min.js'
import { VideoPlay } from '@element-plus/icons-vue'
import { getUploadVideoAuth, updateUploadVideoAuth } from '@/api/base'
;(window as any).OSS = OSS
// uploadInfo 包含要上传的文件信息
interface UploadInfo {
bucket: string
checkpoint: { file: File; name: string; fileSize: number; partSize: number; uploadId: string }
endpoint: string
file: File
fileHash: string
isImage: boolean
loaded: number
object: string
region: string
retry: boolean
ri: string
state: string
userData: string
videoId: string
videoInfo: any
progress: number
}
const emit = defineEmits(['success'])
const uploader = createUploader()
let fileList = $ref<UploadInfo[]>([])
const fileChange = (event: Event) => {
const element = event.currentTarget as HTMLInputElement
const files: FileList | null = element.files
if (!files) return
for (const file of files) {
// 是否重复上传
const hasRepeat = !!fileList.find(
item =>
item.file.name === file.name && item.file.size === file.size && item.file.lastModified === file.lastModified
)
!hasRepeat && uploader.addFile(file, null, null, null, '{"Vod":{}}')
}
uploader.startUpload()
fileList = uploader._uploadList
}
function updateFileList(uploadInfo: UploadInfo) {
if (!uploadInfo) return
fileList = fileList.map(item => {
if (item.ri === uploadInfo.ri) {
return { ...item, ...uploadInfo }
}
return item
})
}
function createUploader() {
return new (window as any).AliyunUpload.Vod({
//userID,必填,您可以使用阿里云账号访问账号中心(https://account.console.aliyun.com/),即可查看账号ID
userId: '1303984639806000',
//上传到视频点播的地域,默认值为'cn-shanghai',
//eu-central-1,ap-southeast-1
region: 'cn-shanghai',
//分片大小默认1 MB,不能小于100 KB(100*1024)
partSize: 1048576,
//并行上传分片个数,默认5
parallel: 5,
//网络原因失败时,重新上传次数,默认为3
retryCount: 3,
//网络原因失败时,重新上传间隔时间,默认为2秒
retryDuration: 2,
//开始上传
onUploadstarted: onUploadStarted,
//文件上传成功
onUploadSucceed: onUploadSucceed,
//文件上传失败
onUploadFailed: onUploadFailed,
//文件上传进度,单位:字节
onUploadProgress: onUploadProgress,
//上传凭证或STS token超时
onUploadTokenExpired: onUploadTokenExpired,
//全部文件上传结束
onUploadEnd: onUploadEnd
})
}
// 开始上传
function onUploadStarted(uploadInfo: UploadInfo) {
console.log('onUploadStarted', uploadInfo)
getUploadVideoAuth({ title: uploadInfo.file.name, file_name: uploadInfo.file.name }).then(res => {
uploader.setUploadAuthAndAddress(uploadInfo, res.data.upload_auth, res.data.upload_address, res.data.source_id)
})
updateFileList(uploadInfo)
}
// 文件上传成功
function onUploadSucceed(uploadInfo: UploadInfo) {
console.log('onUploadSucceed', uploadInfo)
updateFileList(uploadInfo)
emit('success', uploadInfo)
}
//文件上传失败
function onUploadFailed(uploadInfo: UploadInfo, code: number, message: string) {
console.log('onUploadFailed', uploadInfo, code, message)
updateFileList(uploadInfo)
}
//文件上传进度,单位:字节
function onUploadProgress(uploadInfo: UploadInfo, totalSize: number, loadedPercent: number) {
console.log('onUploadProgress', uploadInfo.file.name, uploadInfo, totalSize, loadedPercent)
updateFileList(uploadInfo)
}
//上传凭证或STS token超时
function onUploadTokenExpired(uploadInfo: UploadInfo) {
console.log('onUploadTokenExpired', uploadInfo)
updateUploadVideoAuth({ source_id: uploadInfo.videoId }).then(res => {
uploader.resumeUploadWithAuth(res.data.UploadAuth)
})
updateFileList(uploadInfo)
}
// 全部文件上传结束
function onUploadEnd(uploadInfo: UploadInfo) {
console.log('onUploadEnd', uploadInfo)
updateFileList(uploadInfo)
}
// 进度条
function percentage(value: number) {
return parseFloat((value ? value * 100 : 0).toFixed(2))
}
defineExpose({ uploader, fileList: $$(fileList) })
</script>
<template>
<div class="upload-video">
<div class="upload-video__button">
本地文件
<input accept=".mp4" type="file" @change="fileChange" />
</div>
<div class="tips">推荐视频格式:帧率为25fps\输出码率为4M\输出格式为mp4,建议采用格式工厂等工具处理后上传。</div>
<ul class="upload-video-list" v-for="(item, index) in fileList" :key="index">
<li>
<div class="upload-video-list__item-info">
<el-icon><VideoPlay /></el-icon>
<p>{{ item.file?.name }}</p>
</div>
<el-progress :stroke-width="2" :percentage="percentage(item.loaded)" v-if="item.loaded !== 1" />
</li>
</ul>
</div>
</template>
<style lang="scss" scoped>
.tips {
font-size: 12px;
line-height: 100%;
color: #999999;
margin: 10px 0;
}
.upload-video {
width: 100%;
}
.upload-video__button {
position: relative;
width: 94px;
line-height: 36px;
background: rgba(250, 223, 230, 0.39);
border: 1px solid #aa1941;
border-radius: 20px;
font-size: 14px;
color: #aa1941;
margin-right: 30px;
text-align: center;
cursor: pointer;
input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
}
.upload-video-list {
li {
padding: 4px;
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
font-size: 14px;
color: var(--el-text-color-regular);
margin-bottom: 5px;
position: relative;
box-sizing: border-box;
border-radius: 4px;
background-color: var(--el-fill-color-light);
}
.el-progress {
font-size: 12px;
:deep(.el-progress__text) {
position: absolute;
right: 0;
bottom: 4px;
min-width: auto;
}
}
}
.upload-video-list__item-info {
display: flex;
align-items: center;
line-height: 24px;
overflow: hidden;
margin-right: 50px;
p {
margin-left: 10px;
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
</style>
<script lang="ts" setup>
import type { UploadProps, UploadUserFile } from 'element-plus'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import type { UploadProps, UploadUserFile } from 'element-plus'
import md5 from 'blueimp-md5'
import { getSignature } from '@/api/base'
......
......@@ -28,7 +28,7 @@ withDefaults(defineProps<{ sidebar?: boolean; hasTitle?: boolean }>(), {
background-color: #f8f8f8;
}
.app-layout-container {
height: calc(100vh - 70px);
min-height: calc(100vh - 70px);
display: flex;
}
</style>
......@@ -6,16 +6,15 @@ export function getCourseList() {
return httpRequest.get('/api/lab/v1/teacher/book/courses')
}
// 获取实验列表
export function getExperimentList(params: { course_id: string }) {
export function getExperimentList(params: { course_id?: string }) {
return httpRequest.get('/api/lab/v1/teacher/book/experiments', { params })
}
// 获取指导书列表
export function getBookList(params?: { name?: string; Book_id?: string; page?: number; page_size?: number }) {
export function getBookList(params?: { name?: string; experiment_id?: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/lab/v1/teacher/book/list', { params })
}
// 获取指导书详情
export function getBook(params: { Book_id: string; page?: number; 'per-page'?: number }) {
export function getBook(params: { id: string }) {
return httpRequest.get('/api/lab/v1/teacher/book/view', { params })
}
......
......@@ -40,7 +40,8 @@ const form = reactive<BookCreateItem>({
})
watchEffect(() => {
if (!props.data) return
Object.assign(form, props.data)
form.files = [{ name: props.data.name, url: props.data.url }]
Object.assign(form, { protocol: true }, props.data)
})
watchEffect(() => {
updateExperiments(form.course_id)
......@@ -53,29 +54,34 @@ const checkProtocol = (rule: any, value: any, callback: any) => {
}
}
const rules = ref<FormRules>({
files: [{ type: 'array', required: true, message: '请上传实验指导书', trigger: 'blur' }],
files: [{ type: 'array', required: true, message: '请上传实验指导书' }],
name: [{ required: true, message: '请输入实验指导书名称', trigger: 'blur' }],
course_id: [{ required: true, message: '请选择关联实验课程', trigger: 'blur' }],
experiment_id: [{ required: true, message: '请选择关联实验', trigger: 'blur' }],
course_id: [{ required: true, message: '请选择关联实验课程', trigger: 'change' }],
experiment_id: [{ required: true, message: '请选择关联实验', trigger: 'change' }],
protocol: [{ validator: checkProtocol, message: '请阅读并同意', trigger: 'change' }]
})
const isUpdate = $computed(() => {
return !!form.id
return !!props.data?.id
})
const title = $computed(() => {
return isUpdate ? '编辑实验指导书' : '新增实验指导书'
})
// 实验列表
const experimentList = $computed(() => {
if (!props.data) return experiments.value
return isUpdate
? [{ id: props.data.experiment_id, name: props.data.experiment_id_name }, ...experiments.value]
: experiments.value
})
function handleUploadSuccess(file: any) {
form.name = file.name
form.size = file.size
form.size = (file.size / 1024 / 1024).toString()
form.url = file.raw.url
form.type = file.raw.type
console.log(file)
}
// 提交
function handleSubmit() {
formRef?.validate().then(() => {
console.log(form)
isUpdate ? handleUpdate(form) : handleCreate(form)
})
}
......@@ -99,7 +105,7 @@ function handleUpdate(params: BookCreateItem) {
<template>
<el-dialog :title="title" :close-on-click-modal="false" width="600px" @update:modelValue="$emit('update:modelValue')">
<el-form ref="formRef" :model="form" :rules="rules" label-width="130px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="124px">
<el-form-item label="实验指导书文件" prop="files">
<AppUpload v-model="form.files" :limit="1" @success="handleUploadSuccess">
<template #tip>实验指导书文件支持格式包含:doc docx xls xlsx pdf ppt pptx,大小不超过50M</template>
......@@ -109,13 +115,13 @@ function handleUpdate(params: BookCreateItem) {
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="关联实验课程" prop="course_id">
<el-select v-model="form.course_id" style="width: 100%">
<el-select v-model="form.course_id" filterable style="width: 100%">
<el-option v-for="item in courses" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="关联实验" prop="experiment_id">
<el-select v-model="form.experiment_id" style="width: 100%">
<el-option v-for="item in experiments" :key="item.id" :label="item.name" :value="item.id"></el-option>
<el-select v-model="form.experiment_id" filterable style="width: 100%">
<el-option v-for="item in experimentList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="有效状态" prop="status">
......
......@@ -9,17 +9,19 @@ defineProps<Props>()
<div class="list-item">
<div class="icon"></div>
<div class="list-item-main">
<h2>基金产品(中)(初级)</h2>
<h6>失效</h6>
<p>文件大小:1MB</p>
<p>观看次数:165</p>
<p>创建人:张三丰</p>
<p>创建时间:2022-03-21 15:36:07</p>
<h2>{{ data.name }}</h2>
<h6>{{ data.status_name }}</h6>
<p>文件大小:{{ data.size_name }}</p>
<p>观看次数:{{ data.pv }}</p>
<p>创建人:{{ data.created_operator_name }}</p>
<p>创建时间:{{ data.created_time }}</p>
</div>
<div class="cover">
<div class="cover-inner">
<el-button type="primary" round auto-insert-space>查看</el-button>
<el-button type="primary" round auto-insert-space>编辑</el-button>
<router-link :to="`/admin/lab/book/${data.id}`" target="_blank">
<el-button type="primary" round plain auto-insert-space>查看</el-button>
</router-link>
<el-button type="primary" round plain auto-insert-space @click="$emit('clickEdit', data)">编辑</el-button>
</div>
</div>
</div>
......@@ -38,6 +40,9 @@ defineProps<Props>()
display: block;
}
}
.list-item-main {
line-height: 30px;
}
.cover {
display: none;
position: absolute;
......
......@@ -7,7 +7,7 @@ export interface ExperimentType {
export function useGetExperimentList() {
const experiments = ref<ExperimentType[]>([])
function updateExperiments(courseId: string) {
function updateExperiments(courseId?: string) {
getExperimentList({ course_id: courseId }).then((res: any) => {
experiments.value = res.data
})
......
......@@ -4,11 +4,14 @@ import AppLayout from '@/components/layout/Index.vue'
export const routes: Array<RouteRecordRaw> = [
{
path: '/admin/lab',
redirect: '/admin/lab/book'
},
{
path: '/admin/lab/book',
component: AppLayout,
redirect: '/admin/lab/book',
children: [
{ path: 'book', component: () => import('./views/Index.vue') },
{ path: 'book/:id', component: () => import('./views/View.vue'), props: true }
{ path: '', component: () => import('./views/Index.vue') },
{ path: ':id', component: () => import('./views/View.vue'), props: true }
]
}
]
export interface BookItem {
course_id: string
course_name: string
course_id_name: string
created_operator: string
created_operator_name: string
created_time: string
delete_time: string
experiment_id: string
experiment_id_name: string
id: string
length: string
name: string
organ_id: string
organ_id_name: string
project_id: string
project_id_name: string
score: string
pv: string
size: string
size_name: string
status: string
status_name: string
type: string
type_name: string
updated_operator: string
updated_operator_name: string
updated_time: string
url: string
}
export interface BookCreateItem {
......@@ -32,5 +31,13 @@ export interface BookCreateItem {
url: string
size: string
protocol: boolean
files: []
files: FileItem[]
}
export interface FileItem {
name: string
url: string
raw?: File
type?: string
size?: number
}
......@@ -4,22 +4,36 @@ import { CirclePlusFilled } from '@element-plus/icons-vue'
import AppList from '@/components/base/AppList.vue'
import ListItem from '../components/ListItem.vue'
import { getBookList } from '../api'
import { useGetExperimentList } from '../composables/useGetExperimentList'
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
// 实验列表
const { experiments, updateExperiments } = useGetExperimentList()
updateExperiments()
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listOptions = {
const listOptions = $computed(() => {
return {
remote: {
httpRequest: getBookList,
params: { name: '', experiment_id: '' }
},
filters: [
{ type: 'input', prop: 'name', label: '实验指导书名称', placeholder: '请输入实验指导书名称' },
{ type: 'select', prop: 'lab_id', label: '关联实验', placeholder: '请选择关联实验' }
{
type: 'select',
prop: 'experiment_id',
label: '关联实验',
options: experiments.value,
labelKey: 'name',
valueKey: 'id',
placeholder: '请选择关联实验'
}
],
columns: [{ label: '名称', prop: 'name' }]
}
}
})
let dialogVisible = $ref(false)
const rowData = ref<BookItem | undefined | null>(null)
......@@ -47,9 +61,10 @@ function onUpdateSuccess() {
<el-button type="primary" round :icon="CirclePlusFilled" @click="handleAdd">新增实验指导书</el-button>
</template>
<template #body="{ data }">
<div class="list-card">
<ListItem v-for="item in data" :data="item" :key="item.id"></ListItem>
<div class="list-card" v-if="data.length">
<ListItem v-for="item in data" :data="item" :key="item.id" @clickEdit="handleUpdate(item)"></ListItem>
</div>
<el-empty description="暂无数据" v-else />
</template>
</AppList>
</AppCard>
......
<script setup lang="ts"></script>
<script setup lang="ts">
import type { BookItem } from '../types'
import Preview from '@/components/Preview.vue'
import { getBook } from '../api'
interface Props {
id: string
}
const props = defineProps<Props>()
let detail = $ref<BookItem | null>(null)
provide('detail', $$(detail))
function fetchInfo() {
if (!props.id) return
getBook({ id: props.id }).then(res => {
detail = res.data
})
}
onMounted(() => {
fetchInfo()
})
</script>
<template>
<AppCard title="查看实验指导书">
<template v-if="detail">
<el-descriptions>
<el-descriptions-item label="实验指导书名称">kooriookami</el-descriptions-item>
<el-descriptions-item label="文件大小">18100000000</el-descriptions-item>
<el-descriptions-item label="观看次数">Suzhou</el-descriptions-item>
<el-descriptions-item label="关联实验课程">kooriookami</el-descriptions-item>
<el-descriptions-item label="关联实验">18100000000</el-descriptions-item>
<el-descriptions-item label="有效状态">Suzhou</el-descriptions-item>
<el-descriptions-item label="创建人">Suzhou</el-descriptions-item>
<el-descriptions-item label="创建时间">Suzhou</el-descriptions-item>
<el-descriptions-item label="实验指导书名称:">{{ detail.name }}</el-descriptions-item>
<el-descriptions-item label="文件大小:">{{ detail.size_name }}</el-descriptions-item>
<el-descriptions-item label="观看次数:">{{ detail.pv }}</el-descriptions-item>
<el-descriptions-item label="关联实验课程:">{{ detail.course_id_name }}</el-descriptions-item>
<el-descriptions-item label="关联实验:">{{ detail.experiment_id_name }}</el-descriptions-item>
<el-descriptions-item label="有效状态:">{{ detail.status_name }}</el-descriptions-item>
<el-descriptions-item label="创建人:">{{ detail.created_operator_name }}</el-descriptions-item>
<el-descriptions-item label="创建时间:">{{ detail.created_time }}</el-descriptions-item>
</el-descriptions>
<div style="height: 80vh">
<Preview :url="detail.url"></Preview>
</div>
</template>
</AppCard>
</template>
import httpRequest from '@/utils/axios'
import type { VideoCreateItem } from './types'
// 获取学员列表
export function getBookList(params?: { name?: string; lab_id?: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/lab/backend/book/index', { params })
// 获取课程列表
export function getCourseList() {
return httpRequest.get('/api/lab/v1/teacher/video/courses')
}
// 获取实验列表
export function getExperimentList(params: { course_id?: string }) {
return httpRequest.get('/api/lab/v1/teacher/video/experiments', { params })
}
// 获取操作视频列表
export function getVideoList(params?: { name?: string; experiment_id?: string; page?: number; page_size?: number }) {
return httpRequest.get('/api/lab/v1/teacher/video/list', { params })
}
// 获取操作视频详情
export function getVideo(params: { id: string }) {
return httpRequest.get('/api/lab/v1/teacher/video/view', { params })
}
// 创建操作视频
export function createVideo(data: VideoCreateItem) {
return httpRequest.post('/api/lab/v1/teacher/video/create', data)
}
// 更新操作视频
export function updateVideo(data: VideoCreateItem) {
return httpRequest.post('/api/lab/v1/teacher/video/update', data)
}
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
// import { ElMessage } from 'element-plus'
import type { VideoItem, VideoCreateItem } from '../types'
import { ElMessage } from 'element-plus'
import UploadVideo from '@/components/UploadVideo.vue'
import { createVideo, updateVideo } from '../api'
import { useGetCourseList } from '../composables/useGetCourseList'
import { useGetExperimentList } from '../composables/useGetExperimentList'
import { useMapStore } from '@/stores/map'
interface Props {
data?: any
data?: VideoItem | null
}
const props = defineProps<Props>()
defineEmits<{
const emit = defineEmits<{
(e: 'update'): void
(e: 'update:modelValue', visible: boolean): void
}>()
const title = $computed(() => {
return '新增实验指导书'
})
// 课程列表
const { courses } = useGetCourseList()
// 数据状态
const status = useMapStore().getMapValuesByKey('system_status')
// 实验列表
const { experiments, updateExperiments } = useGetExperimentList()
const formRef = $ref<FormInstance>()
const form = reactive({ title: '', file: [], status: 1 })
const form = reactive<VideoCreateItem>({
name: '',
course_id: '',
experiment_id: '',
status: '1',
source_id: '',
protocol: false
})
watchEffect(() => {
if (!props.data) return
Object.assign(form, { protocol: true }, props.data)
})
watchEffect(() => {
Object.assign(form, props.data)
updateExperiments(form.course_id)
})
const checkProtocol = (rule: any, value: any, callback: any) => {
if (!value) {
return callback(new Error('请阅读并同意'))
} else {
callback()
}
}
const rules = ref<FormRules>({
title: [{ required: true, message: '请输入问题描述', trigger: 'blur' }],
content: [{ required: true, message: '请输入问题详情', trigger: 'blur' }]
source_id: [{ required: true, message: '请上传实验操作视频' }],
name: [{ required: true, message: '请输入实验操作视频名称', trigger: 'blur' }],
course_id: [{ required: true, message: '请选择关联实验课程', trigger: 'change' }],
experiment_id: [{ required: true, message: '请选择关联实验', trigger: 'change' }],
protocol: [{ validator: checkProtocol, message: '请阅读并同意', trigger: 'change' }]
})
const isUpdate = $computed(() => {
return !!props.data?.id
})
const title = $computed(() => {
return isUpdate ? '编辑实验操作视频' : '新增实验操作视频'
})
// 实验列表
const experimentList = $computed(() => {
if (!props.data) return experiments.value
return isUpdate
? [{ id: props.data.experiment_id, name: props.data.experiment_id_name }, ...experiments.value]
: experiments.value
})
function handleUploadSuccess(uploadInfo: any) {
form.name = uploadInfo.file.name
form.source_id = uploadInfo.videoId
}
// 提交
function handleSubmit() {
formRef?.validate().then(update)
formRef?.validate().then(() => {
isUpdate ? handleUpdate(form) : handleCreate(form)
})
}
// 新增
function handleCreate(params: VideoCreateItem) {
createVideo(params).then(() => {
ElMessage({ message: '创建成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
// 修改
const update = () => {
// submitSuggestion(form).then(() => {
// ElMessage({ message: '提交成功', type: 'success' })
// emit('update')
// formRef?.resetFields()
// })
function handleUpdate(params: VideoCreateItem) {
updateVideo(params).then(() => {
ElMessage({ message: '修改成功', type: 'success' })
emit('update')
emit('update:modelValue', false)
})
}
</script>
<template>
<el-dialog :title="title" :close-on-click-modal="false" width="600px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="150px">
<el-form-item label="实验指导书文件" prop="title">
<AppUpload v-model="form.file">
<template #tip>实验指导书文件支持格式包含:doc docx xls xlsx pdf ppt pptx,大小不超过50M</template>
</AppUpload>
<el-dialog :title="title" :close-on-click-modal="false" width="600px" @update:modelValue="$emit('update:modelValue')">
<el-form ref="formRef" :model="form" :rules="rules" label-width="140px">
<el-form-item label="实验操作视频文件" prop="source_id">
<UploadVideo @success="handleUploadSuccess">
<template #tip>实验操作视频文件支持格式包含:doc docx xls xlsx pdf ppt pptx,大小不超过50M</template>
</UploadVideo>
</el-form-item>
<el-form-item label="实验指导书名称" prop="content">
<el-input v-model="form.title"></el-input>
<el-form-item label="实验操作视频名称" prop="name">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="关联实验课程" prop="files">
<el-input v-model="form.title" />
<el-form-item label="关联实验课程" prop="course_id">
<el-select v-model="form.course_id" filterable style="width: 100%">
<el-option v-for="item in courses" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="关联实验" prop="files">
<el-select v-model="form.title" />
<el-form-item label="关联实验" prop="experiment_id">
<el-select v-model="form.experiment_id" filterable style="width: 100%">
<el-option v-for="item in experimentList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="有效状态" prop="files">
<el-radio-group v-model="form.status" class="ml-4">
<el-radio label="1">有效</el-radio>
<el-radio label="2">失效</el-radio>
<el-form-item label="有效状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio v-for="item in status" :key="item.id" :label="item.value">{{ item.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-checkbox label="我已阅读并同意"></el-checkbox>
<el-form-item prop="protocol">
<el-checkbox label="我已阅读并同意" v-model="form.protocol" />
<a
href="https://view.officeapps.live.com/op/view.aspx?src=https://webapp-pub.oss-cn-beijing.aliyuncs.com/center_resource/%E7%B4%AB%E8%8D%86%E6%95%99%E8%82%B2%E7%94%A8%E6%88%B7%E5%85%A5%E9%A9%BB%E5%8F%8A%E7%BD%91%E7%BB%9C%E6%95%99%E5%AD%A6%E8%B5%84%E6%BA%90%E5%8D%8F%E8%AE%AE(1).docx"
target="_blank"
......
......@@ -7,19 +7,16 @@ defineProps<Props>()
<template>
<div class="list-item">
<div class="icon"></div>
<div class="list-item-main">
<h2>基金产品(中)(初级)</h2>
<h6>失效</h6>
<p>文件大小:1MB</p>
<p>观看次数:165次</p>
<p>创建人:张三丰</p>
<p>创建时间:2022-03-21 15:36:07</p>
<img :src="data.cover" />
<p>{{ data.name }}</p>
</div>
<div class="cover">
<div class="cover-inner">
<el-button type="primary" round auto-insert-space>查看</el-button>
<el-button type="primary" round auto-insert-space>编辑</el-button>
<router-link :to="`/admin/lab/video/${data.id}`" target="_blank">
<el-button type="primary" round plain auto-insert-space>查看</el-button>
</router-link>
<el-button type="primary" round plain auto-insert-space @click="$emit('clickEdit', data)">编辑</el-button>
</div>
</div>
</div>
......@@ -29,7 +26,6 @@ defineProps<Props>()
.list-item {
position: relative;
display: flex;
padding: 20px;
background: #f2f2f2;
border-radius: 5px;
overflow: hidden;
......@@ -38,6 +34,28 @@ defineProps<Props>()
display: block;
}
}
.list-item-main {
position: relative;
width: 100%;
img {
width: 100%;
height: 200px;
object-fit: cover;
}
p {
position: absolute;
left: 0;
bottom: 0;
right: 0;
line-height: 40px;
color: #fff;
padding: 0 10px;
background-color: rgba(0, 0, 0, 0.5);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
.cover {
display: none;
position: absolute;
......
import { getCourseList } from '../api'
export interface CourseType {
id: string
name: string
}
const courses = ref<CourseType[]>([])
export function useGetCourseList() {
!courses.value.length &&
getCourseList().then((res: any) => {
courses.value = res.data
})
return { courses }
}
import { getExperimentList } from '../api'
export interface ExperimentType {
id: string
name: string
}
export function useGetExperimentList() {
const experiments = ref<ExperimentType[]>([])
function updateExperiments(courseId?: string) {
getExperimentList({ course_id: courseId }).then((res: any) => {
experiments.value = res.data
})
}
return { experiments, updateExperiments }
}
......@@ -5,6 +5,9 @@ export const routes: Array<RouteRecordRaw> = [
{
path: '/admin/lab/video',
component: AppLayout,
children: [{ path: '', component: () => import('./views/Index.vue') }]
children: [
{ path: '', component: () => import('./views/Index.vue') },
{ path: ':id', component: () => import('./views/View.vue'), props: true }
]
}
]
export interface VideoItem {
course_id: string
course_id_name: string
created_operator: string
created_operator_name: string
created_time: string
delete_time: string
experiment_id: string
experiment_id_name: string
id: string
name: string
pv: string
size: string
size_name: string
status: string
status_name: string
type: string
updated_operator: string
updated_operator_name: string
updated_time: string
url: string
}
export interface VideoCreateItem {
id?: string
experiment_id: string
course_id: string
status: string
name: string
source_id: string
protocol: boolean
}
<script setup lang="ts">
import type { VideoItem } from '../types'
import { CirclePlusFilled } from '@element-plus/icons-vue'
import AppList from '@/components/base/AppList.vue'
import ListItem from '../components/ListItem.vue'
// import { getBookList } from '../api'
import { getVideoList } from '../api'
import { useGetExperimentList } from '../composables/useGetExperimentList'
const FormDialog = defineAsyncComponent(() => import('../components/FormDialog.vue'))
// 实验列表
const { experiments, updateExperiments } = useGetExperimentList()
updateExperiments()
const appList = $ref<InstanceType<typeof AppList> | null>(null)
// 列表配置
const listOptions = {
// remote: {
// httpRequest: getBookList,
// params: { name: '', lab_id: '' }
// },
const listOptions = $computed(() => {
return {
remote: {
httpRequest: getVideoList,
params: { name: '', experiment_id: '' }
},
filters: [
{ type: 'input', prop: 'name', label: '操作视频名称', placeholder: '请输入操作视频名称' },
{ type: 'select', prop: 'lab_id', label: '关联实验', placeholder: '请选择关联实验' }
{ type: 'input', prop: 'name', label: '实验操作视频名称', placeholder: '请输入实验操作视频名称' },
{
type: 'select',
prop: 'experiment_id',
label: '关联实验',
options: experiments.value,
labelKey: 'name',
valueKey: 'id',
placeholder: '请选择关联实验'
}
],
columns: [{ label: '名称', prop: 'name' }],
data: [{}]
columns: [{ label: '名称', prop: 'name' }]
}
})
let dialogVisible = $ref(false)
const rowData = ref<VideoItem | undefined | null>(null)
// 新增
function handleAdd() {
rowData.value = null
dialogVisible = true
}
// 编辑
function handleUpdate(row: VideoItem) {
rowData.value = row
dialogVisible = true
}
function onUpdateSuccess() {
dialogVisible = false
appList?.refetch()
}
</script>
<template>
<AppCard title="实验操作视频管理">
<AppList v-bind="listOptions">
<AppList v-bind="listOptions" ref="appList">
<template #header-buttons>
<el-button type="primary" round :icon="CirclePlusFilled">新增操作视频</el-button>
<el-button type="primary" round :icon="CirclePlusFilled" @click="handleAdd">新增操作视频</el-button>
</template>
<template #body="{ data }">
<div class="list-card">
<ListItem v-for="item in data" :data="item" :key="item.id"></ListItem>
<div class="list-card" v-if="data.length">
<ListItem v-for="item in data" :data="item" :key="item.id" @clickEdit="handleUpdate(item)"></ListItem>
</div>
<el-empty description="暂无数据" v-else />
</template>
</AppList>
</AppCard>
<FormDialog v-model="dialogVisible" :data="rowData" @update="onUpdateSuccess" v-if="dialogVisible"></FormDialog>
</template>
<style lang="scss" scoped>
......
<script setup lang="ts"></script>
<script setup lang="ts">
import type { VideoItem } from '../types'
import AppVideoPlayer from '@/components/base/AppVideoPlayer.vue'
import { getVideo } from '../api'
interface Props {
id: string
}
const props = defineProps<Props>()
let detail = $ref<VideoItem | null>(null)
let playUrl = $ref<string | null>(null)
function fetchInfo() {
if (!props.id) return
getVideo({ id: props.id }).then(res => {
detail = res.data
playUrl = res.data?.play_auth?.play_info_list[0]?.PlayURL || ''
})
}
onMounted(() => {
fetchInfo()
})
</script>
<template>
<AppCard title="查看实验指导书">
<AppCard title="查看实验操作视频">
<template v-if="detail">
<el-descriptions>
<el-descriptions-item label="实验指导书名称">kooriookami</el-descriptions-item>
<el-descriptions-item label="文件大小">18100000000</el-descriptions-item>
<el-descriptions-item label="观看次数">Suzhou</el-descriptions-item>
<el-descriptions-item label="关联实验课程">kooriookami</el-descriptions-item>
<el-descriptions-item label="关联实验">18100000000</el-descriptions-item>
<el-descriptions-item label="有效状态">Suzhou</el-descriptions-item>
<el-descriptions-item label="创建人">Suzhou</el-descriptions-item>
<el-descriptions-item label="创建时间">Suzhou</el-descriptions-item>
<el-descriptions-item label="实验操作视频名称:">{{ detail.name }}</el-descriptions-item>
<el-descriptions-item label="文件大小:">{{ detail.size_name }}</el-descriptions-item>
<el-descriptions-item label="观看次数:">{{ detail.pv }}</el-descriptions-item>
<el-descriptions-item label="关联实验课程:">{{ detail.course_id_name }}</el-descriptions-item>
<el-descriptions-item label="关联实验:">{{ detail.experiment_id_name }}</el-descriptions-item>
<el-descriptions-item label="有效状态:">{{ detail.status_name }}</el-descriptions-item>
<el-descriptions-item label="创建人:">{{ detail.created_operator_name }}</el-descriptions-item>
<el-descriptions-item label="创建时间:">{{ detail.created_time }}</el-descriptions-item>
</el-descriptions>
<AppVideoPlayer
:options="{ sources: [{ src: playUrl, type: 'application/x-mpegURL' }] }"
style="width: 100%; height: 600px"
v-if="playUrl"
></AppVideoPlayer>
</template>
</AppCard>
</template>
<script setup lang="ts">
import type { ExperimentBookType } from '../types'
import Preview from '@/components/Preview.vue'
import { getExperimentBook } from '../api'
interface Props {
......@@ -21,23 +22,11 @@ watchEffect(() => {
const isEmpty = $computed(() => {
return !props.experiment_id || !detail?.id
})
// 文件扩展名
const fileExtensionName = $computed(() => {
return detail?.url?.split('.').pop() || ''
})
// office文件
const officeUrl = $computed(() => {
return ['pptx', 'doc', 'docx', 'xls', 'xlsx'].includes(fileExtensionName)
? `https://view.officeapps.live.com/op/view.aspx?src=${detail.url}`
: ''
})
</script>
<template>
<el-empty description="暂无数据" v-if="isEmpty" />
<template v-else>
<iframe :src="officeUrl" frameborder="0" style="width: 100%; height: 100%" v-if="officeUrl"></iframe>
<object :data="detail.url" style="width: 100%; height: 100%; object-fit: none" v-else></object>
<Preview :url="detail.url"></Preview>
</template>
</template>
......@@ -169,7 +169,7 @@ function handleSubmit() {
<style lang="scss" scoped>
.lab {
display: flex;
height: 100%;
height: calc(100vh - 110px);
}
.lab-left {
display: flex;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论