feat: add OpenList upload support and upload tasks monitoring page

Backend:
- Add OpenListRecordArtifactUploader implementing AList-compatible API upload
- Add Uploading status to RecordArtifactUploadStatus enum
- Add MarkUploadStarted() to RecordResult entity for upload progress tracking
- Add ListUploadStatus API endpoint with pagination and status filtering
- Add UploadTaskItemDto and UploadTaskListResponse models
- Add upload segment count stats (uploaded/failed/uploading) to session DTO
- Add OpenListUploadSettingsDto and upload target type OpenList

Frontend:
- Add UploadTasksView page with route /upload-tasks
- Add upload status labels and UploadTaskItem types
- Refactor MainLayout navigation and clean up main.css
- Polish DashboardView, MetricCard, StatusBadge, RightDrawer components
- Update SettingsView to support OpenList upload configuration

Build:
- Add frontend/Dockerfile.arm64 for ARM64 frontend image
- Update build-arm64-image.sh script

Other:
- Add segment_completed_openlist.sh trigger script
- Add prototype/ directory with UI mockups
- Add frontend .dockerignore refinements

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 21:14:50 +08:00
co-authored by Claude Fable 5
parent f8abae0a7a
commit 9df174bb0b
44 changed files with 3962 additions and 1827 deletions
File diff suppressed because it is too large Load Diff
+12 -63
View File
@@ -8,78 +8,27 @@ withDefaults(
description?: string;
icon?: Component | null;
}>(),
{
description: "",
icon: null
}
{ description: "", icon: null }
);
</script>
<template>
<article class="metric-card">
<div class="metric-card__header">
<span class="metric-card__label">{{ label }}</span>
<span v-if="icon" class="metric-card__icon">
<el-icon><component :is="icon" /></el-icon>
<article class="stat-card">
<div class="stat-card__top" style="display:flex;align-items:center;justify-content:space-between">
<span class="stat-card__label">{{ label }}</span>
<span v-if="icon" class="stat-card__ic">
<el-icon :size="18"><component :is="icon" /></el-icon>
</span>
</div>
<div class="metric-card__value">{{ value }}</div>
<div v-if="description" class="metric-card__description">{{ description }}</div>
<div class="stat-card__value">{{ value }}</div>
<div v-if="description" class="stat-card__hint">{{ description }}</div>
</article>
</template>
<style scoped>
.metric-card {
display: grid;
gap: 12px;
min-height: 164px;
padding: 20px;
border-radius: 16px;
border: 1px solid var(--border-subtle);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.98)),
var(--surface);
box-shadow: var(--shadow-soft);
}
.metric-card__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.metric-card__label {
color: var(--text-muted);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.metric-card__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 38px;
height: 38px;
border-radius: 12px;
background: rgba(37, 99, 235, 0.1);
color: var(--accent);
font-size: 18px;
}
.metric-card__value {
color: var(--text-primary);
font-size: clamp(28px, 2vw, 40px);
font-weight: 760;
letter-spacing: -0.06em;
line-height: 1;
}
.metric-card__description {
color: var(--text-secondary);
font-size: 13px;
line-height: 1.7;
.stat-card__top { display: flex; align-items: center; justify-content: space-between; }
.stat-card__ic {
width: 36px; height: 36px; border-radius: 8px; display: grid; place-items: center;
background: var(--accent-soft); color: var(--accent);
}
</style>
+1 -1
View File
@@ -59,7 +59,7 @@ const visible = computed({
display: flex;
min-height: 100%;
flex-direction: column;
background: linear-gradient(180deg, var(--surface-raised), var(--surface));
background: var(--surface);
}
.right-drawer__header {
+12 -12
View File
@@ -238,21 +238,21 @@ const displayLabel = computed(() => {
}
.status-badge--blue {
background: rgba(37, 99, 235, 0.1);
border-color: rgba(37, 99, 235, 0.16);
color: #2563eb;
background: var(--accent-soft);
border-color: color-mix(in srgb, var(--accent) 20%, transparent);
color: var(--accent);
}
.status-badge--yellow {
background: rgba(245, 158, 11, 0.12);
border-color: rgba(245, 158, 11, 0.18);
color: #b45309;
background: var(--warning-soft);
border-color: color-mix(in srgb, var(--warning) 22%, transparent);
color: var(--warning);
}
.status-badge--red {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.16);
color: #dc2626;
background: var(--danger-soft);
border-color: color-mix(in srgb, var(--danger) 20%, transparent);
color: var(--danger);
}
.status-badge--orange {
@@ -262,9 +262,9 @@ const displayLabel = computed(() => {
}
.status-badge--indigo {
background: rgba(99, 102, 241, 0.12);
border-color: rgba(99, 102, 241, 0.18);
color: #4f46e5;
background: var(--accent-soft);
border-color: color-mix(in srgb, var(--accent) 22%, transparent);
color: var(--accent);
}
:global(html[data-theme="dark"]) .status-badge--gray {