fix: refine polling and settings layout
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref } from "vue";
|
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import apiClient, { getApiErrorMessage } from "@/api/client";
|
import apiClient, { getApiErrorMessage } from "@/api/client";
|
||||||
import { useViewport } from "@/composables/useViewport";
|
import { useViewport } from "@/composables/useViewport";
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from "@/types";
|
} from "@/types";
|
||||||
|
|
||||||
const inheritValue = "__inherit__";
|
const inheritValue = "__inherit__";
|
||||||
|
const AUTO_REFRESH_INTERVAL_MS = 15000;
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const submitLoading = ref(false);
|
const submitLoading = ref(false);
|
||||||
@@ -98,6 +99,7 @@ const importDialogWidth = computed(() => (isMobile.value ? "min(100vw - 24px, 72
|
|||||||
const recordDialogWidth = computed(() => (isMobile.value ? "min(100vw - 24px, 440px)" : "440px"));
|
const recordDialogWidth = computed(() => (isMobile.value ? "min(100vw - 24px, 440px)" : "440px"));
|
||||||
const settingsDialogWidth = computed(() => (isMobile.value ? "min(100vw - 24px, 720px)" : "720px"));
|
const settingsDialogWidth = computed(() => (isMobile.value ? "min(100vw - 24px, 720px)" : "720px"));
|
||||||
const deleteDialogWidth = computed(() => (isMobile.value ? "min(100vw - 24px, 560px)" : "clamp(480px, 46vw, 560px)"));
|
const deleteDialogWidth = computed(() => (isMobile.value ? "min(100vw - 24px, 560px)" : "clamp(480px, 46vw, 560px)"));
|
||||||
|
let autoRefreshTimer: number | null = null;
|
||||||
|
|
||||||
async function loadRooms() {
|
async function loadRooms() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
@@ -113,6 +115,59 @@ async function loadRooms() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldAutoRefreshRooms() {
|
||||||
|
return !document.hidden &&
|
||||||
|
!loading.value &&
|
||||||
|
!createDialogVisible.value &&
|
||||||
|
!importDialogVisible.value &&
|
||||||
|
!recordDialogVisible.value &&
|
||||||
|
!settingsDialogVisible.value &&
|
||||||
|
!deleteDialogVisible.value &&
|
||||||
|
selectedRooms.value.length === 0 &&
|
||||||
|
!submitLoading.value &&
|
||||||
|
!importLoading.value &&
|
||||||
|
!exportLoading.value &&
|
||||||
|
!batchActionLoading.value &&
|
||||||
|
!startLoading.value &&
|
||||||
|
!settingsLoading.value &&
|
||||||
|
!deletingRoom.value &&
|
||||||
|
!togglingRoomId.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function autoRefreshRooms() {
|
||||||
|
if (!shouldAutoRefreshRooms()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await apiClient.get<LiveRoom[]>("/live-rooms");
|
||||||
|
rooms.value = data;
|
||||||
|
loadError.value = "";
|
||||||
|
} catch {
|
||||||
|
// Keep the current view stable; the background poller will try again.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAutoRefresh() {
|
||||||
|
stopAutoRefresh();
|
||||||
|
autoRefreshTimer = window.setInterval(() => {
|
||||||
|
void autoRefreshRooms();
|
||||||
|
}, AUTO_REFRESH_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAutoRefresh() {
|
||||||
|
if (autoRefreshTimer !== null) {
|
||||||
|
window.clearInterval(autoRefreshTimer);
|
||||||
|
autoRefreshTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleVisibilityChange() {
|
||||||
|
if (!document.hidden) {
|
||||||
|
void autoRefreshRooms();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function openCreateDialog() {
|
function openCreateDialog() {
|
||||||
createDialogVisible.value = true;
|
createDialogVisible.value = true;
|
||||||
}
|
}
|
||||||
@@ -469,7 +524,16 @@ function nullableStringFromSelect(value: string | number) {
|
|||||||
return value === inheritValue ? null : String(value);
|
return value === inheritValue ? null : String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadRooms);
|
onMounted(async () => {
|
||||||
|
await loadRooms();
|
||||||
|
startAutoRefresh();
|
||||||
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopAutoRefresh();
|
||||||
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1094,19 +1094,27 @@ onMounted(loadSettings);
|
|||||||
|
|
||||||
.settings-grid {
|
.settings-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(320px, 0.92fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
align-items: start;
|
align-items: start;
|
||||||
gap: 20px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-grid__full {
|
.settings-grid__full {
|
||||||
grid-column: 1 / -1;
|
grid-column: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-card :deep(.el-card__body) {
|
.settings-card :deep(.el-card__body) {
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-card :deep(.el-form) {
|
||||||
|
max-width: 1120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-card :deep(.el-input-number) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.helper-panel {
|
.helper-panel {
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
@@ -1359,12 +1367,6 @@ onMounted(loadSettings);
|
|||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.settings-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 960px) {
|
@media (max-width: 960px) {
|
||||||
.event-script-grid {
|
.event-script-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -1391,6 +1393,10 @@ onMounted(loadSettings);
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-card :deep(.el-form) {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
.event-script-example__row {
|
.event-script-example__row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
|||||||
private static readonly TimeSpan OfflineGracefulStopTimeout = TimeSpan.FromSeconds(20);
|
private static readonly TimeSpan OfflineGracefulStopTimeout = TimeSpan.FromSeconds(20);
|
||||||
private static readonly TimeSpan OfflineForcedStopTimeout = TimeSpan.FromSeconds(8);
|
private static readonly TimeSpan OfflineForcedStopTimeout = TimeSpan.FromSeconds(8);
|
||||||
private static readonly TimeSpan ExceptionEmailCooldown = TimeSpan.FromHours(6);
|
private static readonly TimeSpan ExceptionEmailCooldown = TimeSpan.FromHours(6);
|
||||||
|
private static readonly TimeSpan PollDispatchSpacing = TimeSpan.FromMilliseconds(400);
|
||||||
|
private const int MaxConcurrentLiveRoomPolls = 2;
|
||||||
|
|
||||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||||
private readonly ILogger<LiveRoomPollingBackgroundService> _logger;
|
private readonly ILogger<LiveRoomPollingBackgroundService> _logger;
|
||||||
@@ -73,15 +75,7 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
|||||||
.Select(static item => item.Id)
|
.Select(static item => item.Id)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (var liveRoomId in liveRoomIds)
|
await PollLiveRoomsAsync(liveRoomIds, settings, stoppingToken);
|
||||||
{
|
|
||||||
if (stoppingToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
await PollLiveRoomAsync(liveRoomId, settings, stoppingToken);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@@ -130,6 +124,52 @@ public sealed class LiveRoomPollingBackgroundService : BackgroundService
|
|||||||
private static Task DelayAsync(int delaySeconds, CancellationToken cancellationToken) =>
|
private static Task DelayAsync(int delaySeconds, CancellationToken cancellationToken) =>
|
||||||
Task.Delay(TimeSpan.FromSeconds(Math.Clamp(delaySeconds, 10, 3600)), cancellationToken);
|
Task.Delay(TimeSpan.FromSeconds(Math.Clamp(delaySeconds, 10, 3600)), cancellationToken);
|
||||||
|
|
||||||
|
private async Task PollLiveRoomsAsync(
|
||||||
|
IReadOnlyList<Guid> liveRoomIds,
|
||||||
|
SystemSettingsDto settings,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (liveRoomIds.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var semaphore = new SemaphoreSlim(Math.Min(MaxConcurrentLiveRoomPolls, liveRoomIds.Count));
|
||||||
|
var tasks = new List<Task>(liveRoomIds.Count);
|
||||||
|
|
||||||
|
for (var index = 0; index < liveRoomIds.Count; index++)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
await semaphore.WaitAsync(cancellationToken);
|
||||||
|
var liveRoomId = liveRoomIds[index];
|
||||||
|
tasks.Add(PollLiveRoomWithReleaseAsync(liveRoomId, settings, semaphore, cancellationToken));
|
||||||
|
|
||||||
|
if (index < liveRoomIds.Count - 1)
|
||||||
|
{
|
||||||
|
await Task.Delay(PollDispatchSpacing, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PollLiveRoomWithReleaseAsync(
|
||||||
|
Guid liveRoomId,
|
||||||
|
SystemSettingsDto settings,
|
||||||
|
SemaphoreSlim semaphore,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await PollLiveRoomAsync(liveRoomId, settings, cancellationToken);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task PollLiveRoomAsync(Guid liveRoomId, SystemSettingsDto settings, CancellationToken cancellationToken)
|
private async Task PollLiveRoomAsync(Guid liveRoomId, SystemSettingsDto settings, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using var scope = _serviceScopeFactory.CreateScope();
|
using var scope = _serviceScopeFactory.CreateScope();
|
||||||
|
|||||||
Reference in New Issue
Block a user