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:
@@ -32,7 +32,7 @@ function formatDate(value?: string) {
|
||||
return value ? new Date(value).toLocaleString() : "-";
|
||||
}
|
||||
|
||||
function sessionStatusTagType(status: number) {
|
||||
function sessionStatusTagType(status: number): "" | "success" | "warning" | "danger" | "info" {
|
||||
if (status === 2) return "success";
|
||||
if (status === 5) return "danger";
|
||||
if (status === 4 || status === 6) return "info";
|
||||
@@ -73,12 +73,13 @@ onMounted(loadData);
|
||||
<div class="page-stack">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-kicker">系统概览</div>
|
||||
<h1 class="page-title">仪表盘</h1>
|
||||
<p class="page-subtitle">系统运行状态一览,包含直播间、录制会话、弹幕和存储概况。</p>
|
||||
</div>
|
||||
<el-space class="header-actions">
|
||||
<div class="header-actions">
|
||||
<el-button @click="loadData" :loading="loading">刷新</el-button>
|
||||
</el-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert v-if="loadError" class="page-error-alert" type="error" :closable="false" show-icon :title="loadError" />
|
||||
@@ -86,28 +87,23 @@ onMounted(loadData);
|
||||
<el-skeleton v-if="loading && !data" animated :rows="6" />
|
||||
|
||||
<template v-else-if="data">
|
||||
<!-- KPI Cards -->
|
||||
<!-- KPI -->
|
||||
<div class="stats-grid">
|
||||
<MetricCard label="正在录制" :value="data.activeRecordingCount">
|
||||
<template #icon><el-icon :size="18"><VideoCamera /></el-icon></template>
|
||||
</MetricCard>
|
||||
|
||||
<MetricCard label="直播间" :value="`${data.liveRoomCount} / ${data.offlineRoomCount}`" :description="`共 ${data.totalRoomCount} 个直播间`">
|
||||
<MetricCard label="直播间" :value="`${data.liveRoomCount} / ${data.offlineRoomCount}`" description="共接入">
|
||||
<template #icon><el-icon :size="18"><House /></el-icon></template>
|
||||
</MetricCard>
|
||||
|
||||
<MetricCard label="今日录制时长" :value="formatDuration(data.todayRecordingSeconds)">
|
||||
<template #icon><el-icon :size="18"><Clock /></el-icon></template>
|
||||
</MetricCard>
|
||||
|
||||
<MetricCard label="今日数据量" :value="formatDataSize(data.todayDataBytes)">
|
||||
<template #icon><el-icon :size="18"><DataAnalysis /></el-icon></template>
|
||||
</MetricCard>
|
||||
|
||||
<MetricCard label="今日弹幕" :value="data.todayDanmakuCount.toLocaleString()">
|
||||
<template #icon><el-icon :size="18"><Bell /></el-icon></template>
|
||||
</MetricCard>
|
||||
|
||||
<MetricCard
|
||||
label="24h 异常"
|
||||
:value="data.recentErrorCount"
|
||||
@@ -117,69 +113,64 @@ onMounted(loadData);
|
||||
</MetricCard>
|
||||
</div>
|
||||
|
||||
<!-- Storage Status -->
|
||||
<el-card class="surface-card" shadow="never">
|
||||
<h3 class="section-title">存储状态</h3>
|
||||
<p class="section-subtitle">录制输出路径的磁盘剩余空间和当前录制保护阈值。</p>
|
||||
<div class="storage-bar-row">
|
||||
<div class="storage-bar-card">
|
||||
<span class="storage-bar-card__label">水位线</span>
|
||||
<el-tag :type="storageTierTagType()" size="large">
|
||||
{{ storageTierLabel() }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="storage-bar-card">
|
||||
<span class="storage-bar-card__label">使用率</span>
|
||||
<span class="storage-bar-card__value">{{ data.storageStatus.usagePercent.toFixed(1) }}%</span>
|
||||
</div>
|
||||
<div class="storage-bar-card">
|
||||
<span class="storage-bar-card__label">可用空间</span>
|
||||
<span class="storage-bar-card__value">{{ formatDataSize(data.storageStatus.availableBytes) }}</span>
|
||||
</div>
|
||||
<div class="storage-bar-card">
|
||||
<span class="storage-bar-card__label">说明</span>
|
||||
<span class="storage-bar-card__value storage-bar-card__value--message">{{ data.storageStatus.message || "-" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- Queue Status -->
|
||||
<!-- storage + queue -->
|
||||
<el-row :gutter="18">
|
||||
<el-col :lg="12" :sm="24">
|
||||
<el-card class="surface-card" shadow="never">
|
||||
<h3 class="section-title">存储状态</h3>
|
||||
<p class="section-subtitle">录制输出路径的磁盘剩余空间和当前录制保护阈值。</p>
|
||||
<div style="display:flex;align-items:center;gap:28px;margin-top:18px">
|
||||
<div class="ring-wrap" :style="{ background: `conic-gradient(var(--success) ${Number(data.storageStatus.usagePercent.toFixed(1))}%, var(--surface-hover) 0)` }">
|
||||
<div class="ring-inner">
|
||||
<div class="ring-num">{{ data.storageStatus.usagePercent.toFixed(1) }}%</div>
|
||||
<div class="ring-cap">已使用</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex:1;display:grid;gap:14px">
|
||||
<div>
|
||||
<div style="font-size:12px;color:var(--text-muted)">可用空间</div>
|
||||
<div style="font-size:18px;font-weight:700;font-variant-numeric:tabular-nums">
|
||||
{{ formatDataSize(data.storageStatus.availableBytes) }}
|
||||
<span style="font-size:13px;font-weight:500;color:var(--text-muted)">/ 总计</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:24px">
|
||||
<div><div style="font-size:12px;color:var(--text-muted)">水位线</div><el-tag :type="storageTierTagType()" size="small">{{ storageTierLabel() }}</el-tag></div>
|
||||
<div><div style="font-size:12px;color:var(--text-muted)">说明</div><div style="font-size:13px;font-weight:500;color:var(--text-secondary)">{{ data.storageStatus.message || "-" }}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :lg="12" :sm="24">
|
||||
<el-card class="surface-card" shadow="never">
|
||||
<h3 class="section-title">处理队列</h3>
|
||||
<p class="section-subtitle">待转码和待上传的文件积压情况。</p>
|
||||
<div class="storage-bar-row">
|
||||
<div class="storage-bar-card">
|
||||
<span class="storage-bar-card__label">待转码</span>
|
||||
<span class="storage-bar-card__value" :style="{ color: data.pendingTranscodeCount > 0 ? 'var(--warning)' : 'var(--text-primary)' }">
|
||||
{{ data.pendingTranscodeCount }}
|
||||
</span>
|
||||
<div style="display:grid;gap:14px;margin-top:18px">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border:1px solid var(--border-subtle);border-radius:var(--radius-sm);background:var(--surface-muted)">
|
||||
<div><div style="font-weight:600;font-size:13.5px">待转码</div><div style="font-size:12px;color:var(--text-muted)">FFmpeg 后处理队列</div></div>
|
||||
<div :style="{ fontSize: '26px', fontWeight: 800, fontVariantNumeric: 'tabular-nums', color: data.pendingTranscodeCount > 0 ? 'var(--warning)' : 'var(--text-primary)' }">{{ data.pendingTranscodeCount }}</div>
|
||||
</div>
|
||||
<div class="storage-bar-card">
|
||||
<span class="storage-bar-card__label">待上传</span>
|
||||
<span class="storage-bar-card__value" :style="{ color: data.pendingUploadCount > 0 ? 'var(--warning)' : 'var(--text-primary)' }">
|
||||
{{ data.pendingUploadCount }}
|
||||
</span>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border:1px solid var(--border-subtle);border-radius:var(--radius-sm);background:var(--surface-muted)">
|
||||
<div><div style="font-weight:600;font-size:13.5px">待上传</div><div style="font-size:12px;color:var(--text-muted)">WebDAV / S3 / OpenList</div></div>
|
||||
<div :style="{ fontSize: '26px', fontWeight: 800, fontVariantNumeric: 'tabular-nums', color: data.pendingUploadCount > 0 ? 'var(--warning)' : 'var(--text-primary)' }">{{ data.pendingUploadCount }}</div>
|
||||
</div>
|
||||
<div class="storage-bar-card">
|
||||
<span class="storage-bar-card__label">积压数据量</span>
|
||||
<span class="storage-bar-card__value" :style="{ color: data.queuedDataBytes > 0 ? 'var(--warning)' : 'var(--text-primary)' }">
|
||||
{{ formatDataSize(data.queuedDataBytes) }}
|
||||
</span>
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border:1px solid var(--border-subtle);border-radius:var(--radius-sm);background:var(--surface-muted)">
|
||||
<div><div style="font-weight:600;font-size:13.5px">积压数据量</div><div style="font-size:12px;color:var(--text-muted)">等待归档的本地文件总量</div></div>
|
||||
<div style="font-size:20px;font-weight:700;font-variant-numeric:tabular-nums">{{ formatDataSize(data.queuedDataBytes) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- Recent Sessions + Top Rooms -->
|
||||
<!-- recent sessions + top rooms -->
|
||||
<el-row :gutter="18">
|
||||
<el-col :lg="12" :sm="24">
|
||||
<el-card class="surface-card" shadow="never">
|
||||
<h3 class="section-title">最近会话</h3>
|
||||
<p class="section-subtitle">最近创建的录制会话,点击可跳转至详情。</p>
|
||||
<div class="table-scroll-shell">
|
||||
<div class="table-scroll-shell" style="margin-top:14px">
|
||||
<el-table :data="data.recentSessions" class="premium-table" size="small">
|
||||
<el-table-column label="直播间" min-width="140" prop="liveRoomTitle" />
|
||||
<el-table-column label="状态" width="90">
|
||||
@@ -202,12 +193,11 @@ onMounted(loadData);
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :lg="12" :sm="24">
|
||||
<el-card class="surface-card" shadow="never">
|
||||
<h3 class="section-title">今日热门直播间</h3>
|
||||
<p class="section-subtitle">今日录制时长最长的直播间(Top 5)。</p>
|
||||
<div class="table-scroll-shell">
|
||||
<div class="table-scroll-shell" style="margin-top:14px">
|
||||
<el-table :data="data.topRooms" class="premium-table" size="small">
|
||||
<el-table-column label="直播间" min-width="130">
|
||||
<template #default="{ row }">{{ row.title || row.anchorName || "-" }}</template>
|
||||
@@ -232,50 +222,27 @@ onMounted(loadData);
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.page-stack {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
.page-stack { display: grid; gap: var(--page-gap); }
|
||||
.header-actions { align-self: center; }
|
||||
.page-error-alert { border-radius: var(--radius-md); }
|
||||
|
||||
.ring-wrap {
|
||||
--p: 32;
|
||||
width: 92px; height: 92px; flex-shrink: 0;
|
||||
border-radius: 99px; display: grid; place-items: center;
|
||||
}
|
||||
.header-actions {
|
||||
align-self: center;
|
||||
}
|
||||
.page-error-alert {
|
||||
border-radius: 14px;
|
||||
}
|
||||
.storage-bar-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px 40px;
|
||||
}
|
||||
.storage-bar-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.storage-bar-card__label {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
.storage-bar-card__value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.storage-bar-card__value--message {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--text-secondary);
|
||||
.ring-inner {
|
||||
width: 70px; height: 70px; border-radius: 99px;
|
||||
background: var(--surface); display: grid; place-items: center; text-align: center;
|
||||
}
|
||||
.ring-num { font-size: 20px; font-weight: 800; line-height: 1; }
|
||||
.ring-cap { font-size: 10.5px; color: var(--text-muted); margin-top: 2px; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
justify-content: stretch;
|
||||
}
|
||||
.header-actions :deep(.el-button) {
|
||||
flex: 1 1 0;
|
||||
margin: 0;
|
||||
}
|
||||
.header-actions { width: 100%; }
|
||||
.header-actions :deep(.el-button) { flex: 1; }
|
||||
.ring-wrap { width: 68px; height: 68px; }
|
||||
.ring-inner { width: 50px; height: 50px; }
|
||||
.ring-num { font-size: 16px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user