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
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>
|