智慧教务系统
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

706 lines
18 KiB

<template>
<el-dialog
:title="dialogTitle"
v-model="showDialog"
:width="800"
:close-on-click-modal="false"
:destroy-on-close="true"
>
<el-form
:model="formData"
label-width="120px"
ref="formRef"
:rules="rules"
class="page-form"
>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item :label="t('title')" prop="title">
<el-input
v-model="formData.title"
:placeholder="t('titlePlaceholder')"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="getPreviewLabel" prop="image">
<div class="image-preview-container">
<!-- 根据素材类型和是否有URL显示不同内容 -->
<!-- 图片类型:显示图片预览 -->
<div v-if="formData.type === '3' && formData.url" class="image-display">
<el-image
:src="formData.url"
style="width: 120px; height: 120px"
fit="cover"
:preview-src-list="[formData.url]"
/>
<div class="image-info">
<p>图片预览</p>
<el-button type="danger" size="small" @click="clearFile">删除</el-button>
</div>
</div>
<!-- 视频类型:显示视频预览 -->
<div v-else-if="formData.type === '1' && formData.url" class="video-display">
<video
:src="formData.url"
controls
style="width: 120px; height: 120px"
preload="metadata"
>
您的浏览器不支持视频播放
</video>
<div class="image-info">
<p>视频预览</p>
<el-button type="danger" size="small" @click="clearFile">删除</el-button>
</div>
</div>
<!-- 文件类型:显示文件图标和下载 -->
<div v-else-if="formData.type === '2' && formData.url" class="file-display">
<div class="file-icon-container" @click="downloadFile">
<el-icon size="40" class="file-icon">
<Document />
</el-icon>
<div class="file-name">{{ getFileName(formData.url) }}</div>
</div>
<div class="image-info">
<el-button type="primary" size="small" @click="downloadFile">
<el-icon><Download /></el-icon>
下载
</el-button>
<el-button type="danger" size="small" @click="clearFile">删除</el-button>
</div>
</div>
<!-- 暂无内容时显示占位符 -->
<div v-else class="image-placeholder">
<el-icon size="30" class="mb-2">
<Picture v-if="!formData.type || formData.type === '3'" />
<VideoPlay v-else-if="formData.type === '1'" />
<Document v-else-if="formData.type === '2'" />
</el-icon>
<div class="text-xs text-gray-400">{{ getPlaceholderText }}</div>
</div>
</div>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="t('type')" prop="type">
<el-select v-model="formData.type" :placeholder="t('typePlaceholder')" class="w-full" @change="onTypeChange">
<el-option
v-for="(item, index) in typeList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<!-- 文件上传区域 -->
<el-row :gutter="20" v-if="formData.type">
<el-col :span="24">
<el-form-item :label="materialLabel" prop="url">
<!-- 文件上传区域 -->
<div class="material-upload-area">
<direct-upload
ref="uploadRef"
:file-type="getFileTypeForDirectUpload()"
:max-size="getMaxSizeForFileType()"
:accept="getFileAccept"
:fallback-url="uploadUrl"
@success="onDirectUploadSuccess"
@error="onUploadError"
@progress="onUploadProgress"
>
<el-button type="primary" :loading="uploading">
{{ uploading ? `上传中 ${uploadProgress}%` : '选择文件' }}
</el-button>
</direct-upload>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item :label="t('content')" prop="content">
<editor v-model="formData.content" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item :label="t('status')" prop="status">
<el-select v-model="formData.status" :placeholder="t('statusPlaceholder')" class="w-full">
<el-option
v-for="(item, index) in statusList"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="员工权限" prop="user_permission">
<el-select
v-model="formData.user_permission"
multiple
filterable
clearable
placeholder="搜索并选择员工"
style="width: 100%"
collapse-tags
collapse-tags-tooltip
max-collapse-tags="3"
>
<el-option
v-for="item in personnelList"
:key="item.id"
:label="item.name"
:value="item.id"
>
<span>{{ item.name }}</span>
<span v-if="item.phone" style="float: right; color: #8492a6; font-size: 13px">
{{ item.phone }}
</span>
</el-option>
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
{{ t('confirm') }}
</el-button>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, defineProps, defineEmits, onMounted } from 'vue'
import { t } from '@/lang'
import { useDictionary } from '@/app/api/dict'
import { addUnified, editUnified, getUnifiedInfo } from '@/app/api/lesson_course_teaching'
import { getWithPersonnelDataList } from '@/app/api/lesson_course_teaching'
import { img } from '@/utils/common'
import { ElMessage, FormInstance } from 'element-plus'
import { Plus, Document, Download, Picture, VideoPlay } from '@element-plus/icons-vue'
import { getToken } from '@/utils/common'
import type { UploadProps } from 'element-plus'
import Editor from '@/components/editor/index.vue'
import DirectUpload from '@/components/direct-upload/index.vue'
const props = defineProps<{
moduleConfig: {
name: string
table_type: number
auto_distribute: boolean
} | null
}>()
const emit = defineEmits(['complete'])
const showDialog = ref(false)
const submitLoading = ref(false)
const formRef = ref<FormInstance>()
const uploadRef = ref()
const uploading = ref(false)
const uploadProgress = ref(0)
const formData = reactive({
id: 0,
title: '',
image: '',
type: '',
url: '',
content: '',
status: '1',
user_permission: [] as number[],
table_type: 0
})
// 字典数据
const typeList = ref([] as any[])
const statusList = ref([] as any[])
const personnelList = ref([] as any[])
// 表单验证规则
const rules = {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
url: [{ required: true, message: '请输入或选择文件', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
// 对话框标题
const dialogTitle = computed(() => {
const action = formData.id ? '编辑' : '添加'
return `${action}${props.moduleConfig?.name || ''}`
})
// 预览区域标签
const getPreviewLabel = computed(() => {
switch (formData.type) {
case '1': return '视频预览'
case '2': return '文件预览'
case '3': return '图片预览'
default: return '封面预览'
}
})
// 占位文本
const getPlaceholderText = computed(() => {
switch (formData.type) {
case '1': return '暂无视频文件'
case '2': return '暂无文档文件'
case '3': return '暂无图片文件'
default: return '暂无封面图片'
}
})
// 文件接受类型
const getFileAccept = computed(() => {
const typeMap: Record<string, string> = {
'1': 'video/*',
'2': '.pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx',
'3': 'image/*'
}
return typeMap[formData.type] || '*/*'
})
// 计算属性:根据素材类型返回对应的上传接口
const uploadUrl = computed(() => {
const baseUrl = import.meta.env.VITE_IMG_DOMAIN || 'http://localhost:20080'
switch (formData.type) {
case '1': // 视频
return `${baseUrl}/adminapi/sys/video`
case '2': // 文件
return `${baseUrl}/adminapi/sys/document/material`
case '3': // 图片
return `${baseUrl}/adminapi/sys/image`
default:
return ''
}
})
// 计算属性:上传请求头
const uploadHeaders = computed(() => ({
'token': getToken() || ''
}))
// 计算属性:素材标签
const materialLabel = computed(() => {
switch (formData.type) {
case '1': return '视频素材'
case '2': return '文件素材'
case '3': return '图片素材'
default: return '素材内容'
}
})
// 初始化字典数据
const initDictionaries = async () => {
try {
typeList.value = (await useDictionary('material_type')).data.dictionary
statusList.value = (await useDictionary('course_status')).data.dictionary
// 获取人员列表
const personnelRes = await getWithPersonnelDataList({})
personnelList.value = personnelRes.data.data || []
} catch (error) {
console.error('初始化字典数据失败:', error)
}
}
// 重置表单
const resetForm = () => {
formData.id = 0
formData.title = ''
formData.image = ''
formData.type = ''
formData.url = ''
formData.content = ''
formData.status = '1'
formData.user_permission = []
formData.table_type = props.moduleConfig?.table_type || 0
formRef.value?.clearValidate()
}
// 打开对话框
const open = async (rowData?: any) => {
resetForm()
if (rowData && rowData.id) {
// 编辑模式:获取详细数据
try {
const res = await getUnifiedInfo(rowData.id)
const info = res.data
formData.id = info.id
formData.title = info.title
formData.image = info.image
formData.type = info.type
formData.url = info.url
formData.content = info.content
formData.status = info.status
formData.table_type = info.table_type
// 处理用户权限数组
if (info.user_permission) {
formData.user_permission = info.user_permission.split(',').map((id: string) => parseInt(id)).filter((id: number) => !isNaN(id))
}
} catch (error) {
ElMessage.error('获取数据失败')
return
}
} else {
// 新增模式
formData.table_type = props.moduleConfig?.table_type || 0
}
showDialog.value = true
}
// 文件上传前的验证
const beforeUpload: UploadProps['beforeUpload'] = (file) => {
console.log('准备上传文件:', file.name, '类型:', formData.type)
// 根据素材类型验证文件
let isValid = false
let errorMsg = ''
switch (formData.type) {
case '1': // 视频
isValid = file.type.startsWith('video/')
errorMsg = '请选择视频文件'
break
case '2': // 文件
isValid = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'].includes(file.type)
errorMsg = '请选择PDF、Word或文本文件'
break
case '3': // 图片
isValid = file.type.startsWith('image/')
errorMsg = '请选择图片文件'
break
default:
errorMsg = '请先选择素材类型'
return false
}
if (!isValid) {
ElMessage.error(errorMsg)
return false
}
// 文件大小限制(根据文件类型设置不同限制)
let maxSize = 10 // 默认10MB
let sizeText = '10MB'
if (formData.type === '1') { // 视频文件
maxSize = 100 // 100MB
sizeText = '100MB'
}
const isValidSize = file.size / 1024 / 1024 < maxSize
if (!isValidSize) {
ElMessage.error(`文件大小不能超过 ${sizeText}!`)
return false
}
uploading.value = true
return true
}
// 上传成功处理
const onUploadSuccess = (response: any) => {
console.log('上传成功响应:', response)
uploading.value = false
if (response.code === 1) {
formData.url = response.data.url
// 如果上传的是图片,同时设置为封面图片
if (formData.type === '3') {
formData.image = response.data.url
}
ElMessage.success('文件上传成功')
} else {
ElMessage.error(response.msg || '上传失败')
}
}
// 上传失败处理
const onUploadError = (error: any) => {
console.error('上传失败:', error)
uploading.value = false
ElMessage.error('上传失败,请重试')
}
// 清除文件
const clearFile = () => {
formData.url = ''
ElMessage.success('文件已清除')
}
// 下载文件
const downloadFile = () => {
if (formData.url) {
window.open(formData.url, '_blank')
}
}
// 获取文件名
const getFileName = (url: string) => {
if (!url) return ''
return url.split('/').pop() || 'unknown'
}
// 清除封面图片
const clearImage = () => {
formData.image = ''
ElMessage.success('封面图片已清除')
}
// 素材类型改变时的处理
const onTypeChange = () => {
// 清空已上传的文件
formData.url = ''
// 如果切换到非图片类型,清除封面图片
if (formData.type !== '3') {
formData.image = ''
}
}
// 获取直传文件类型
const getFileTypeForDirectUpload = () => {
switch (formData.type) {
case '1': return 'video'
case '2': return 'document'
case '3': return 'image'
default: return 'image'
}
}
// 获取文件大小限制
const getMaxSizeForFileType = () => {
switch (formData.type) {
case '1': return 500 // 视频500MB
case '2': return 50 // 文档50MB
case '3': return 10 // 图片10MB
default: return 10
}
}
// 直传成功处理
const onDirectUploadSuccess = (url: string) => {
formData.url = url
// 如果上传的是图片,同时设置为封面图片
if (formData.type === '3') {
formData.image = url
}
uploading.value = false
uploadProgress.value = 0
ElMessage.success('文件上传成功')
}
// 上传进度处理
const onUploadProgress = (percent: number) => {
uploadProgress.value = percent
uploading.value = percent > 0 && percent < 100
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
submitLoading.value = true
const submitData = {
...formData,
user_permission: formData.user_permission.join(',')
}
if (formData.id) {
await editUnified(submitData)
ElMessage.success('编辑成功')
} else {
await addUnified(submitData)
ElMessage.success('添加成功')
}
showDialog.value = false
emit('complete')
} catch (error) {
console.error('提交失败:', error)
} finally {
submitLoading.value = false
}
}
// 初始化
onMounted(() => {
initDictionaries()
})
// 暴露方法给父组件
defineExpose({
open
})
</script>
<style lang="scss" scoped>
.page-form {
.el-input, .el-select, .el-textarea {
width: 100%;
}
}
.material-upload-area {
margin-bottom: 15px;
}
.image-preview-container {
.image-display, .video-display, .file-display {
display: flex;
align-items: flex-start;
gap: 15px;
.image-info {
display: flex;
flex-direction: column;
gap: 10px;
p {
margin: 0;
font-size: 14px;
color: #606266;
}
}
}
.file-display {
.file-icon-container {
width: 120px;
height: 120px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: #fafafa;
cursor: pointer;
transition: all 0.3s;
&:hover {
border-color: #409eff;
background-color: #ecf5ff;
}
.file-icon {
color: #606266;
margin-bottom: 8px;
}
.file-name {
font-size: 12px;
color: #909399;
text-align: center;
word-break: break-word;
max-width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.image-placeholder {
width: 120px;
height: 120px;
border: 1px dashed #dcdfe6;
border-radius: 4px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #c0c4cc;
background-color: #fafafa;
}
}
.material-preview-area {
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 15px;
background-color: #fafafa;
.image-preview {
display: flex;
align-items: center;
gap: 15px;
}
.video-preview {
display: flex;
align-items: center;
gap: 15px;
}
.file-preview {
display: flex;
align-items: center;
justify-content: space-between;
.file-info {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
color: #606266;
}
.file-actions {
display: flex;
gap: 10px;
}
}
.preview-info {
display: flex;
flex-direction: column;
gap: 10px;
p {
margin: 0;
font-size: 14px;
color: #606266;
}
}
}
</style>