fix: evaluation report P0/P1/P2 fixes, remove Docker, add upload UI
Backend: - Add NotFoundException + BusinessException, return correct HTTP status (404/400) - Add @Index on reports.project_id and reports.upload_time - Add fileSize column to reports, populate on upload, return in DTO - Cascade delete: deleting project now removes all reports (DB + files + PDFs) - Delete report: also clean up pre-rendered PDF - File upload MIME validation (extension + Content-Type) - Remove duplicate @ExceptionHandler from ReportController - Switch from System.err to SLF4J logger - Handle MethodArgumentNotValid, MissingServletRequestPart, etc. Frontend: - Remove all Docker files (project uses 宝塔 panel deployment) - Upgrade axios 1.6.8 -> 1.7.7 (CVE-2024-39338) - Remove unused @vue-office/pptx + vue-demi (see CHANGELOG for rationale) - Fix vite proxy port 37821 -> 30081 - Remove mock data fallback in production - Add upload report UI (button + modal in ProjectDetail) - Add create project UI (button + modal in ProjectList) - Add filename search box in ProjectDetail - New useApi methods: createProject, uploadReport, deleteProject, deleteReport - FilePreview/ReportCard: show fileSize (was undefined before) Docs: - Add README.md (overview, quick start, structure) - Add CHANGELOG.md (full change log + pptx removal rationale) - Include EVALUATION_REPORT.md and blog-vibe-coding.md Tests: - All 73 backend tests pass - All 43 frontend tests pass - Updated test fixtures for new API contract
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
<span class="px-2.5 py-1 bg-orange-100 text-orange-600 rounded-full font-medium">{{ fileTypeLabel }}</span>
|
||||
<span class="text-slate-500">{{ formatUploadTime(report.uploadTime) }}</span>
|
||||
<span class="text-slate-400">·</span>
|
||||
<span class="text-slate-500">{{ report.size }}</span>
|
||||
<span class="text-slate-500">{{ formatFileSize(report.fileSize) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,7 +40,7 @@
|
||||
<div v-if="normalizedFileType === 'html'" class="bg-white rounded-2xl shadow-xl overflow-hidden border border-orange-200/30 flex flex-col h-full min-h-[500px]">
|
||||
<iframe
|
||||
ref="iframeRef"
|
||||
:srcdoc="content"
|
||||
:srcdoc="htmlContent"
|
||||
class="w-full h-full"
|
||||
sandbox="allow-same-origin"
|
||||
></iframe>
|
||||
@@ -135,6 +135,21 @@ const renderedMarkdown = computed(() => {
|
||||
return marked(props.content)
|
||||
})
|
||||
|
||||
// Inject <base target="_top"> so links open in the parent window, not the iframe
|
||||
const htmlContent = computed(() => {
|
||||
if (!props.content) return ''
|
||||
const base = '<base target="_top">'
|
||||
// If content already has <head>, inject base right after <head> tag
|
||||
if (/<head[^>]*>/i.test(props.content)) {
|
||||
return props.content.replace(/(<head[^>]*>)/i, `$1\n${base}`)
|
||||
}
|
||||
// Otherwise prepend before the body or first element
|
||||
if (/<body[^>]*>/i.test(props.content)) {
|
||||
return props.content.replace(/(<body[^>]*>)/i, `${base}\n$1`)
|
||||
}
|
||||
return base + props.content
|
||||
})
|
||||
|
||||
const formatUploadTime = (isoString) => {
|
||||
if (!isoString) return ''
|
||||
const d = new Date(isoString)
|
||||
@@ -142,6 +157,13 @@ const formatUploadTime = (isoString) => {
|
||||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}`
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes == null || isNaN(bytes)) return ''
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
// Watch for report changes and load PDF preview for PPTX
|
||||
watch(() => props.report, async (newReport) => {
|
||||
pdfUrl.value = null
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
'text-sm',
|
||||
isSelected ? 'text-white/80' : 'text-slate-500'
|
||||
]">
|
||||
{{ report.size }}
|
||||
{{ formatFileSize(report.fileSize) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,6 +121,13 @@ const formatDate = (isoString) => {
|
||||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
const formatFileSize = (bytes) => {
|
||||
if (bytes == null || isNaN(bytes)) return ''
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
const fileIconComponent = computed(() => FileIcon)
|
||||
|
||||
const iconClass = computed(() => {
|
||||
|
||||
Reference in New Issue
Block a user