feat: 日志内容搜索、设置分类Tabs、日报推送、删除合并/条件清理/无分片清理
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useViewport } from "@/composables/useViewport";
|
||||
import { ElMessage } from "element-plus";
|
||||
import axios from "axios";
|
||||
import apiClient, { getApiErrorMessage } from "@/api/client";
|
||||
import type { DailyReviewReport } from "@/types";
|
||||
@@ -10,6 +11,7 @@ const router = useRouter();
|
||||
const { isMobile } = useViewport();
|
||||
|
||||
const loading = ref(false);
|
||||
const pushing = ref(false);
|
||||
const loadError = ref("");
|
||||
const selectedDate = ref(defaultReviewDate());
|
||||
const report = ref<DailyReviewReport | null>(null);
|
||||
@@ -38,6 +40,23 @@ async function loadReport() {
|
||||
}
|
||||
}
|
||||
|
||||
async function pushReport() {
|
||||
pushing.value = true;
|
||||
try {
|
||||
const { data } = await apiClient.post("/reports/daily/push", null, {
|
||||
params: {
|
||||
date: selectedDate.value,
|
||||
utcOffsetMinutes: getLocalUtcOffsetMinutes()
|
||||
}
|
||||
});
|
||||
ElMessage.success(data.message);
|
||||
} catch (error) {
|
||||
ElMessage.error(getApiErrorMessage(error, "日报推送失败。"));
|
||||
} finally {
|
||||
pushing.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openSession(recordSessionId: string) {
|
||||
router.push({ name: "record-session-detail", params: { id: recordSessionId } });
|
||||
}
|
||||
@@ -88,7 +107,7 @@ onMounted(loadReport);
|
||||
<div>
|
||||
<h1 class="page-title">回顾日报</h1>
|
||||
<p class="page-subtitle">
|
||||
按天聚合录制时长、异常和弹幕热度。日报只做页面内查看,不会主动推送。
|
||||
按天聚合录制时长、异常和弹幕热度。支持手动推送到已配置的 Webhook。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -103,6 +122,7 @@ onMounted(loadReport);
|
||||
@change="loadReport"
|
||||
/>
|
||||
<el-button @click="loadReport">刷新日报</el-button>
|
||||
<el-button type="primary" :loading="pushing" :disabled="!report" @click="pushReport">推送日报</el-button>
|
||||
</el-space>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ const filters = reactive({
|
||||
liveRoomId: "",
|
||||
level: undefined as number | undefined,
|
||||
recordTaskId: "",
|
||||
content: "",
|
||||
take: 200
|
||||
});
|
||||
|
||||
@@ -44,6 +45,7 @@ async function loadLogs() {
|
||||
liveRoomId: filters.liveRoomId || undefined,
|
||||
level: typeof filters.level === "number" ? filters.level : undefined,
|
||||
recordTaskId: filters.recordTaskId || undefined,
|
||||
content: filters.content || undefined,
|
||||
take: filters.take
|
||||
}
|
||||
});
|
||||
@@ -178,6 +180,9 @@ onBeforeUnmount(() => {
|
||||
<el-form-item label="任务 ID">
|
||||
<el-input v-model="filters.recordTaskId" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="内容关键词">
|
||||
<el-input v-model="filters.content" placeholder="按消息或详情内容搜索" />
|
||||
</el-form-item>
|
||||
<el-form-item label="数量">
|
||||
<el-input-number v-model="filters.take" :min="1" :max="500" />
|
||||
</el-form-item>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { ElMessage, ElNotification } from "element-plus";
|
||||
import apiClient, {
|
||||
@@ -30,9 +30,18 @@ const stoppingSessionId = ref<string | null>(null);
|
||||
const uploadingSessionId = ref<string | null>(null);
|
||||
const uploadingTaskId = ref<string | null>(null);
|
||||
const deleteDialogVisible = ref(false);
|
||||
const deleteDialogMode = ref<"tasks" | "sessions" | "missing-sessions">("tasks");
|
||||
const deleteDialogMode = ref<"tasks" | "sessions" | "missing-sessions" | "mixed" | "empty-sessions">("tasks");
|
||||
const deleteDialogTaskIds = ref<string[]>([]);
|
||||
const deleteDialogSessionIds = ref<string[]>([]);
|
||||
|
||||
const conditionalDialogVisible = ref(false);
|
||||
const conditionalFilter = reactive({
|
||||
checkFileExists: true as boolean | null,
|
||||
taskStatuses: [] as number[]
|
||||
});
|
||||
const conditionalTaskStatusOptions = Object.entries(taskStatusLabelMap)
|
||||
.map(([value, label]) => ({ value: Number(value), label }))
|
||||
.filter(o => o.value !== 1 && o.value !== 2 && o.value !== 3);
|
||||
const sessions = ref<RecordSession[]>([]);
|
||||
const loadError = ref("");
|
||||
const activeSessionPanels = ref<string[]>([]);
|
||||
@@ -49,11 +58,21 @@ const activeSessionCount = computed(() => sessions.value.filter((item) => isActi
|
||||
const totalTaskCount = computed(() => sessions.value.reduce((sum, item) => sum + item.tasks.length, 0));
|
||||
const totalDanmakuCount = computed(() => sessions.value.reduce((sum, item) => sum + item.totalDanmakuMessageCount, 0));
|
||||
const selectedSessionCount = computed(() => selectedSessionIds.value.length);
|
||||
const mixedSelectionLabel = computed(() => {
|
||||
const parts = [];
|
||||
if (selectedSessionCount.value > 0) {
|
||||
parts.push(`会话${selectedSessionCount.value}`);
|
||||
}
|
||||
if (selectedTasks.value.length > 0) {
|
||||
parts.push(`分片${selectedTasks.value.length}`);
|
||||
}
|
||||
return parts.length > 0 ? `(${parts.join("/")})` : "";
|
||||
});
|
||||
const deleteDialogTaskCount = computed(() => deleteDialogTaskIds.value.length);
|
||||
const deleteDialogSessionCount = computed(() => deleteDialogSessionIds.value.length);
|
||||
const deleteDialogWidth = computed(() => (isMobile.value ? "min(100vw - 24px, 560px)" : "clamp(480px, 48vw, 560px)"));
|
||||
const deleteDialogEyebrow = computed(() => {
|
||||
if (deleteDialogMode.value === "sessions") {
|
||||
if (deleteDialogMode.value === "sessions" || deleteDialogMode.value === "mixed") {
|
||||
return "会话删除";
|
||||
}
|
||||
|
||||
@@ -61,9 +80,17 @@ const deleteDialogEyebrow = computed(() => {
|
||||
return "无文件清理";
|
||||
}
|
||||
|
||||
if (deleteDialogMode.value === "empty-sessions") {
|
||||
return "空闲会话清理";
|
||||
}
|
||||
|
||||
return "删除确认";
|
||||
});
|
||||
const deleteDialogTitle = computed(() => {
|
||||
if (deleteDialogMode.value === "mixed") {
|
||||
return "删除已选会话和分片";
|
||||
}
|
||||
|
||||
if (deleteDialogMode.value === "sessions") {
|
||||
return "删除录制会话";
|
||||
}
|
||||
@@ -72,15 +99,27 @@ const deleteDialogTitle = computed(() => {
|
||||
return "清理无实体文件会话";
|
||||
}
|
||||
|
||||
if (deleteDialogMode.value === "empty-sessions") {
|
||||
return "清理无分片会话";
|
||||
}
|
||||
|
||||
return "删除分片任务";
|
||||
});
|
||||
const deleteDialogLead = computed(() => {
|
||||
if (deleteDialogMode.value === "mixed") {
|
||||
return `将删除 ${deleteDialogSessionCount.value} 个会话和 ${deleteDialogTaskCount.value} 个分片任务。会话删除会先停止录制再清理,你也可以选择同时删除本地文件。`;
|
||||
}
|
||||
|
||||
if (deleteDialogMode.value === "sessions") {
|
||||
return `将删除 ${deleteDialogSessionCount.value} 个录制会话。删除会先停止当前录制,再清理该会话下的分片记录;你也可以选择同时删除本地视频和弹幕 XML 文件。`;
|
||||
}
|
||||
|
||||
if (deleteDialogMode.value === "missing-sessions") {
|
||||
return "将自动筛出所有“分片视频文件都不存在”的录制会话,并批量清理这些会话及其分片记录。你也可以选择同时尝试删除残留的 XML 或其他本地文件。";
|
||||
return `将自动筛出所有无实体文件的录制会话并批量清理。你也可以选择同时尝试删除残留的本地文件。`;
|
||||
}
|
||||
|
||||
if (deleteDialogMode.value === "empty-sessions") {
|
||||
return `将自动找出所有没有任何分片任务的录制会话并批量删除。你也可以选择同时删除可能残留的本地文件。`;
|
||||
}
|
||||
|
||||
return `将删除 ${deleteDialogTaskCount.value} 个已选择分片任务。你可以只移除数据库记录,也可以同时清理本地视频和弹幕 XML 文件。`;
|
||||
@@ -94,7 +133,11 @@ const deleteDialogNote = computed(() => {
|
||||
return "只有当会话下所有分片都找不到实体视频文件时,才会命中这类清理。只要仍有任意一个视频文件存在,该会话就不会被误删。";
|
||||
}
|
||||
|
||||
return "“记录 + 文件”会尝试删除视频文件和对应弹幕 XML。文件不存在时不会阻断删除,但会返回警告信息。";
|
||||
if (deleteDialogMode.value === "empty-sessions") {
|
||||
return "仅清理没有任何关联分片的空会话,不影响有录制产物的会话。";
|
||||
}
|
||||
|
||||
return "记录加文件会尝试删除视频文件和对应弹幕 XML。文件不存在时不会阻断删除,但会返回警告信息。";
|
||||
});
|
||||
function isActiveStatus(status: number) {
|
||||
return status === 1 || status === 2 || status === 3 || status === 7;
|
||||
@@ -309,6 +352,41 @@ function openSessionDetail(session: RecordSession) {
|
||||
router.push({ name: "record-session-detail", params: { id: session.id } });
|
||||
}
|
||||
|
||||
function openMixedDeleteDialog() {
|
||||
if (selectedSessionCount.value === 0 && selectedTasks.value.length === 0) {
|
||||
ElMessage.warning("请先选择要删除的会话或分片。");
|
||||
return;
|
||||
}
|
||||
|
||||
deleteDialogMode.value = "mixed";
|
||||
deleteDialogSessionIds.value = [...selectedSessionIds.value];
|
||||
deleteDialogTaskIds.value = selectedTasks.value.map(item => item.id);
|
||||
deleteDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openConditionalDeleteDialog() {
|
||||
conditionalFilter.checkFileExists = true;
|
||||
conditionalFilter.taskStatuses = [];
|
||||
conditionalDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openDeleteEmptySessionsDialog() {
|
||||
deleteDialogMode.value = "empty-sessions";
|
||||
deleteDialogSessionIds.value = [];
|
||||
deleteDialogTaskIds.value = [];
|
||||
deleteDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function confirmConditionalDelete() {
|
||||
conditionalDialogVisible.value = false;
|
||||
await loadSessions({ resetPanels: false, resetSelection: false });
|
||||
|
||||
deleteDialogMode.value = "missing-sessions";
|
||||
deleteDialogSessionIds.value = [];
|
||||
deleteDialogTaskIds.value = [];
|
||||
deleteDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function openDeleteTaskDialog(taskIds: string[]) {
|
||||
if (taskIds.length === 0) {
|
||||
ElMessage.warning("请选择可删除的分片任务。");
|
||||
@@ -374,10 +452,11 @@ function applyDeleteResult(data: DeleteCompletedRecordTasksResult) {
|
||||
}
|
||||
|
||||
async function confirmDelete(deleteFiles: boolean) {
|
||||
const deletingSessions = deleteDialogMode.value === "sessions";
|
||||
const deletingSessions = deleteDialogMode.value === "sessions" || deleteDialogMode.value === "mixed";
|
||||
const deletingMissingSessions = deleteDialogMode.value === "missing-sessions";
|
||||
const deletingEmptySessions = deleteDialogMode.value === "empty-sessions";
|
||||
const selectedIds = deletingSessions ? deleteDialogSessionIds.value : deleteDialogTaskIds.value;
|
||||
if (!deletingMissingSessions && selectedIds.length === 0) {
|
||||
if (!deletingMissingSessions && !deletingEmptySessions && selectedIds.length === 0) {
|
||||
closeDeleteDialog();
|
||||
return;
|
||||
}
|
||||
@@ -385,25 +464,27 @@ async function confirmDelete(deleteFiles: boolean) {
|
||||
deleting.value = true;
|
||||
|
||||
try {
|
||||
const { data } = deletingMissingSessions
|
||||
? await apiClient.post<DeleteCompletedRecordTasksResult>("/record-sessions/delete-missing-files", {
|
||||
deleteFiles
|
||||
})
|
||||
: deletingSessions
|
||||
? await apiClient.post<DeleteCompletedRecordTasksResult>("/record-sessions/delete", {
|
||||
sessionIds: deleteDialogSessionIds.value,
|
||||
const { data } = deletingEmptySessions
|
||||
? await apiClient.post<DeleteCompletedRecordTasksResult>("/record-sessions/delete-empty?deleteFiles=" + deleteFiles)
|
||||
: deletingMissingSessions
|
||||
? await apiClient.post<DeleteCompletedRecordTasksResult>("/record-sessions/delete-missing-files", {
|
||||
deleteFiles
|
||||
})
|
||||
: await apiClient.post<DeleteCompletedRecordTasksResult>("/record-tasks/delete", {
|
||||
taskIds: deleteDialogTaskIds.value,
|
||||
deleteFiles
|
||||
});
|
||||
: deletingSessions
|
||||
? await apiClient.post<DeleteCompletedRecordTasksResult>("/record-sessions/delete", {
|
||||
sessionIds: deleteDialogSessionIds.value,
|
||||
deleteFiles
|
||||
})
|
||||
: await apiClient.post<DeleteCompletedRecordTasksResult>("/record-tasks/delete", {
|
||||
taskIds: deleteDialogTaskIds.value,
|
||||
deleteFiles
|
||||
});
|
||||
|
||||
if (deletingMissingSessions && data.deletedSessionIds.length === 0) {
|
||||
ElMessage.info("没有找到符合条件的无实体文件会话。");
|
||||
if ((deletingMissingSessions || deletingEmptySessions) && data.deletedSessionIds.length === 0) {
|
||||
ElMessage.info(deletingEmptySessions ? "没有找到无分片的空会话。" : "没有找到符合条件的无实体文件会话。");
|
||||
} else {
|
||||
ElMessage.success(
|
||||
deletingSessions || deletingMissingSessions
|
||||
deletingSessions || deletingMissingSessions || deletingEmptySessions
|
||||
? `已删除 ${data.deletedSessionIds.length} 个录制会话。`
|
||||
: `已删除 ${data.deletedTaskIds.length} 个分片任务。`
|
||||
);
|
||||
@@ -510,11 +591,11 @@ onBeforeUnmount(() => {
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="selectedTasks.length === 0"
|
||||
:disabled="selectedSessionCount === 0 && selectedTasks.length === 0"
|
||||
:loading="deleting"
|
||||
@click="openDeleteTaskDialog(selectedTasks.map((item) => item.id))"
|
||||
@click="openMixedDeleteDialog"
|
||||
>
|
||||
删除已选分片{{ selectedTasks.length > 0 ? `(${selectedTasks.length})` : "" }}
|
||||
删除已选{{ mixedSelectionLabel }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -572,8 +653,11 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
删除已选会话{{ selectedSessionCount > 0 ? `(${selectedSessionCount})` : "" }}
|
||||
</el-button>
|
||||
<el-button plain :loading="deleting" @click="openDeleteMissingSessionsDialog()">
|
||||
清理无文件会话
|
||||
<el-button plain :loading="deleting" @click="openConditionalDeleteDialog">
|
||||
按条件清理
|
||||
</el-button>
|
||||
<el-button plain :loading="deleting" @click="openDeleteEmptySessionsDialog">
|
||||
清理无分片会话
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -921,6 +1005,65 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="conditionalDialogVisible"
|
||||
width="clamp(480px, 48vw, 560px)"
|
||||
:show-close="!deleting"
|
||||
:close-on-click-modal="!deleting"
|
||||
:close-on-press-escape="!deleting"
|
||||
>
|
||||
<template #header>
|
||||
<div class="delete-dialog__header">
|
||||
<span class="delete-dialog__eyebrow">按条件清理</span>
|
||||
<h3 class="delete-dialog__title">清理满足条件的无文件会话</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="delete-dialog__body">
|
||||
<div class="delete-dialog__lead">
|
||||
筛选出所有分片视频文件不存在的会话。可额外按分片状态筛选。
|
||||
</div>
|
||||
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="检查文件存在">
|
||||
<el-switch
|
||||
v-model="conditionalFilter.checkFileExists"
|
||||
active-text="仅删除无实体文件的会话"
|
||||
inactive-text="不检查文件"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="按分片状态筛选(可多选,为空则不限)">
|
||||
<el-select
|
||||
v-model="conditionalFilter.taskStatuses"
|
||||
multiple
|
||||
placeholder="不限状态"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in conditionalTaskStatusOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="delete-dialog__note">
|
||||
<div class="delete-dialog__note-title">提示</div>
|
||||
<p>仅当所有文件检查和不限时全部满足才会命中。此操作不可恢复,请确认后再执行。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer dialog-footer--end">
|
||||
<el-button :disabled="deleting" @click="conditionalDialogVisible = false">取消</el-button>
|
||||
<el-button type="danger" :loading="deleting" @click="confirmConditionalDelete">执行清理</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ const scriptTestResults = reactive<Record<ScriptEventType, EventScriptTestResult
|
||||
const webhookTestResult = ref<WebhookTestResult | null>(null);
|
||||
const retentionCleanupResult = ref<RetentionCleanupResult | null>(null);
|
||||
const loadError = ref("");
|
||||
const activeSettingTab = ref("recording");
|
||||
|
||||
const form = reactive<SystemSettings>({
|
||||
ffmpegPath: "ffmpeg",
|
||||
@@ -558,6 +559,8 @@ onMounted(loadSettings);
|
||||
<el-alert v-if="loadError" class="page-error-alert" type="error" :closable="false" show-icon :title="loadError" />
|
||||
|
||||
<div class="settings-grid" v-loading="loading">
|
||||
<el-tabs v-model="activeSettingTab" type="border-card" class="settings-tabs">
|
||||
<el-tab-pane label="录制" name="recording">
|
||||
<el-card class="surface-card settings-card" shadow="never">
|
||||
<h3 class="section-title">录制基础</h3>
|
||||
<p class="section-subtitle">默认清晰度、输出格式、分段时长、ffmpeg 模板和网络容错参数统一维护在这里。</p>
|
||||
@@ -816,6 +819,9 @@ onMounted(loadSettings);
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="巡检与上传" name="polling">
|
||||
|
||||
<el-card class="surface-card settings-card" shadow="never">
|
||||
<h3 class="section-title">后台巡检</h3>
|
||||
<p class="section-subtitle">控制是否定时检查直播状态,并在发现开播后自动创建录制任务。</p>
|
||||
@@ -1016,6 +1022,9 @@ onMounted(loadSettings);
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="事件脚本" name="scripts">
|
||||
|
||||
<el-card class="surface-card settings-card settings-grid__full" shadow="never">
|
||||
<h3 class="section-title">事件脚本</h3>
|
||||
<p class="section-subtitle">
|
||||
@@ -1238,6 +1247,9 @@ onMounted(loadSettings);
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="通知" name="notifications">
|
||||
|
||||
<el-card class="surface-card settings-card settings-grid__full" shadow="never">
|
||||
<h3 class="section-title">Webhook 通知</h3>
|
||||
<p class="section-subtitle">固定 JSON POST 形式,支持自定义请求头。当前只发送开播和异常两类事件。</p>
|
||||
@@ -1422,6 +1434,9 @@ onMounted(loadSettings);
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="安全与平台" name="security">
|
||||
|
||||
<el-card class="surface-card settings-card settings-grid__full" shadow="never">
|
||||
<h3 class="section-title">管理员密码</h3>
|
||||
<p class="section-subtitle">修改当前登录账户的密码,修改后立即生效。</p>
|
||||
@@ -1469,6 +1484,8 @@ onMounted(loadSettings);
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user