| | |
| | | <el-button |
| | | type="primary" |
| | | size="small" |
| | | @click="previewAttachment(attachment)" |
| | | > |
| | | 预览 |
| | | </el-button> |
| | | <el-button |
| | | type="primary" |
| | | size="small" |
| | | @click="downloadAttachment(attachment)" |
| | | > |
| | | 下载 |
| | |
| | | <div class="card-header"> |
| | | <span>审核管理</span> |
| | | </div> |
| | | </template> |
| | | <!-- 附件预览对话框 --> |
| | | <el-dialog v-model="previewVisible" title="文件预览" width="80%" center> |
| | | <div class="preview-content"> |
| | | <!-- 图片预览 --> |
| | | <img v-if="previewType === 'image' && previewUrl" :src="previewUrl" style="max-width: 100%; max-height: 70vh; object-fit: contain;" /> |
| | | <!-- 视频预览 --> |
| | | <video v-else-if="previewType === 'video' && previewUrl" :src="previewUrl" controls style="width: 100%; max-height: 70vh;"></video> |
| | | <!-- PDF 预览 --> |
| | | <iframe v-else-if="previewType === 'pdf' && previewUrl" :src="previewUrl" style="width: 100%; height: 70vh; border: none;"></iframe> |
| | | <!-- DOCX 预览 --> |
| | | <div v-else-if="previewType === 'docx'" ref="docxContainer" class="docx-preview"></div> |
| | | <!-- 其它不支持 --> |
| | | <div v-else class="preview-error"> |
| | | <el-empty description="无法预览此文件类型,请下载查看" /> |
| | | </div> |
| | | </div> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <div class="review-section"> |
| | | <div class="review-status"> |
| | |
| | | const activityPlayerData = ref<any>(null) |
| | | const attachments = ref<any[]>([]) |
| | | |
| | | // 预览相关 |
| | | const previewVisible = ref(false) |
| | | const previewUrl = ref('') |
| | | const previewType = ref<'' | 'image' | 'video' | 'pdf' | 'docx' | 'unknown'>('') |
| | | const docxContainer = ref<HTMLElement | null>(null) |
| | | |
| | | // 审核相关数据 |
| | | const feedbackText = ref('') |
| | | const approving = ref(false) |
| | |
| | | try { |
| | | loading.value = true |
| | | const playerId = route.params.id as string |
| | | |
| | | // 这里应该调用API获取数据 |
| | | // 暂时使用模拟数据 |
| | | await loadPlayerData(playerId) |
| | | |
| | | } catch (error) { |
| | | console.error('加载数据失败:', error) |
| | | ElMessage.error('加载数据失败') |
| | |
| | | regionInfo { |
| | | id |
| | | name |
| | | fullPath |
| | | } |
| | | activityName |
| | | projectName |
| | |
| | | fileExt |
| | | fileSize |
| | | mediaType |
| | | thumbUrl |
| | | } |
| | | } |
| | | } |
| | |
| | | const loadPlayerData = async (playerId: string) => { |
| | | try { |
| | | const data = await graphqlRequest(ACTIVITY_PLAYER_DETAIL_QUERY, { id: playerId }) |
| | | const detail = data.activityPlayerDetail |
| | | const detail = data?.data?.activityPlayerDetail || data?.activityPlayerDetail |
| | | |
| | | if (detail) { |
| | | // 设置player基本信息 |
| | |
| | | |
| | | // 初始化审核意见 |
| | | feedbackText.value = detail.feedback || '' |
| | | } else { |
| | | ElMessage.warning('未找到对应的参赛人员详情信息') |
| | | } |
| | | } catch (error) { |
| | | console.error('加载数据失败:', error) |
| | | ElMessage.error(`加载数据失败: ${error.message || error}`) |
| | | throw error |
| | | } |
| | | } |
| | |
| | | const getGenderText = (gender: number) => { |
| | | const genderMap: Record<number, string> = { |
| | | 1: '男', |
| | | 2: '女' |
| | | 0: '女' |
| | | } |
| | | return genderMap[gender] || '-' |
| | | } |
| | |
| | | window.open(attachment.url, '_blank') |
| | | } |
| | | |
| | | /** |
| | | * 预览附件:按扩展名分发 图片/视频/PDF/DOCX |
| | | */ |
| | | const previewAttachment = async (attachment: any) => { |
| | | const name: string = attachment.originalName || attachment.name || '' |
| | | const url: string = attachment.url |
| | | const ext = (name.split('.').pop() || '').toLowerCase() |
| | | |
| | | previewVisible.value = true |
| | | previewUrl.value = '' |
| | | previewType.value = '' |
| | | |
| | | const imageExts = ['jpg','jpeg','png','gif','bmp','webp'] |
| | | const videoExts = ['mp4','webm','ogg','avi','mov','wmv','flv','mkv'] |
| | | |
| | | if (imageExts.includes(ext)) { |
| | | previewType.value = 'image' |
| | | previewUrl.value = url |
| | | return |
| | | } |
| | | if (videoExts.includes(ext)) { |
| | | previewType.value = 'video' |
| | | previewUrl.value = url |
| | | return |
| | | } |
| | | if (ext === 'pdf') { |
| | | previewType.value = 'pdf' |
| | | previewUrl.value = url |
| | | return |
| | | } |
| | | if (ext === 'docx') { |
| | | previewType.value = 'docx' |
| | | try { |
| | | await renderDocx(url) |
| | | } catch (e: any) { |
| | | console.error('DOCX 预览失败:', e) |
| | | ElMessage.warning('DOCX 预览失败,建议下载查看') |
| | | previewType.value = 'unknown' |
| | | } |
| | | return |
| | | } |
| | | if (ext === 'doc') { |
| | | ElMessage.info('暂不支持 .doc 预览,请下载查看') |
| | | previewType.value = 'unknown' |
| | | return |
| | | } |
| | | |
| | | ElMessage.warning('此文件类型不支持预览,请下载查看') |
| | | previewType.value = 'unknown' |
| | | } |
| | | |
| | | /** |
| | | * 动态加载 docx-preview 并渲染 DOCX |
| | | */ |
| | | const renderDocx = async (url: string) => { |
| | | // 动态加载 docx-preview |
| | | const ensureScript = () => new Promise((resolve, reject) => { |
| | | if ((window as any).docx && (window as any).docx.renderAsync) return resolve(true) |
| | | const existed = document.querySelector('script[data-docx-preview]') |
| | | if (existed) { |
| | | existed.addEventListener('load', () => resolve(true)) |
| | | existed.addEventListener('error', reject) |
| | | return |
| | | } |
| | | const s = document.createElement('script') |
| | | s.src = 'https://unpkg.com/docx-preview/dist/docx-preview.min.js' |
| | | s.async = true |
| | | s.setAttribute('data-docx-preview', '1') |
| | | s.onload = () => resolve(true) |
| | | s.onerror = reject |
| | | document.head.appendChild(s) |
| | | }) |
| | | |
| | | // 规范化 URL(支持相对路径) |
| | | const buildUrl = (u: string) => { |
| | | if (!u) return u |
| | | if (u.startsWith('http://') || u.startsWith('https://')) return u |
| | | if (u.startsWith('/')) return location.origin + u |
| | | return u |
| | | } |
| | | |
| | | // 携带鉴权头访问附件(很多文件服务不走 cookie,而是 JWT Header) |
| | | const { getToken } = await import('@/utils/auth') |
| | | const token = getToken ? getToken() : '' |
| | | const requestUrl = buildUrl(url) |
| | | |
| | | let res: Response |
| | | try { |
| | | res = await fetch(requestUrl, { |
| | | credentials: 'include', |
| | | headers: token ? { Authorization: `Bearer ${token}` } : undefined, |
| | | mode: 'cors' |
| | | } as RequestInit) |
| | | } catch (e) { |
| | | throw new Error('获取 DOCX 失败: 网络不可达或被拦截') |
| | | } |
| | | if (!res.ok) throw new Error('获取 DOCX 失败: ' + res.status) |
| | | |
| | | const blob = await res.blob() |
| | | |
| | | await ensureScript() |
| | | if (docxContainer.value) { |
| | | docxContainer.value.innerHTML = '' |
| | | await (window as any).docx.renderAsync(blob, docxContainer.value, null, { inWrapper: true }) |
| | | } |
| | | } |
| | | |
| | | // 审核通过 |
| | | const handleApprove = async () => { |
| | | try { |
| | |
| | | approving.value = true |
| | | const result = await approveActivityPlayer(activityPlayerData.value.id, feedbackText.value) |
| | | |
| | | if (result.approveActivityPlayer) { |
| | | if (result.data.approveActivityPlayer) { |
| | | ElMessage.success('审核通过成功') |
| | | activityPlayerData.value.state = 1 |
| | | activityPlayerData.value.feedback = feedbackText.value |
| | |
| | | rejecting.value = true |
| | | const result = await rejectActivityPlayer(activityPlayerData.value.id, feedbackText.value) |
| | | |
| | | if (result.rejectActivityPlayer) { |
| | | if (result.data.rejectActivityPlayer) { |
| | | ElMessage.success('审核驳回成功') |
| | | activityPlayerData.value.state = 2 |
| | | activityPlayerData.value.feedback = feedbackText.value |
| | |
| | | updating.value = true |
| | | const result = await updatePlayerFeedback(activityPlayerData.value.id, feedbackText.value) |
| | | |
| | | if (result.updatePlayerFeedback) { |
| | | if (result.data.updatePlayerFeedback) { |
| | | ElMessage.success('审核意见更新成功') |
| | | activityPlayerData.value.feedback = feedbackText.value |
| | | } else { |
| | |
| | | grid-template-columns: 1fr; |
| | | } |
| | | } |
| | | .preview-content { |
| | | text-align: center; |
| | | } |
| | | |
| | | .preview-error { |
| | | padding: 40px 0; |
| | | } |
| | | |
| | | .docx-preview { |
| | | text-align: left; |
| | | max-height: 70vh; |
| | | overflow: auto; |
| | | background: #fff; |
| | | padding: 12px; |
| | | } |
| | | </style> |