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>
300 lines
5.6 KiB
Vue
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>
|