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:
2026-06-01 21:35:13 +08:00
parent 7000c186e2
commit afcd18c54f
77 changed files with 1498 additions and 2886 deletions
+24 -2
View File
@@ -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
+8 -1
View File
@@ -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(() => {