feat: redesign live recorder control console ui
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import apiClient, { getApiErrorMessage } from "@/api/client";
|
||||
import EmptyState from "@/components/ui/EmptyState.vue";
|
||||
import MetricCard from "@/components/ui/MetricCard.vue";
|
||||
import RightDrawer from "@/components/ui/RightDrawer.vue";
|
||||
import StatusBadge from "@/components/ui/StatusBadge.vue";
|
||||
import { useViewport } from "@/composables/useViewport";
|
||||
import type { BatchLiveRoomsResult, ImportLiveRoomsRequest, ImportLiveRoomsResult, LiveRoom, RecordTask } from "@/types";
|
||||
import {
|
||||
@@ -10,10 +14,12 @@ import {
|
||||
availabilityLabelMap,
|
||||
currentRecordingStateLabelMap,
|
||||
outputFormatLabelMap,
|
||||
platformOptionList,
|
||||
qualityOptionList,
|
||||
recordingTemplateLabelMap,
|
||||
saveModeLabelMap
|
||||
} from "@/types";
|
||||
import { House, RefreshRight, SwitchButton, VideoCamera } from "@element-plus/icons-vue";
|
||||
|
||||
const inheritValue = "__inherit__";
|
||||
const AUTO_REFRESH_INTERVAL_MS = 15000;
|
||||
@@ -38,6 +44,8 @@ const settingsRoom = ref<LiveRoom | null>(null);
|
||||
const rooms = ref<LiveRoom[]>([]);
|
||||
const selectedRooms = ref<LiveRoom[]>([]);
|
||||
const loadError = ref("");
|
||||
const roomDetailVisible = ref(false);
|
||||
const activeRoom = ref<LiveRoom | null>(null);
|
||||
const { isMobile } = useViewport();
|
||||
const roomsTableShellRef = ref<HTMLElement | null>(null);
|
||||
const roomsTableProxyRef = ref<HTMLElement | null>(null);
|
||||
@@ -85,14 +93,15 @@ const settingsForm = reactive({
|
||||
});
|
||||
|
||||
const platformOptions = [
|
||||
{ label: "自动识别", value: null },
|
||||
{ label: "抖音", value: 1 },
|
||||
{ label: "Bilibili", value: 2 },
|
||||
{ label: "虎牙", value: 3 }
|
||||
{ label: "自动识别", value: null as number | null },
|
||||
...platformOptionList.map((option) => ({
|
||||
label: option.label,
|
||||
value: option.value
|
||||
}))
|
||||
];
|
||||
|
||||
const qualityOptions = qualityOptionList;
|
||||
const qualitySupportHint = "抖音和 Bilibili 支持按画质选流;虎牙暂未实现。若目标档位不可用,平台会自动回退到最接近的可用画质。";
|
||||
const qualitySupportHint = "不同平台的可选画质不同;如果目标档位不可用,系统会自动回退到当前平台最接近的可用流。";
|
||||
const booleanOverrideOptions = [
|
||||
{ label: "跟随全局", value: inheritValue },
|
||||
{ label: "开启", value: "true" },
|
||||
@@ -108,6 +117,15 @@ const totalRooms = computed(() => rooms.value.length);
|
||||
const enabledRooms = computed(() => rooms.value.filter((item) => item.isEnabled).length);
|
||||
const disabledRooms = computed(() => rooms.value.filter((item) => !item.isEnabled).length);
|
||||
const liveRooms = computed(() => rooms.value.filter((item) => item.availabilityStatus === 2).length);
|
||||
const recordingRooms = computed(() => rooms.value.filter((item) => item.currentRecordingState === 2).length);
|
||||
const priorityRooms = computed(() => rooms.value.filter((item) => item.isPriority).length);
|
||||
const activeRoomSubtitle = computed(() => {
|
||||
if (!activeRoom.value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${activeRoom.value.platformName || "--"} · ${activeRoom.value.roomId || "--"}`;
|
||||
});
|
||||
const tableHeight = computed(() => (isMobile.value ? undefined : 700));
|
||||
const createDialogWidth = computed(() => (isMobile.value ? "min(100vw - 24px, 560px)" : "560px"));
|
||||
const importDialogWidth = computed(() => (isMobile.value ? "min(100vw - 24px, 720px)" : "720px"));
|
||||
@@ -501,6 +519,11 @@ function openRecordDialog(room: LiveRoom) {
|
||||
recordDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openRoomDetails(room: LiveRoom) {
|
||||
activeRoom.value = room;
|
||||
roomDetailVisible.value = true;
|
||||
}
|
||||
|
||||
async function copyRoomLink(room: LiveRoom) {
|
||||
const url = room.originalLiveRoomUrl || room.sourceUrl;
|
||||
|
||||
@@ -688,6 +711,14 @@ function getRoomAvatarText(room: LiveRoom) {
|
||||
return source.slice(0, 1).toUpperCase();
|
||||
}
|
||||
|
||||
function roomAvailabilityLabel(room: LiveRoom) {
|
||||
return availabilityLabelMap[room.availabilityStatus] ?? "--";
|
||||
}
|
||||
|
||||
function latestEventLabel(room: LiveRoom) {
|
||||
return room.lastAutoStartDecisionSummary || "暂无事件";
|
||||
}
|
||||
|
||||
function getQualityLabel(quality?: string | null) {
|
||||
return formatQualityLabel(quality);
|
||||
}
|
||||
@@ -802,14 +833,14 @@ onBeforeUnmount(() => {
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<div class="page-kicker">直播监控</div>
|
||||
<h1 class="page-title">直播间管理</h1>
|
||||
<h1 class="page-title">直播间监控录制控制台</h1>
|
||||
<p class="page-subtitle">
|
||||
直播间会长期保留在系统里。单房间配置优先于全局配置;未设置的项会自动回退到系统设置。
|
||||
聚合直播状态、自动开录决策和单房间录制配置,面向开播检测、异常巡检与手动介入场景。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="page-toolbar">
|
||||
<el-button @click="loadRooms">刷新列表</el-button>
|
||||
<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>
|
||||
@@ -818,34 +849,44 @@ onBeforeUnmount(() => {
|
||||
|
||||
<el-alert v-if="loadError" class="page-error-alert" type="error" :closable="false" show-icon :title="loadError" />
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-card__label">直播间总数</div>
|
||||
<div class="stat-card__value">{{ totalRooms }}</div>
|
||||
<div class="stat-card__hint">当前已接入系统的直播间数量</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card__label">启用中</div>
|
||||
<div class="stat-card__value">{{ enabledRooms }}</div>
|
||||
<div class="stat-card__hint">参与后台巡检与自动录制</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card__label">已停用</div>
|
||||
<div class="stat-card__value">{{ disabledRooms }}</div>
|
||||
<div class="stat-card__hint">保留解析结果,但不再自动录制</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card__label">当前开播</div>
|
||||
<div class="stat-card__value">{{ liveRooms }}</div>
|
||||
<div class="stat-card__hint">基于最近一次状态检测</div>
|
||||
<div class="live-console-grid">
|
||||
<div class="stats-grid">
|
||||
<MetricCard label="在线直播间" :value="liveRooms" description="基于最近一次巡检识别为开播中的房间" :icon="House" />
|
||||
<MetricCard label="录制中任务" :value="recordingRooms" description="当前处于录制状态的直播间数量" :icon="VideoCamera" />
|
||||
<MetricCard label="启用房间" :value="enabledRooms" description="参与自动检测与自动录制的直播间" :icon="SwitchButton" />
|
||||
<MetricCard label="重点监控" :value="priorityRooms" description="标记为重点巡检与优先关注的房间" :icon="RefreshRight" />
|
||||
</div>
|
||||
|
||||
<el-card class="surface-card focus-card" shadow="never">
|
||||
<div class="focus-card__header">
|
||||
<div>
|
||||
<div class="focus-card__eyebrow">监控概览</div>
|
||||
<h3 class="section-title">直播采集态势</h3>
|
||||
</div>
|
||||
<StatusBadge
|
||||
:label="recordingRooms > 0 ? '录制通道活跃' : '等待开播'"
|
||||
:status="recordingRooms > 0"
|
||||
context="boolean"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="focus-card__description">
|
||||
当前共接入 {{ totalRooms }} 个直播间,其中 {{ enabledRooms }} 个处于自动巡检范围,{{ disabledRooms }} 个处于保留但停用状态。
|
||||
</p>
|
||||
|
||||
<div class="focus-card__chips">
|
||||
<span class="info-pill">自动开录复用现有后端策略</span>
|
||||
<span class="info-pill">单房间配置优先于全局设置</span>
|
||||
<span class="info-pill">无实时吞吐字段时统一显示占位</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-card class="surface-card table-card" shadow="never">
|
||||
<el-card class="surface-card table-card" shadow="never" v-loading="loading">
|
||||
<div class="toolbar-row">
|
||||
<div>
|
||||
<h3 class="section-title">直播间列表</h3>
|
||||
<p class="section-subtitle">支持刷新状态、手动启动录制、单房间配置覆盖,以及按需删除直播间。</p>
|
||||
<p class="section-subtitle">继续使用真实接口数据,统一强化为监控控制台视图,支持查看详情、手动录制和单房间配置覆盖。</p>
|
||||
</div>
|
||||
<el-space v-if="!isMobile" wrap class="batch-actions">
|
||||
<span class="batch-actions__count">已选 {{ selectedRoomCount }} 个</span>
|
||||
@@ -854,10 +895,18 @@ onBeforeUnmount(() => {
|
||||
<el-button type="danger" plain :disabled="!hasSelectedRooms || batchActionLoading" @click="openBatchDeleteDialog">
|
||||
批量删除
|
||||
</el-button>
|
||||
</el-space>
|
||||
</el-space>
|
||||
</div>
|
||||
|
||||
<div v-if="isMobile" class="data-card-list room-card-list">
|
||||
<EmptyState
|
||||
v-if="!loading && rooms.length === 0"
|
||||
title="暂无数据"
|
||||
description="当前筛选条件下没有可展示内容"
|
||||
action-text="刷新列表"
|
||||
@action="loadRooms"
|
||||
/>
|
||||
|
||||
<div v-else-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">
|
||||
@@ -874,30 +923,42 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<div class="badge-row">
|
||||
<el-tag :type="currentRecordingStateTagType(row.currentRecordingState)">
|
||||
{{ currentRecordingStateLabelMap[row.currentRecordingState] }}
|
||||
</el-tag>
|
||||
<el-tag effect="plain">{{ row.platformName }}</el-tag>
|
||||
<el-tag v-if="row.isPinned" size="small" effect="plain">置顶</el-tag>
|
||||
<el-tag v-if="row.isPriority" size="small" effect="plain" type="danger">重点</el-tag>
|
||||
<el-tag size="small" effect="plain" :type="autoStartDecisionTagType(row.lastAutoStartDecisionCode)">
|
||||
{{ autoStartDecisionLabel(row.lastAutoStartDecisionCode) }}
|
||||
</el-tag>
|
||||
<StatusBadge :label="roomAvailabilityLabel(row)" :status="row.availabilityStatus" context="availability" />
|
||||
<StatusBadge
|
||||
:label="currentRecordingStateLabelMap[row.currentRecordingState]"
|
||||
:status="row.currentRecordingState"
|
||||
context="recording"
|
||||
/>
|
||||
<StatusBadge :label="row.platformName || '--'" :status="row.platformName || 'unknown'" />
|
||||
<StatusBadge v-if="row.isPinned" label="置顶" status="completed" size="sm" />
|
||||
<StatusBadge v-if="row.isPriority" label="重点" status="retrying" size="sm" />
|
||||
</div>
|
||||
|
||||
<div class="data-card__grid">
|
||||
<div>
|
||||
<dt>自动开录</dt>
|
||||
<dd>{{ row.lastAutoStartDecisionSummary || "暂无自动开录摘要" }}</dd>
|
||||
<dt>最近事件</dt>
|
||||
<dd>{{ latestEventLabel(row) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>最近巡检</dt>
|
||||
<dd>{{ formatDate(row.lastCheckedAt) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>在线人数</dt>
|
||||
<dd>--</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>码率</dt>
|
||||
<dd>--</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>检测间隔</dt>
|
||||
<dd>{{ formatPollingInterval(row.pollingIntervalSecondsOverride) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>采集账号</dt>
|
||||
<dd>--</dd>
|
||||
</div>
|
||||
<div style="grid-column: 1 / -1;">
|
||||
<dt>房间配置</dt>
|
||||
<dd>
|
||||
@@ -926,6 +987,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<div class="data-card__actions">
|
||||
<el-button size="small" @click="openRoomDetails(row)">查看详情</el-button>
|
||||
<el-button size="small" @click="refreshRoom(row)">刷新</el-button>
|
||||
<el-button size="small" @click="openSettingsDialog(row)">配置</el-button>
|
||||
<el-button size="small" @click="copyRoomLink(row)">复制链接</el-button>
|
||||
@@ -950,7 +1012,6 @@ onBeforeUnmount(() => {
|
||||
|
||||
<el-table
|
||||
:data="rooms"
|
||||
v-loading="loading"
|
||||
:height="tableHeight"
|
||||
class="premium-table rooms-table"
|
||||
table-layout="fixed"
|
||||
@@ -959,53 +1020,59 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<el-table-column type="selection" width="48" />
|
||||
|
||||
<el-table-column label="头像" width="88">
|
||||
<el-table-column label="直播间" min-width="340">
|
||||
<template #default="{ row }">
|
||||
<el-avatar :src="row.avatarUrl || row.coverUrl" :size="56" class="room-avatar">
|
||||
{{ getRoomAvatarText(row) }}
|
||||
</el-avatar>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="直播间" min-width="320">
|
||||
<template #default="{ row }">
|
||||
<div class="cell-title">{{ row.title || row.anchorName || row.roomId }}</div>
|
||||
<div class="cell-subtitle">{{ row.anchorName || "未知主播" }}</div>
|
||||
<div class="room-summary-cell">
|
||||
<el-avatar :src="row.avatarUrl || row.coverUrl" :size="52" class="room-avatar">
|
||||
{{ getRoomAvatarText(row) }}
|
||||
</el-avatar>
|
||||
<div class="room-summary-cell__copy">
|
||||
<div class="cell-title">{{ row.title || row.anchorName || row.roomId }}</div>
|
||||
<div class="cell-subtitle">{{ row.anchorName || "未知主播" }}</div>
|
||||
<div class="config-summary">
|
||||
<span>{{ row.platformName || "--" }}</span>
|
||||
<span>{{ row.roomId || "--" }}</span>
|
||||
<span>{{ formatPollingInterval(row.pollingIntervalSecondsOverride) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="row.alias && row.alias !== row.anchorName" class="cell-subtitle">别名:{{ row.alias }}</div>
|
||||
<div v-if="row.remark" class="cell-subtitle">{{ row.remark }}</div>
|
||||
<div class="config-summary">
|
||||
<span v-if="row.isPinned">置顶</span>
|
||||
<span v-if="row.isPriority">重点</span>
|
||||
<span>{{ formatPollingInterval(row.pollingIntervalSecondsOverride) }}</span>
|
||||
<div class="badge-row">
|
||||
<StatusBadge v-if="row.isPinned" label="置顶" status="completed" size="sm" />
|
||||
<StatusBadge v-if="row.isPriority" label="重点" status="retrying" size="sm" />
|
||||
</div>
|
||||
<div class="monospace cell-mono">{{ row.roomId }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="平台" width="110">
|
||||
<el-table-column label="直播状态" width="128">
|
||||
<template #default="{ row }">
|
||||
<el-tag effect="plain">{{ row.platformName }}</el-tag>
|
||||
<StatusBadge :label="roomAvailabilityLabel(row)" :status="row.availabilityStatus" context="availability" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="直播状态" width="120">
|
||||
<el-table-column label="录制状态" width="128">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="currentRecordingStateTagType(row.currentRecordingState)">
|
||||
{{ currentRecordingStateLabelMap[row.currentRecordingState] }}
|
||||
</el-tag>
|
||||
<StatusBadge
|
||||
:label="currentRecordingStateLabelMap[row.currentRecordingState]"
|
||||
:status="row.currentRecordingState"
|
||||
context="recording"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="自动开录" min-width="280">
|
||||
<el-table-column label="最近事件" min-width="300">
|
||||
<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>
|
||||
<StatusBadge
|
||||
size="sm"
|
||||
:label="autoStartDecisionLabel(row.lastAutoStartDecisionCode)"
|
||||
:status="row.lastAutoStartDecisionCode || 'unknown'"
|
||||
/>
|
||||
<span class="table-date-text">{{ formatDate(row.lastAutoStartDecisionAt) }}</span>
|
||||
</div>
|
||||
<div class="cell-subtitle">{{ row.lastAutoStartDecisionSummary || "暂无自动开录摘要" }}</div>
|
||||
<div class="cell-subtitle">{{ latestEventLabel(row) }}</div>
|
||||
<el-popover
|
||||
v-if="row.lastAutoStartDecisionDetail"
|
||||
trigger="hover"
|
||||
@@ -1032,7 +1099,7 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="录制开关" width="128">
|
||||
<el-table-column label="录制开关" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
:model-value="row.isEnabled"
|
||||
@@ -1045,6 +1112,16 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="监控字段" width="168">
|
||||
<template #default="{ row }">
|
||||
<div class="table-placeholder-grid">
|
||||
<span>在线人数 --</span>
|
||||
<span>码率 --</span>
|
||||
<span>采集账号 --</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="最近检测" width="180">
|
||||
<template #default="{ row }">
|
||||
<div class="table-date-text">{{ formatDate(row.lastCheckedAt) }}</div>
|
||||
@@ -1054,6 +1131,7 @@ onBeforeUnmount(() => {
|
||||
<el-table-column label="操作" width="380">
|
||||
<template #default="{ row }">
|
||||
<div class="room-actions-cell">
|
||||
<el-button size="small" @click="openRoomDetails(row)">查看</el-button>
|
||||
<el-button size="small" @click="refreshRoom(row)">刷新</el-button>
|
||||
<el-button size="small" @click="openSettingsDialog(row)">配置</el-button>
|
||||
<el-button size="small" @click="copyRoomLink(row)">复制链接</el-button>
|
||||
@@ -1069,6 +1147,76 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<RightDrawer
|
||||
v-model="roomDetailVisible"
|
||||
:title="activeRoom?.title || activeRoom?.anchorName || activeRoom?.roomId || '直播间详情'"
|
||||
:subtitle="activeRoomSubtitle"
|
||||
>
|
||||
<div v-if="activeRoom" class="detail-panel">
|
||||
<div class="detail-panel__hero">
|
||||
<el-avatar :src="activeRoom.avatarUrl || activeRoom.coverUrl" :size="64" class="room-avatar">
|
||||
{{ getRoomAvatarText(activeRoom) }}
|
||||
</el-avatar>
|
||||
<div>
|
||||
<div class="detail-panel__title">{{ activeRoom.anchorName || "未知主播" }}</div>
|
||||
<div class="detail-panel__meta">{{ activeRoom.originalLiveRoomUrl || activeRoom.sourceUrl || "--" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="badge-row">
|
||||
<StatusBadge :label="roomAvailabilityLabel(activeRoom)" :status="activeRoom.availabilityStatus" context="availability" />
|
||||
<StatusBadge
|
||||
:label="currentRecordingStateLabelMap[activeRoom.currentRecordingState]"
|
||||
:status="activeRoom.currentRecordingState"
|
||||
context="recording"
|
||||
/>
|
||||
<StatusBadge :label="activeRoom.platformName || '--'" :status="activeRoom.platformName || 'unknown'" />
|
||||
</div>
|
||||
|
||||
<el-descriptions :column="1" border class="detail-panel__descriptions">
|
||||
<el-descriptions-item label="直播间名称">
|
||||
{{ activeRoom.title || activeRoom.anchorName || activeRoom.roomId || "--" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="平台 + Room ID">
|
||||
{{ activeRoom.platformName || "--" }} · {{ activeRoom.roomId || "--" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="直播状态">
|
||||
{{ roomAvailabilityLabel(activeRoom) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="录制状态">
|
||||
{{ currentRecordingStateLabelMap[activeRoom.currentRecordingState] || "--" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="在线人数">--</el-descriptions-item>
|
||||
<el-descriptions-item label="码率">--</el-descriptions-item>
|
||||
<el-descriptions-item label="录制时长">--</el-descriptions-item>
|
||||
<el-descriptions-item label="采集账号">--</el-descriptions-item>
|
||||
<el-descriptions-item label="最近事件">
|
||||
{{ latestEventLabel(activeRoom) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最近巡检">
|
||||
{{ formatDate(activeRoom.lastCheckedAt) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="自动开录详情">
|
||||
{{ activeRoom.lastAutoStartDecisionDetail || "--" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="房间配置">
|
||||
{{ getQualityLabel(activeRoom.effectiveSettings.preferredQuality) }} ·
|
||||
{{ outputFormatLabelMap[activeRoom.effectiveSettings.outputFormat] }} ·
|
||||
{{ saveModeLabelMap[activeRoom.effectiveSettings.saveMode] }} ·
|
||||
{{ activeRoom.effectiveSettings.enableDanmakuRecording ? "弹幕开" : "弹幕关" }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="roomDetailVisible = false">关闭</el-button>
|
||||
<el-button @click="activeRoom && openSettingsDialog(activeRoom)">配置</el-button>
|
||||
<el-button type="primary" :disabled="!activeRoom?.isEnabled" @click="activeRoom && openRecordDialog(activeRoom)">
|
||||
开始录制
|
||||
</el-button>
|
||||
</template>
|
||||
</RightDrawer>
|
||||
|
||||
<el-dialog
|
||||
v-model="createDialogVisible"
|
||||
class="form-dialog"
|
||||
@@ -1083,7 +1231,7 @@ onBeforeUnmount(() => {
|
||||
v-model="createForm.url"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="粘贴抖音 / Bilibili / 虎牙直播间链接,或直接输入 roomId"
|
||||
placeholder="粘贴 Douyin / Bilibili / Huya / Douyu / Kuaishou / TikTok / YouTube / Twitch 等直播间链接"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
@@ -1129,7 +1277,7 @@ onBeforeUnmount(() => {
|
||||
v-model="importForm.content"
|
||||
type="textarea"
|
||||
:rows="12"
|
||||
placeholder="https://live.douyin.com/845878323112,主播: 熊宇一 https://live.douyin.com/262011082654"
|
||||
placeholder="https://live.douyin.com/845878323112,主播: 熊宇一 https://www.twitch.tv/example_channel https://www.youtube.com/watch?v=example12345"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
@@ -1455,6 +1603,46 @@ onBeforeUnmount(() => {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.live-console-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.7fr) minmax(320px, 0.9fr);
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.focus-card :deep(.el-card__body) {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.focus-card__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.focus-card__eyebrow {
|
||||
margin-bottom: 8px;
|
||||
color: var(--accent);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.focus-card__description {
|
||||
margin: 0;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.focus-card__chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
align-self: center;
|
||||
}
|
||||
@@ -1518,6 +1706,16 @@ onBeforeUnmount(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.room-summary-cell {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.room-summary-cell__copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.room-avatar {
|
||||
box-shadow: 0 14px 28px rgba(52, 84, 112, 0.14);
|
||||
}
|
||||
@@ -1617,10 +1815,46 @@ onBeforeUnmount(() => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table-placeholder-grid {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.room-actions-cell :deep(.el-button) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.detail-panel {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.detail-panel__hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--border-subtle);
|
||||
background: var(--surface-muted);
|
||||
}
|
||||
|
||||
.detail-panel__title {
|
||||
color: var(--text-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.detail-panel__meta {
|
||||
margin-top: 6px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.field-help {
|
||||
margin-top: -8px;
|
||||
margin-bottom: 14px;
|
||||
@@ -1798,6 +2032,10 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.live-console-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.toolbar-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user