Codex Assistant
昨天 afeeed281e60466b576fbe74d339634cc5d07b82
web/src/views/check-detail.vue
@@ -115,6 +115,13 @@
              <el-button 
                type="primary" 
                size="small" 
                @click="previewAttachment(attachment)"
              >
                预览
              </el-button>
              <el-button
                type="primary"
                size="small"
                @click="downloadAttachment(attachment)"
              >
                下载
@@ -130,7 +137,24 @@
          <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">
@@ -223,6 +247,12 @@
const playerData = ref<any>(null)
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('')
@@ -422,6 +452,113 @@
const downloadAttachment = (attachment: any) => {
  // TODO: 实现附件下载功能
  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 })
  }
}
// 审核通过
@@ -770,4 +907,19 @@
    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>