fix: FilePreview fileType case + Tailwind v4 gradient transparent bug

- FilePreview.vue: add normalizedFileType computed to handle backend
  returning uppercase HTML/MD/PPTX (fixes preview/download buttons)
- FilePreview.vue: bg-gradient-to-r from-orange-500 -> bg-orange-500
  (Tailwind v4 gradient + CSS variable = transparent)
- ReportCard.vue: bg-gradient-to-r -> bg-orange-600 for selected state
- Add .opencode/, node_modules/, dist/ to .gitignore
- Initial git setup for publish project
This commit is contained in:
2026-05-24 20:09:42 +08:00
commit b9137204a0
78 changed files with 12950 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
<template>
<div class="h-full flex flex-col">
<!-- Preview header -->
<div v-if="report" class="glass border-b border-orange-200/50 px-6 py-4 shadow-sm">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<!-- File type icon -->
<div :class="iconBgClass" class="w-12 h-12 rounded-xl flex items-center justify-center shadow-lg">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
<div>
<h3 class="font-semibold text-slate-800 text-lg">{{ report.fileName }}</h3>
<div class="mt-1 flex items-center space-x-3 text-sm">
<span class="px-2.5 py-1 bg-orange-100 text-orange-600 rounded-full font-medium">{{ fileTypeLabel }}</span>
<span class="text-slate-500">{{ report.reportDate }}</span>
<span class="text-slate-400">·</span>
<span class="text-slate-500">{{ report.size }}</span>
</div>
</div>
</div>
<!-- Download button -->
<button
@click="downloadReport"
class="group px-5 py-2.5 bg-orange-500 text-white rounded-xl hover:bg-orange-600 transition-all shadow-lg shadow-orange-500/30 flex items-center space-x-2 hover:shadow-xl"
>
<svg class="w-5 h-5 group-hover:translate-y-0.5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
<span class="font-medium">下载</span>
</button>
</div>
</div>
<!-- Preview content -->
<div class="flex-1 overflow-auto p-6 bg-slate-50/50">
<!-- HTML Preview -->
<div v-if="normalizedFileType === 'html'" class="bg-white rounded-2xl shadow-xl overflow-hidden border border-orange-200/30">
<iframe
ref="iframeRef"
:srcdoc="content"
class="w-full h-full min-h-[500px]"
sandbox="allow-same-origin"
></iframe>
</div>
<!-- Markdown Preview -->
<div v-else-if="normalizedFileType === 'md'" class="bg-white rounded-2xl shadow-xl p-8 border border-orange-200/30 prose max-w-none prose-orange">
<div v-html="renderedMarkdown"></div>
</div>
<!-- PPTX Preview (PDF iframe) -->
<div v-else-if="normalizedFileType === 'pptx'" class="bg-white rounded-2xl shadow-xl overflow-hidden h-full flex flex-col border border-orange-200/30">
<iframe
v-if="pdfUrl"
:src="pdfUrl"
class="w-full flex-1 min-h-[500px]"
type="application/pdf"
></iframe>
<div v-else class="flex items-center justify-center h-full">
<div class="text-center">
<div class="w-16 h-16 mx-auto bg-gradient-to-br from-orange-400 to-orange-600 rounded-2xl flex items-center justify-center mb-4 shadow-lg">
<svg class="w-8 h-8 text-white animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</div>
<p class="text-slate-500 font-medium">正在加载预览...</p>
</div>
</div>
</div>
<!-- Empty state -->
<div v-else class="flex items-center justify-center h-full">
<div class="text-center">
<div class="w-24 h-24 glass rounded-2xl flex items-center justify-center mx-auto mb-6">
<svg class="w-12 h-12 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
<p class="text-slate-500 text-lg">选择一份报告以预览</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onUnmounted } from 'vue'
import { marked } from 'marked'
import { useApi } from '../composables/useApi'
const props = defineProps({
report: {
type: Object,
default: null
},
content: {
type: String,
default: ''
}
})
const iframeRef = ref(null)
const pdfUrl = ref(null)
const { fetchReportBytes, fetchReportPdf } = useApi()
const fileTypeLabelMap = {
html: 'HTML',
md: 'Markdown',
pptx: 'PowerPoint'
}
const iconBgClassMap = {
html: 'bg-orange-500',
md: 'bg-orange-400',
pptx: 'bg-orange-500'
}
const fileTypeLabel = computed(() => {
return fileTypeLabelMap[normalizedFileType.value] || props.report?.fileType?.toUpperCase() || ''
})
const iconBgClass = computed(() => {
return iconBgClassMap[normalizedFileType.value] || iconBgClassMap.html
})
const normalizedFileType = computed(() => {
return (props.report?.fileType || '').toLowerCase()
})
const renderedMarkdown = computed(() => {
if (!props.content) return ''
return marked(props.content)
})
// Watch for report changes and load PDF preview for PPTX
watch(() => props.report, async (newReport) => {
pdfUrl.value = null
if (normalizedFileType.value === 'pptx') {
try {
const pdfBlob = await fetchReportPdf(newReport.id)
if (pdfBlob) {
pdfUrl.value = URL.createObjectURL(pdfBlob)
}
} catch (e) {
console.error('Failed to load PDF preview:', e)
}
}
}, { immediate: true })
onUnmounted(() => {
if (pdfUrl.value) {
URL.revokeObjectURL(pdfUrl.value)
}
})
const downloadReport = async () => {
if (!props.report) return
const ft = normalizedFileType.value
if (ft === 'pptx' || ft === 'html' || ft === 'md') {
try {
const bytes = await fetchReportBytes(props.report.id)
if (bytes) {
const blob = new Blob([bytes], { type: 'application/octet-stream' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = props.report.fileName
a.click()
URL.revokeObjectURL(url)
} else {
alert('文件不存在或无法读取')
}
} catch (e) {
console.error('Download failed:', e)
alert('下载失败')
}
return
}
}
</script>