feat: localize quality labels and add quality template token
This commit is contained in:
@@ -572,6 +572,28 @@ export const recordingTemplateLabelMap: Record<number, string> = {
|
|||||||
2: "归档 TS"
|
2: "归档 TS"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const qualityLabelMap: Record<string, string> = {
|
||||||
|
origin: "原画",
|
||||||
|
FULL_HD: "超清",
|
||||||
|
HD: "高清",
|
||||||
|
SD: "标清"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const qualityOptionList = [
|
||||||
|
{ value: "origin", label: qualityLabelMap.origin },
|
||||||
|
{ value: "FULL_HD", label: qualityLabelMap.FULL_HD },
|
||||||
|
{ value: "HD", label: qualityLabelMap.HD },
|
||||||
|
{ value: "SD", label: qualityLabelMap.SD }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export function formatQualityLabel(value?: string | null) {
|
||||||
|
if (!value) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return qualityLabelMap[value] ?? value;
|
||||||
|
}
|
||||||
|
|
||||||
export const autoStartDecisionLabelMap: Record<string, string> = {
|
export const autoStartDecisionLabelMap: Record<string, string> = {
|
||||||
started: "已启动",
|
started: "已启动",
|
||||||
skipped_disabled: "已禁用",
|
skipped_disabled: "已禁用",
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ import { useViewport } from "@/composables/useViewport";
|
|||||||
import type { BatchLiveRoomsResult, ImportLiveRoomsRequest, ImportLiveRoomsResult, LiveRoom, RecordTask } from "@/types";
|
import type { BatchLiveRoomsResult, ImportLiveRoomsRequest, ImportLiveRoomsResult, LiveRoom, RecordTask } from "@/types";
|
||||||
import {
|
import {
|
||||||
autoStartDecisionLabelMap,
|
autoStartDecisionLabelMap,
|
||||||
|
formatQualityLabel,
|
||||||
availabilityLabelMap,
|
availabilityLabelMap,
|
||||||
currentRecordingStateLabelMap,
|
currentRecordingStateLabelMap,
|
||||||
outputFormatLabelMap,
|
outputFormatLabelMap,
|
||||||
|
qualityOptionList,
|
||||||
recordingTemplateLabelMap,
|
recordingTemplateLabelMap,
|
||||||
saveModeLabelMap
|
saveModeLabelMap
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
@@ -89,7 +91,8 @@ const platformOptions = [
|
|||||||
{ label: "虎牙", value: 3 }
|
{ label: "虎牙", value: 3 }
|
||||||
];
|
];
|
||||||
|
|
||||||
const qualityOptions = ["origin", "FULL_HD", "HD", "SD"];
|
const qualityOptions = qualityOptionList;
|
||||||
|
const qualitySupportHint = "抖音和 Bilibili 支持按画质选流;虎牙暂未实现。若目标档位不可用,平台会自动回退到最接近的可用画质。";
|
||||||
const booleanOverrideOptions = [
|
const booleanOverrideOptions = [
|
||||||
{ label: "跟随全局", value: inheritValue },
|
{ label: "跟随全局", value: inheritValue },
|
||||||
{ label: "开启", value: "true" },
|
{ label: "开启", value: "true" },
|
||||||
@@ -143,6 +146,35 @@ function getRoomsTableScrollWrap() {
|
|||||||
shell.querySelector<HTMLElement>(".el-table__body-wrapper");
|
shell.querySelector<HTMLElement>(".el-table__body-wrapper");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRoomsTableContentWidth(wrap: HTMLElement) {
|
||||||
|
const shell = roomsTableShellRef.value;
|
||||||
|
|
||||||
|
if (!shell) {
|
||||||
|
return wrap.scrollWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
const widths = [
|
||||||
|
wrap.scrollWidth,
|
||||||
|
wrap.firstElementChild?.scrollWidth ?? 0,
|
||||||
|
shell.querySelector<HTMLElement>(".el-table__body table")?.scrollWidth ?? 0,
|
||||||
|
shell.querySelector<HTMLElement>(".el-table__header table")?.scrollWidth ?? 0
|
||||||
|
];
|
||||||
|
|
||||||
|
const wrapRect = wrap.getBoundingClientRect();
|
||||||
|
let maxRight = 0;
|
||||||
|
|
||||||
|
shell.querySelectorAll<HTMLElement>(
|
||||||
|
".el-table__header th, .el-table__body td, .room-actions-cell, .config-summary, .auto-start-cell"
|
||||||
|
).forEach((element) => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
maxRight = Math.max(maxRight, rect.right - wrapRect.left + wrap.scrollLeft);
|
||||||
|
});
|
||||||
|
|
||||||
|
widths.push(Math.ceil(maxRight) + 24);
|
||||||
|
|
||||||
|
return Math.max(...widths);
|
||||||
|
}
|
||||||
|
|
||||||
function handleRoomsTableBodyScroll() {
|
function handleRoomsTableBodyScroll() {
|
||||||
if (syncingRoomsTableProxy) {
|
if (syncingRoomsTableProxy) {
|
||||||
return;
|
return;
|
||||||
@@ -235,11 +267,11 @@ async function syncRoomsTableProxyScroll() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollWidth = Math.max(wrap.scrollWidth, wrap.firstElementChild?.scrollWidth ?? 0);
|
const scrollWidth = getRoomsTableContentWidth(wrap);
|
||||||
const clientWidth = wrap.clientWidth;
|
const clientWidth = wrap.clientWidth;
|
||||||
const canScrollHorizontally = scrollWidth > clientWidth + 1;
|
const canScrollHorizontally = scrollWidth > clientWidth + 1;
|
||||||
|
|
||||||
roomsTableProxyInnerWidth.value = scrollWidth;
|
roomsTableProxyInnerWidth.value = canScrollHorizontally ? scrollWidth : 0;
|
||||||
showRoomsTableProxyScroll.value = canScrollHorizontally;
|
showRoomsTableProxyScroll.value = canScrollHorizontally;
|
||||||
|
|
||||||
if (canScrollHorizontally && proxy && Math.abs(proxy.scrollLeft - wrap.scrollLeft) > 1) {
|
if (canScrollHorizontally && proxy && Math.abs(proxy.scrollLeft - wrap.scrollLeft) > 1) {
|
||||||
@@ -653,6 +685,10 @@ function getRoomAvatarText(room: LiveRoom) {
|
|||||||
return source.slice(0, 1).toUpperCase();
|
return source.slice(0, 1).toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getQualityLabel(quality?: string | null) {
|
||||||
|
return formatQualityLabel(quality);
|
||||||
|
}
|
||||||
|
|
||||||
function autoStartDecisionLabel(code?: string) {
|
function autoStartDecisionLabel(code?: string) {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return "未记录";
|
return "未记录";
|
||||||
@@ -862,7 +898,7 @@ onBeforeUnmount(() => {
|
|||||||
<div style="grid-column: 1 / -1;">
|
<div style="grid-column: 1 / -1;">
|
||||||
<dt>房间配置</dt>
|
<dt>房间配置</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{{ row.effectiveSettings.preferredQuality }} ·
|
{{ getQualityLabel(row.effectiveSettings.preferredQuality) }} ·
|
||||||
{{ outputFormatLabelMap[row.effectiveSettings.outputFormat] }} ·
|
{{ outputFormatLabelMap[row.effectiveSettings.outputFormat] }} ·
|
||||||
{{ saveModeLabelMap[row.effectiveSettings.saveMode] }} ·
|
{{ saveModeLabelMap[row.effectiveSettings.saveMode] }} ·
|
||||||
{{ row.effectiveSettings.enableDanmakuRecording ? "弹幕开" : "弹幕关" }}
|
{{ row.effectiveSettings.enableDanmakuRecording ? "弹幕开" : "弹幕关" }}
|
||||||
@@ -914,7 +950,7 @@ onBeforeUnmount(() => {
|
|||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
:height="tableHeight"
|
:height="tableHeight"
|
||||||
class="premium-table rooms-table"
|
class="premium-table rooms-table"
|
||||||
table-layout="auto"
|
table-layout="fixed"
|
||||||
row-key="id"
|
row-key="id"
|
||||||
@selection-change="handleRoomSelectionChange"
|
@selection-change="handleRoomSelectionChange"
|
||||||
>
|
>
|
||||||
@@ -982,10 +1018,10 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="房间配置" min-width="240">
|
<el-table-column label="房间配置" width="280">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="config-summary">
|
<div class="config-summary">
|
||||||
<span>画质 {{ row.effectiveSettings.preferredQuality }}</span>
|
<span>画质 {{ getQualityLabel(row.effectiveSettings.preferredQuality) }}</span>
|
||||||
<span>{{ outputFormatLabelMap[row.effectiveSettings.outputFormat] }}</span>
|
<span>{{ outputFormatLabelMap[row.effectiveSettings.outputFormat] }}</span>
|
||||||
<span>{{ saveModeLabelMap[row.effectiveSettings.saveMode] }}</span>
|
<span>{{ saveModeLabelMap[row.effectiveSettings.saveMode] }}</span>
|
||||||
<span>{{ row.effectiveSettings.enableDanmakuRecording ? "弹幕开" : "弹幕关" }}</span>
|
<span>{{ row.effectiveSettings.enableDanmakuRecording ? "弹幕开" : "弹幕关" }}</span>
|
||||||
@@ -1012,7 +1048,7 @@ onBeforeUnmount(() => {
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
<el-table-column label="操作" width="320">
|
<el-table-column label="操作" width="380">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="room-actions-cell">
|
<div class="room-actions-cell">
|
||||||
<el-button size="small" @click="refreshRoom(row)">刷新</el-button>
|
<el-button size="small" @click="refreshRoom(row)">刷新</el-button>
|
||||||
@@ -1172,10 +1208,17 @@ onBeforeUnmount(() => {
|
|||||||
<el-form label-position="top">
|
<el-form label-position="top">
|
||||||
<el-form-item label="清晰度">
|
<el-form-item label="清晰度">
|
||||||
<el-select v-model="recordForm.preferredQuality">
|
<el-select v-model="recordForm.preferredQuality">
|
||||||
<el-option v-for="option in qualityOptions" :key="option" :label="option" :value="option" />
|
<el-option
|
||||||
|
v-for="option in qualityOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<div class="field-help">{{ qualitySupportHint }}</div>
|
||||||
|
|
||||||
<el-form-item label="输出格式">
|
<el-form-item label="输出格式">
|
||||||
<el-select v-model="recordForm.outputFormat">
|
<el-select v-model="recordForm.outputFormat">
|
||||||
<el-option
|
<el-option
|
||||||
@@ -1241,10 +1284,17 @@ onBeforeUnmount(() => {
|
|||||||
<el-form-item label="默认画质">
|
<el-form-item label="默认画质">
|
||||||
<el-select v-model="settingsForm.preferredQualityOverride">
|
<el-select v-model="settingsForm.preferredQualityOverride">
|
||||||
<el-option label="跟随全局" :value="inheritValue" />
|
<el-option label="跟随全局" :value="inheritValue" />
|
||||||
<el-option v-for="option in qualityOptions" :key="option" :label="option" :value="option" />
|
<el-option
|
||||||
|
v-for="option in qualityOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<div class="field-help field-help--full">{{ qualitySupportHint }}</div>
|
||||||
|
|
||||||
<el-form-item label="默认输出格式">
|
<el-form-item label="默认输出格式">
|
||||||
<el-select v-model="settingsForm.outputFormatOverride">
|
<el-select v-model="settingsForm.outputFormatOverride">
|
||||||
<el-option label="跟随全局" :value="inheritValue" />
|
<el-option label="跟随全局" :value="inheritValue" />
|
||||||
@@ -1454,7 +1504,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rooms-table {
|
.rooms-table {
|
||||||
min-width: 1380px;
|
min-width: 1920px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rooms-table :deep(.el-table__cell) {
|
.rooms-table :deep(.el-table__cell) {
|
||||||
@@ -1462,7 +1512,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rooms-table :deep(.cell) {
|
.rooms-table :deep(.cell) {
|
||||||
overflow: visible;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-avatar {
|
.room-avatar {
|
||||||
@@ -1557,16 +1607,29 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.room-actions-cell {
|
.room-actions-cell {
|
||||||
display: flex;
|
display: grid;
|
||||||
align-items: center;
|
grid-template-columns: repeat(2, minmax(0, max-content));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
flex-wrap: wrap;
|
justify-content: flex-start;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-actions-cell :deep(.el-button) {
|
.room-actions-cell :deep(.el-button) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field-help {
|
||||||
|
margin-top: -8px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-help--full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-note {
|
.settings-note {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
RecordSessionTimelineSegment
|
RecordSessionTimelineSegment
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
import {
|
import {
|
||||||
|
formatQualityLabel,
|
||||||
logLevelLabelMap,
|
logLevelLabelMap,
|
||||||
outputFormatLabelMap,
|
outputFormatLabelMap,
|
||||||
platformLabelMap,
|
platformLabelMap,
|
||||||
@@ -277,7 +278,7 @@ onMounted(loadDetail);
|
|||||||
{{ outputFormatLabelMap[detail.session.outputFormat] }}
|
{{ outputFormatLabelMap[detail.session.outputFormat] }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="清晰度">
|
<el-descriptions-item label="清晰度">
|
||||||
<span class="monospace">{{ detail.session.preferredQuality }}</span>
|
<span>{{ formatQualityLabel(detail.session.preferredQuality) }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="分片数量">
|
<el-descriptions-item label="分片数量">
|
||||||
{{ detail.session.segmentCount }}
|
{{ detail.session.segmentCount }}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import apiClient, { getApiErrorMessage } from "@/api/client";
|
|||||||
import { useViewport } from "@/composables/useViewport";
|
import { useViewport } from "@/composables/useViewport";
|
||||||
import type { RecordArtifactUploadItemResult, RecordPreviewTicket, RecordTaskDetail } from "@/types";
|
import type { RecordArtifactUploadItemResult, RecordPreviewTicket, RecordTaskDetail } from "@/types";
|
||||||
import {
|
import {
|
||||||
|
formatQualityLabel,
|
||||||
logLevelLabelMap,
|
logLevelLabelMap,
|
||||||
outputFormatLabelMap,
|
outputFormatLabelMap,
|
||||||
sessionStatusLabelMap,
|
sessionStatusLabelMap,
|
||||||
@@ -249,7 +250,7 @@ onMounted(loadDetailAndPreview);
|
|||||||
<span class="monospace">#{{ detail.task.segmentIndex }}</span>
|
<span class="monospace">#{{ detail.task.segmentIndex }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="RoomId">{{ detail.task.roomId }}</el-descriptions-item>
|
<el-descriptions-item label="RoomId">{{ detail.task.roomId }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="清晰度">{{ detail.task.preferredQuality }}</el-descriptions-item>
|
<el-descriptions-item label="清晰度">{{ formatQualityLabel(detail.task.preferredQuality) }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="输出格式">
|
<el-descriptions-item label="输出格式">
|
||||||
{{ outputFormatLabelMap[detail.task.outputFormat] }}
|
{{ outputFormatLabelMap[detail.task.outputFormat] }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type {
|
|||||||
RecordTask
|
RecordTask
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
import {
|
import {
|
||||||
|
formatQualityLabel,
|
||||||
outputFormatLabelMap,
|
outputFormatLabelMap,
|
||||||
saveModeLabelMap,
|
saveModeLabelMap,
|
||||||
sessionStatusLabelMap,
|
sessionStatusLabelMap,
|
||||||
@@ -747,7 +748,7 @@ onBeforeUnmount(() => {
|
|||||||
<div class="data-card__grid">
|
<div class="data-card__grid">
|
||||||
<div>
|
<div>
|
||||||
<dt>清晰度</dt>
|
<dt>清晰度</dt>
|
||||||
<dd class="monospace">{{ task.preferredQuality }}</dd>
|
<dd>{{ formatQualityLabel(task.preferredQuality) }}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>时长</dt>
|
<dt>时长</dt>
|
||||||
@@ -905,7 +906,7 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
<el-table-column label="清晰度" width="120">
|
<el-table-column label="清晰度" width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="monospace">{{ row.preferredQuality }}</span>
|
<span>{{ formatQualityLabel(row.preferredQuality) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
@@ -1435,4 +1436,3 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import type {
|
|||||||
SystemSettings,
|
SystemSettings,
|
||||||
WebhookTestResult
|
WebhookTestResult
|
||||||
} from "@/types";
|
} from "@/types";
|
||||||
import { outputFormatLabelMap, recordingTemplateLabelMap, saveModeLabelMap } from "@/types";
|
import { outputFormatLabelMap, qualityOptionList, recordingTemplateLabelMap, saveModeLabelMap } from "@/types";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useViewport } from "@/composables/useViewport";
|
import { useViewport } from "@/composables/useViewport";
|
||||||
import { useUiPreferences } from "@/composables/useUiPreferences";
|
import { useUiPreferences } from "@/composables/useUiPreferences";
|
||||||
@@ -42,6 +42,8 @@ const webhookTestResult = ref<WebhookTestResult | null>(null);
|
|||||||
const retentionCleanupResult = ref<RetentionCleanupResult | null>(null);
|
const retentionCleanupResult = ref<RetentionCleanupResult | null>(null);
|
||||||
const loadError = ref("");
|
const loadError = ref("");
|
||||||
const activeSettingTab = ref("recording");
|
const activeSettingTab = ref("recording");
|
||||||
|
const qualitySupportHint = "抖音和 Bilibili 支持按画质选流;虎牙暂未实现。若目标档位不可用,平台会自动回退到最接近的可用画质。";
|
||||||
|
const qualityOptions = qualityOptionList;
|
||||||
|
|
||||||
const savebarStyle = computed(() => {
|
const savebarStyle = computed(() => {
|
||||||
if (isMobile.value) {
|
if (isMobile.value) {
|
||||||
@@ -63,7 +65,7 @@ const form = reactive<SystemSettings>({
|
|||||||
ffmpegPath: "ffmpeg",
|
ffmpegPath: "ffmpeg",
|
||||||
outputRoot: "records",
|
outputRoot: "records",
|
||||||
outputDirectoryTemplate: "{platform}/{yyyy}/{MM}/{dd}/{anchor}",
|
outputDirectoryTemplate: "{platform}/{yyyy}/{MM}/{dd}/{anchor}",
|
||||||
outputFileNameTemplate: "{HHmmss}_{anchor}_{title}_{roomId}{segmentSuffix}",
|
outputFileNameTemplate: "{HHmmss}_{quality}_{anchor}_{title}_{roomId}{segmentSuffix}",
|
||||||
defaultQuality: "origin",
|
defaultQuality: "origin",
|
||||||
defaultOutputFormat: 0,
|
defaultOutputFormat: 0,
|
||||||
saveMode: 0,
|
saveMode: 0,
|
||||||
@@ -187,6 +189,7 @@ const outputTemplateTokens = [
|
|||||||
"{roomId}",
|
"{roomId}",
|
||||||
"{anchor}",
|
"{anchor}",
|
||||||
"{title}",
|
"{title}",
|
||||||
|
"{quality}",
|
||||||
"{yyyy}",
|
"{yyyy}",
|
||||||
"{MM}",
|
"{MM}",
|
||||||
"{dd}",
|
"{dd}",
|
||||||
@@ -266,12 +269,12 @@ const canSendTestWebhook = computed(() => Boolean(form.webhookUrl.trim()));
|
|||||||
|
|
||||||
const segmentedExamplePath = computed(() => {
|
const segmentedExamplePath = computed(() => {
|
||||||
const extension = form.defaultOutputFormat === 1 ? "ts" : "mp4";
|
const extension = form.defaultOutputFormat === 1 ? "ts" : "mp4";
|
||||||
return `Douyin/2026/04/15/主播名/221530_主播名_直播标题_123456789_00001.${extension}`;
|
return `Douyin/2026/04/15/主播名/221530_origin_主播名_直播标题_123456789_00001.${extension}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const nestedSegmentedExamplePath = computed(() => {
|
const nestedSegmentedExamplePath = computed(() => {
|
||||||
const extension = form.defaultOutputFormat === 1 ? "ts" : "mp4";
|
const extension = form.defaultOutputFormat === 1 ? "ts" : "mp4";
|
||||||
return `Douyin/2026/04/15/主播名/221530_主播名_直播标题_123456789/221530_主播名_直播标题_123456789_00001.${extension}`;
|
return `Douyin/origin/2026/04/15/主播名/221530_origin_主播名_直播标题_123456789/221530_origin_主播名_直播标题_123456789_00001.${extension}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
@@ -599,7 +602,14 @@ onMounted(loadSettings);
|
|||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-form-item label="默认清晰度">
|
<el-form-item label="默认清晰度">
|
||||||
<el-input v-model="form.defaultQuality" placeholder="origin / FULL_HD / HD / SD" />
|
<el-select v-model="form.defaultQuality">
|
||||||
|
<el-option
|
||||||
|
v-for="option in qualityOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:label="option.label"
|
||||||
|
:value="option.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
@@ -670,6 +680,10 @@ onMounted(loadSettings);
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
|
<div class="helper-panel">
|
||||||
|
{{ qualitySupportHint }}
|
||||||
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card class="surface-card settings-card" shadow="never">
|
<el-card class="surface-card settings-card" shadow="never">
|
||||||
@@ -815,7 +829,7 @@ onMounted(loadSettings);
|
|||||||
v-model="form.outputFileNameTemplate"
|
v-model="form.outputFileNameTemplate"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="3"
|
:rows="3"
|
||||||
placeholder="{HHmmss}_{anchor}_{title}_{roomId}{segmentSuffix}"
|
placeholder="{HHmmss}_{quality}_{anchor}_{title}_{roomId}{segmentSuffix}"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -834,6 +848,10 @@ onMounted(loadSettings);
|
|||||||
目录模板和文件名模板中的时间变量统一按北京时间(UTC+8)渲染,示例路径也按北京时间展示。
|
目录模板和文件名模板中的时间变量统一按北京时间(UTC+8)渲染,示例路径也按北京时间展示。
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="helper-panel">
|
||||||
|
<code>{quality}</code> 输出稳定画质键,例如 <code>origin</code>、<code>FULL_HD</code>、<code>HD</code>、<code>SD</code>,适合放进目录名或文件名里做脚本处理。
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="token-list">
|
<div class="token-list">
|
||||||
<span v-for="token in outputTemplateTokens" :key="token" class="token-chip">{{ token }}</span>
|
<span v-for="token in outputTemplateTokens" :key="token" class="token-chip">{{ token }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public sealed class SystemSettingsDto
|
|||||||
|
|
||||||
public string OutputDirectoryTemplate { get; set; } = "{platform}/{yyyy}/{MM}/{dd}/{anchor}";
|
public string OutputDirectoryTemplate { get; set; } = "{platform}/{yyyy}/{MM}/{dd}/{anchor}";
|
||||||
|
|
||||||
public string OutputFileNameTemplate { get; set; } = "{HHmmss}_{anchor}_{title}_{roomId}";
|
public string OutputFileNameTemplate { get; set; } = "{HHmmss}_{quality}_{anchor}_{title}_{roomId}{segmentSuffix}";
|
||||||
|
|
||||||
public string DefaultQuality { get; set; } = "origin";
|
public string DefaultQuality { get; set; } = "origin";
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ public sealed class UpdateSystemSettingsRequest
|
|||||||
|
|
||||||
public string OutputDirectoryTemplate { get; set; } = "{platform}/{yyyy}/{MM}/{dd}/{anchor}";
|
public string OutputDirectoryTemplate { get; set; } = "{platform}/{yyyy}/{MM}/{dd}/{anchor}";
|
||||||
|
|
||||||
public string OutputFileNameTemplate { get; set; } = "{HHmmss}_{anchor}_{title}_{roomId}";
|
public string OutputFileNameTemplate { get; set; } = "{HHmmss}_{quality}_{anchor}_{title}_{roomId}{segmentSuffix}";
|
||||||
|
|
||||||
public string DefaultQuality { get; set; } = "origin";
|
public string DefaultQuality { get; set; } = "origin";
|
||||||
|
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ public sealed class RecordService
|
|||||||
liveRoom.RoomId,
|
liveRoom.RoomId,
|
||||||
storageAnchorName,
|
storageAnchorName,
|
||||||
liveRoom.Title,
|
liveRoom.Title,
|
||||||
|
streamResult.SelectedQuality,
|
||||||
outputFormat,
|
outputFormat,
|
||||||
saveMode,
|
saveMode,
|
||||||
now);
|
now);
|
||||||
@@ -770,6 +771,7 @@ public sealed class RecordService
|
|||||||
string roomId,
|
string roomId,
|
||||||
string? anchorName,
|
string? anchorName,
|
||||||
string? title,
|
string? title,
|
||||||
|
string selectedQuality,
|
||||||
RecordOutputFormat outputFormat,
|
RecordOutputFormat outputFormat,
|
||||||
RecordSaveMode saveMode,
|
RecordSaveMode saveMode,
|
||||||
DateTimeOffset now)
|
DateTimeOffset now)
|
||||||
@@ -783,6 +785,7 @@ public sealed class RecordService
|
|||||||
safeRoomId,
|
safeRoomId,
|
||||||
anchorName,
|
anchorName,
|
||||||
title,
|
title,
|
||||||
|
selectedQuality,
|
||||||
localNow,
|
localNow,
|
||||||
segmentSuffix: string.Empty);
|
segmentSuffix: string.Empty);
|
||||||
var directoryPath = BuildDirectoryPath(
|
var directoryPath = BuildDirectoryPath(
|
||||||
@@ -791,6 +794,7 @@ public sealed class RecordService
|
|||||||
safeRoomId,
|
safeRoomId,
|
||||||
anchorName,
|
anchorName,
|
||||||
title,
|
title,
|
||||||
|
selectedQuality,
|
||||||
localNow,
|
localNow,
|
||||||
baseFileStem);
|
baseFileStem);
|
||||||
var fileNameStem = BuildFileNameStem(
|
var fileNameStem = BuildFileNameStem(
|
||||||
@@ -799,6 +803,7 @@ public sealed class RecordService
|
|||||||
safeRoomId,
|
safeRoomId,
|
||||||
anchorName,
|
anchorName,
|
||||||
title,
|
title,
|
||||||
|
selectedQuality,
|
||||||
localNow,
|
localNow,
|
||||||
saveMode == RecordSaveMode.Segmented ? "_%05d" : string.Empty);
|
saveMode == RecordSaveMode.Segmented ? "_%05d" : string.Empty);
|
||||||
var folder = Path.Combine(outputRoot, directoryPath);
|
var folder = Path.Combine(outputRoot, directoryPath);
|
||||||
@@ -838,6 +843,7 @@ public sealed class RecordService
|
|||||||
string roomId,
|
string roomId,
|
||||||
string? anchorName,
|
string? anchorName,
|
||||||
string? title,
|
string? title,
|
||||||
|
string quality,
|
||||||
DateTimeOffset now,
|
DateTimeOffset now,
|
||||||
string fileStem)
|
string fileStem)
|
||||||
{
|
{
|
||||||
@@ -847,6 +853,7 @@ public sealed class RecordService
|
|||||||
roomId,
|
roomId,
|
||||||
anchorName,
|
anchorName,
|
||||||
title,
|
title,
|
||||||
|
quality,
|
||||||
now,
|
now,
|
||||||
forPathSegment: true,
|
forPathSegment: true,
|
||||||
fileStem,
|
fileStem,
|
||||||
@@ -866,6 +873,7 @@ public sealed class RecordService
|
|||||||
string roomId,
|
string roomId,
|
||||||
string? anchorName,
|
string? anchorName,
|
||||||
string? title,
|
string? title,
|
||||||
|
string quality,
|
||||||
DateTimeOffset now,
|
DateTimeOffset now,
|
||||||
string segmentSuffix)
|
string segmentSuffix)
|
||||||
{
|
{
|
||||||
@@ -875,6 +883,7 @@ public sealed class RecordService
|
|||||||
roomId,
|
roomId,
|
||||||
anchorName,
|
anchorName,
|
||||||
title,
|
title,
|
||||||
|
quality,
|
||||||
now,
|
now,
|
||||||
forPathSegment: false,
|
forPathSegment: false,
|
||||||
fileStem: string.Empty,
|
fileStem: string.Empty,
|
||||||
@@ -885,7 +894,7 @@ public sealed class RecordService
|
|||||||
private static string EnsureSegmentSuffixTemplate(string template, RecordSaveMode saveMode)
|
private static string EnsureSegmentSuffixTemplate(string template, RecordSaveMode saveMode)
|
||||||
{
|
{
|
||||||
var effectiveTemplate = string.IsNullOrWhiteSpace(template)
|
var effectiveTemplate = string.IsNullOrWhiteSpace(template)
|
||||||
? "{HHmmss}_{anchor}_{title}_{roomId}{segmentSuffix}"
|
? "{HHmmss}_{quality}_{anchor}_{title}_{roomId}{segmentSuffix}"
|
||||||
: template.Trim();
|
: template.Trim();
|
||||||
|
|
||||||
if (saveMode != RecordSaveMode.Segmented)
|
if (saveMode != RecordSaveMode.Segmented)
|
||||||
@@ -908,13 +917,14 @@ public sealed class RecordService
|
|||||||
string roomId,
|
string roomId,
|
||||||
string? anchorName,
|
string? anchorName,
|
||||||
string? title,
|
string? title,
|
||||||
|
string quality,
|
||||||
DateTimeOffset now,
|
DateTimeOffset now,
|
||||||
bool forPathSegment,
|
bool forPathSegment,
|
||||||
string fileStem,
|
string fileStem,
|
||||||
string segmentSuffix)
|
string segmentSuffix)
|
||||||
{
|
{
|
||||||
var effectiveTemplate = string.IsNullOrWhiteSpace(template)
|
var effectiveTemplate = string.IsNullOrWhiteSpace(template)
|
||||||
? (forPathSegment ? "{platform}/{yyyy}/{MM}/{dd}/{anchor}" : "{HHmmss}_{anchor}_{title}_{roomId}{segmentSuffix}")
|
? (forPathSegment ? "{platform}/{yyyy}/{MM}/{dd}/{anchor}" : "{HHmmss}_{quality}_{anchor}_{title}_{roomId}{segmentSuffix}")
|
||||||
: template;
|
: template;
|
||||||
|
|
||||||
// Path tokens must stay case-sensitive so {MM} (month) and {mm} (minute) don't collide.
|
// Path tokens must stay case-sensitive so {MM} (month) and {mm} (minute) don't collide.
|
||||||
@@ -924,6 +934,7 @@ public sealed class RecordService
|
|||||||
["roomId"] = roomId,
|
["roomId"] = roomId,
|
||||||
["anchor"] = NormalizeTokenValue(anchorName, "unknown-anchor"),
|
["anchor"] = NormalizeTokenValue(anchorName, "unknown-anchor"),
|
||||||
["title"] = NormalizeTokenValue(title, "untitled"),
|
["title"] = NormalizeTokenValue(title, "untitled"),
|
||||||
|
["quality"] = NormalizeTokenValue(quality, "origin"),
|
||||||
["fileStem"] = fileStem,
|
["fileStem"] = fileStem,
|
||||||
["segmentSuffix"] = segmentSuffix,
|
["segmentSuffix"] = segmentSuffix,
|
||||||
["yyyy"] = now.ToString("yyyy"),
|
["yyyy"] = now.ToString("yyyy"),
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ public sealed class SystemSettingsService : ISystemSettingsService
|
|||||||
FfmpegPath = GetValue(lookup, FfmpegPathKey, "ffmpeg"),
|
FfmpegPath = GetValue(lookup, FfmpegPathKey, "ffmpeg"),
|
||||||
OutputRoot = GetValue(lookup, OutputRootKey, "records"),
|
OutputRoot = GetValue(lookup, OutputRootKey, "records"),
|
||||||
OutputDirectoryTemplate = GetValue(lookup, OutputDirectoryTemplateKey, "{platform}/{yyyy}/{MM}/{dd}/{anchor}"),
|
OutputDirectoryTemplate = GetValue(lookup, OutputDirectoryTemplateKey, "{platform}/{yyyy}/{MM}/{dd}/{anchor}"),
|
||||||
OutputFileNameTemplate = GetValue(lookup, OutputFileNameTemplateKey, "{HHmmss}_{anchor}_{title}_{roomId}{segmentSuffix}"),
|
OutputFileNameTemplate = GetValue(lookup, OutputFileNameTemplateKey, "{HHmmss}_{quality}_{anchor}_{title}_{roomId}{segmentSuffix}"),
|
||||||
DefaultQuality = GetValue(lookup, DefaultQualityKey, "origin"),
|
DefaultQuality = GetValue(lookup, DefaultQualityKey, "origin"),
|
||||||
DefaultOutputFormat = Enum.TryParse(GetValue(lookup, DefaultOutputFormatKey, "Mp4"), true, out RecordOutputFormat outputFormat)
|
DefaultOutputFormat = Enum.TryParse(GetValue(lookup, DefaultOutputFormatKey, "Mp4"), true, out RecordOutputFormat outputFormat)
|
||||||
? outputFormat
|
? outputFormat
|
||||||
|
|||||||
Reference in New Issue
Block a user