feat: redesign responsive control room ui

This commit is contained in:
2026-04-26 02:18:03 +08:00
parent ff3e1fae70
commit 94f6e08dea
16 changed files with 4189 additions and 926 deletions
+188 -20
View File
@@ -5,6 +5,7 @@ import apiClient, { getApiErrorMessage } from "@/api/client";
import { useViewport } from "@/composables/useViewport";
import type { BatchLiveRoomsResult, ImportLiveRoomsResult, LiveRoom, RecordTask } from "@/types";
import {
autoStartDecisionLabelMap,
availabilityLabelMap,
outputFormatLabelMap,
recordingTemplateLabelMap,
@@ -395,6 +396,30 @@ function getRoomAvatarText(room: LiveRoom) {
return source.slice(0, 1).toUpperCase();
}
function autoStartDecisionLabel(code?: string) {
if (!code) {
return "未记录";
}
return autoStartDecisionLabelMap[code] ?? code;
}
function autoStartDecisionTagType(code?: string) {
if (code === "started") {
return "success";
}
if (code === "failed_startup") {
return "danger";
}
if (code === "skipped_storage" || code === "skipped_active_session") {
return "warning";
}
return "info";
}
function formatDate(value?: string) {
return value ? new Date(value).toLocaleString() : "-";
}
@@ -451,18 +476,19 @@ onMounted(loadRooms);
<div class="page-stack">
<div class="page-header">
<div>
<div class="page-kicker">Live Monitoring</div>
<h1 class="page-title">直播间管理</h1>
<p class="page-subtitle">
直播间会长期保留在系统里单房间配置优先于全局配置未设置的项会自动回退到系统设置
</p>
</div>
<el-space wrap class="header-actions">
<div class="page-toolbar">
<el-button @click="loadRooms">刷新列表</el-button>
<el-button @click="openImportDialog">批量导入</el-button>
<el-button :loading="exportLoading" @click="exportRooms">导出列表</el-button>
<el-button type="primary" @click="openCreateDialog">新增直播间</el-button>
</el-space>
</div>
</div>
<el-alert v-if="loadError" class="page-error-alert" type="error" :closable="false" show-icon :title="loadError" />
@@ -496,7 +522,7 @@ onMounted(loadRooms);
<h3 class="section-title">直播间列表</h3>
<p class="section-subtitle">支持刷新状态手动启动录制单房间配置覆盖以及按需删除直播间</p>
</div>
<el-space wrap class="batch-actions">
<el-space v-if="!isMobile" wrap class="batch-actions">
<span class="batch-actions__count">已选 {{ selectedRoomCount }} </span>
<el-button :disabled="!hasSelectedRooms || batchActionLoading" @click="batchSetRoomsEnabled(true)">批量启用</el-button>
<el-button :disabled="!hasSelectedRooms || batchActionLoading" @click="batchSetRoomsEnabled(false)">批量停用</el-button>
@@ -506,7 +532,78 @@ onMounted(loadRooms);
</el-space>
</div>
<div class="table-scroll-shell rooms-table-shell">
<div v-if="isMobile" class="data-card-list room-card-list">
<article v-for="row in rooms" :key="row.id" class="data-card room-card">
<div class="data-card__header">
<el-avatar :src="row.avatarUrl || row.coverUrl" :size="52" class="room-avatar">
{{ getRoomAvatarText(row) }}
</el-avatar>
<div>
<div class="data-card__title">{{ row.title || row.anchorName || row.roomId }}</div>
<div class="data-card__subtitle">{{ row.anchorName || "未知主播" }}</div>
<div class="cell-mono">{{ row.roomId }}</div>
</div>
</div>
<div class="badge-row">
<el-tag :type="availabilityTagType(row.availabilityStatus)">
{{ availabilityLabelMap[row.availabilityStatus] }}
</el-tag>
<el-tag effect="plain">{{ row.platformName }}</el-tag>
<el-tag size="small" effect="plain" :type="autoStartDecisionTagType(row.lastAutoStartDecisionCode)">
{{ autoStartDecisionLabel(row.lastAutoStartDecisionCode) }}
</el-tag>
</div>
<div class="data-card__grid">
<div>
<dt>自动开录</dt>
<dd>{{ row.lastAutoStartDecisionSummary || "暂无自动开录摘要" }}</dd>
</div>
<div>
<dt>最近巡检</dt>
<dd>{{ formatDate(row.lastCheckedAt) }}</dd>
</div>
<div style="grid-column: 1 / -1;">
<dt>房间配置</dt>
<dd>
{{ row.effectiveSettings.preferredQuality }} ·
{{ outputFormatLabelMap[row.effectiveSettings.outputFormat] }} ·
{{ saveModeLabelMap[row.effectiveSettings.saveMode] }} ·
{{ row.effectiveSettings.enableDanmakuRecording ? "弹幕开" : "弹幕关" }}
</dd>
</div>
<div v-if="row.lastAutoStartDecisionDetail" style="grid-column: 1 / -1;">
<dt>决策详情</dt>
<dd>{{ row.lastAutoStartDecisionDetail }}</dd>
</div>
</div>
<div class="room-card__toggle">
<span class="room-card__toggle-label">录制开关</span>
<el-switch
:model-value="row.isEnabled"
:loading="togglingRoomId === row.id"
inline-prompt
active-text=""
inactive-text=""
@change="setRoomEnabled(row, Boolean($event))"
/>
</div>
<div class="data-card__actions">
<el-button size="small" @click="refreshRoom(row)">刷新</el-button>
<el-button size="small" @click="openSettingsDialog(row)">配置</el-button>
<el-button size="small" type="primary" :disabled="!row.isEnabled" @click="openRecordDialog(row)">
录制
</el-button>
<el-button size="small" type="danger" plain @click="openDeleteDialog(row)">删除</el-button>
</div>
</article>
</div>
<div v-else class="table-scroll-shell rooms-table-shell">
<el-table
:data="rooms"
v-loading="loading"
@@ -548,6 +645,31 @@ onMounted(loadRooms);
</template>
</el-table-column>
<el-table-column label="自动开录" min-width="280">
<template #default="{ row }">
<div class="auto-start-cell">
<div class="auto-start-cell__head">
<el-tag size="small" :type="autoStartDecisionTagType(row.lastAutoStartDecisionCode)" effect="plain">
{{ autoStartDecisionLabel(row.lastAutoStartDecisionCode) }}
</el-tag>
<span class="table-date-text">{{ formatDate(row.lastAutoStartDecisionAt) }}</span>
</div>
<div class="cell-subtitle">{{ row.lastAutoStartDecisionSummary || "暂无自动开录摘要" }}</div>
<el-popover
v-if="row.lastAutoStartDecisionDetail"
trigger="hover"
placement="top-start"
:width="320"
>
<template #reference>
<el-button text size="small" class="auto-start-cell__detail-trigger">详情</el-button>
</template>
<div class="detail-popover">{{ row.lastAutoStartDecisionDetail }}</div>
</el-popover>
</div>
</template>
</el-table-column>
<el-table-column label="房间配置" min-width="240">
<template #default="{ row }">
<div class="config-summary">
@@ -932,7 +1054,7 @@ onMounted(loadRooms);
gap: 16px;
margin-bottom: 18px;
padding-bottom: 18px;
border-bottom: 1px solid rgba(82, 74, 63, 0.08);
border-bottom: 1px solid var(--border-subtle);
}
.batch-actions {
@@ -965,30 +1087,76 @@ onMounted(loadRooms);
box-shadow: 0 14px 28px rgba(52, 84, 112, 0.14);
}
.room-card {
gap: 16px;
}
.room-card__toggle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 14px;
border-radius: 8px;
border: 1px solid var(--border-subtle);
background: var(--surface);
}
.room-card__toggle-label {
color: var(--text-secondary);
font-size: 13px;
font-weight: 600;
}
.cell-title {
font-size: 15px;
font-weight: 700;
color: #17202b;
color: var(--text-primary);
line-height: 1.5;
}
.cell-subtitle {
margin-top: 6px;
color: #5f6d79;
color: var(--text-secondary);
font-size: 13px;
}
.cell-mono {
margin-top: 8px;
color: #7c8690;
color: var(--text-muted);
font-size: 12px;
}
.auto-start-cell {
display: grid;
gap: 6px;
}
.auto-start-cell__head {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.auto-start-cell__detail-trigger {
justify-content: flex-start;
padding: 0;
}
.detail-popover {
color: var(--text-secondary);
font-size: 13px;
line-height: 1.7;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.config-summary {
display: flex;
flex-wrap: wrap;
gap: 8px;
color: #5f6d79;
color: var(--text-secondary);
font-size: 12px;
}
@@ -997,8 +1165,8 @@ onMounted(loadRooms);
align-items: center;
padding: 4px 8px;
border-radius: 999px;
background: rgba(243, 246, 248, 0.9);
border: 1px solid rgba(95, 109, 124, 0.08);
background: var(--surface-muted);
border: 1px solid var(--border-subtle);
}
.table-date-text {
@@ -1024,8 +1192,8 @@ onMounted(loadRooms);
margin-bottom: 18px;
padding: 14px 16px;
border-radius: 14px;
background: rgba(243, 246, 248, 0.9);
color: #51606c;
background: var(--surface-muted);
color: var(--text-secondary);
line-height: 1.6;
}
@@ -1047,13 +1215,13 @@ onMounted(loadRooms);
display: grid;
gap: 12px;
padding: 14px 16px;
border: 1px solid rgba(62, 95, 124, 0.14);
border: 1px solid rgba(47, 111, 180, 0.14);
border-radius: 14px;
background: rgba(246, 248, 250, 0.82);
background: var(--surface-muted);
}
.import-result__summary {
color: #26323d;
color: var(--text-primary);
font-size: 14px;
font-weight: 700;
line-height: 1.7;
@@ -1077,7 +1245,7 @@ onMounted(loadRooms);
}
.import-result__failure code {
color: #475569;
color: var(--text-secondary);
word-break: break-all;
}
@@ -1091,7 +1259,7 @@ onMounted(loadRooms);
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #6e7c88;
color: var(--text-muted);
}
.delete-dialog__title {
@@ -1099,7 +1267,7 @@ onMounted(loadRooms);
font-size: 22px;
font-weight: 700;
letter-spacing: -0.03em;
color: #15202b;
color: var(--text-primary);
}
.delete-dialog__body {
@@ -1108,7 +1276,7 @@ onMounted(loadRooms);
}
.delete-dialog__lead {
color: #4f5d69;
color: var(--text-secondary);
font-size: 14px;
line-height: 1.75;
}