feat: redesign responsive control room ui
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user