Files
live_recorder/frontend/src/components/ui/StatusBadge.vue
T
nanxunandClaude Fable 5 9df174bb0b 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>
2026-07-01 21:14:50 +08:00

300 lines
5.6 KiB
Vue

<script setup lang="ts">
import { computed } from "vue";
type BadgeContext =
| "generic"
| "availability"
| "recording"
| "task"
| "session"
| "cleanup"
| "upload"
| "boolean";
const props = withDefaults(
defineProps<{
label?: string;
status?: string | number | boolean | null;
context?: BadgeContext;
size?: "sm" | "md";
}>(),
{
label: "",
status: null,
context: "generic",
size: "md"
}
);
type Tone = "gray" | "green" | "blue" | "yellow" | "red" | "orange" | "indigo";
function normalizeValue(value: string | number | boolean | null) {
if (typeof value === "string") {
return value.trim().toLowerCase();
}
return value;
}
function resolveToneByNumber(value: number, context: BadgeContext): Tone {
if (context === "availability") {
if (value === 2) {
return "green";
}
return "gray";
}
if (context === "recording") {
if (value === 2) {
return "blue";
}
if (value === 1) {
return "green";
}
return "gray";
}
if (context === "task" || context === "session") {
if (value === 2) {
return "blue";
}
if (value === 1 || value === 7) {
return "indigo";
}
if (value === 0) {
return "yellow";
}
if (value === 3) {
return "orange";
}
if (value === 4) {
return "green";
}
if (value === 5) {
return "red";
}
return "gray";
}
if (context === "upload") {
if (value === 1) {
return "green";
}
if (value === 2) {
return "red";
}
return "gray";
}
if (context === "boolean") {
return value ? "green" : "gray";
}
return "gray";
}
function resolveToneByKeyword(value: string): Tone {
if (
value.includes("live") ||
value.includes("online") ||
value.includes("living") ||
value.includes("开播") ||
value.includes("直播中") ||
value === "started" ||
value === "completed" ||
value.includes("归档") ||
value.includes("healthy")
) {
return "green";
}
if (value.includes("recording") || value.includes("录制中") || value.includes("uploading")) {
return "blue";
}
if (
value.includes("pending") ||
value.includes("queued") ||
value.includes("待") ||
value.includes("waiting") ||
value.includes("storage")
) {
return "yellow";
}
if (value.includes("retry") || value.includes("stopping") || value.includes("停止中")) {
return "orange";
}
if (value.includes("process") || value.includes("transcod")) {
return "indigo";
}
if (
value.includes("error") ||
value.includes("fail") ||
value.includes("异常") ||
value.includes("错误") ||
value.includes("离线") ||
value === "poll_failed"
) {
return "red";
}
return "gray";
}
const tone = computed<Tone>(() => {
const normalized = normalizeValue(props.status);
if (typeof normalized === "number") {
return resolveToneByNumber(normalized, props.context);
}
if (typeof normalized === "boolean") {
return normalized ? "green" : "gray";
}
if (typeof normalized === "string" && normalized.length > 0) {
return resolveToneByKeyword(normalized);
}
return "gray";
});
const displayLabel = computed(() => {
if (props.label) {
return props.label;
}
if (props.status === null || props.status === undefined || props.status === "") {
return "--";
}
return String(props.status);
});
</script>
<template>
<span class="status-badge" :class="[`status-badge--${tone}`, `status-badge--${size}`]">
<span class="status-badge__dot"></span>
<span>{{ displayLabel }}</span>
</span>
</template>
<style scoped>
.status-badge {
display: inline-flex;
align-items: center;
gap: 8px;
max-width: 100%;
border-radius: 999px;
border: 1px solid transparent;
font-weight: 600;
white-space: nowrap;
}
.status-badge--sm {
min-height: 28px;
padding: 0 10px;
font-size: 12px;
}
.status-badge--md {
min-height: 30px;
padding: 0 12px;
font-size: 12px;
}
.status-badge__dot {
width: 8px;
height: 8px;
border-radius: 999px;
flex-shrink: 0;
background: currentColor;
}
.status-badge--gray {
background: #f8fafc;
border-color: #e2e8f0;
color: #64748b;
}
.status-badge--green {
background: rgba(34, 197, 94, 0.12);
border-color: rgba(34, 197, 94, 0.18);
color: #15803d;
}
.status-badge--blue {
background: var(--accent-soft);
border-color: color-mix(in srgb, var(--accent) 20%, transparent);
color: var(--accent);
}
.status-badge--yellow {
background: var(--warning-soft);
border-color: color-mix(in srgb, var(--warning) 22%, transparent);
color: var(--warning);
}
.status-badge--red {
background: var(--danger-soft);
border-color: color-mix(in srgb, var(--danger) 20%, transparent);
color: var(--danger);
}
.status-badge--orange {
background: rgba(249, 115, 22, 0.12);
border-color: rgba(249, 115, 22, 0.18);
color: #c2410c;
}
.status-badge--indigo {
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 {
background: rgba(148, 163, 184, 0.12);
border-color: rgba(148, 163, 184, 0.2);
color: #cbd5e1;
}
:global(html[data-theme="dark"]) .status-badge--green {
color: #86efac;
}
:global(html[data-theme="dark"]) .status-badge--blue {
color: #93c5fd;
}
:global(html[data-theme="dark"]) .status-badge--yellow {
color: #fcd34d;
}
:global(html[data-theme="dark"]) .status-badge--red {
color: #fca5a5;
}
:global(html[data-theme="dark"]) .status-badge--orange {
color: #fdba74;
}
:global(html[data-theme="dark"]) .status-badge--indigo {
color: #a5b4fc;
}
</style>