|
|
|
|
@ -1,12 +1,13 @@
|
|
|
|
|
<template>
|
|
|
|
|
<main class="config-root config-page">
|
|
|
|
|
<div class="config-top-status row-between">
|
|
|
|
|
<div class="config-top-status">
|
|
|
|
|
<div class="config-status-row">
|
|
|
|
|
<span class="socket-status" :class="socketRunning ? 'running' : 'stopped'">
|
|
|
|
|
<i class="socket-dot" />
|
|
|
|
|
Socket {{ socketRunning ? "运行中" : "未启动" }} (9501)
|
|
|
|
|
</span>
|
|
|
|
|
<div class="top-actions">
|
|
|
|
|
<el-button type="primary" @click="saveConfig">保存配置</el-button>
|
|
|
|
|
<el-checkbox v-model="draft.autoStartSocket">启动时自动开始监听</el-checkbox>
|
|
|
|
|
<el-button type="success" plain :disabled="socketRunning" @click="startSocketService">
|
|
|
|
|
启动 Socket 服务
|
|
|
|
|
</el-button>
|
|
|
|
|
@ -15,43 +16,39 @@
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="config-status-row">
|
|
|
|
|
<el-checkbox-group class="delivery-targets" v-model="selectedDeliveryTargets">
|
|
|
|
|
<el-checkbox label="syncScreen">同步屏</el-checkbox>
|
|
|
|
|
<el-checkbox label="compositeScreen">综合屏</el-checkbox>
|
|
|
|
|
<el-checkbox label="voiceBroadcast">语音播报</el-checkbox>
|
|
|
|
|
</el-checkbox-group>
|
|
|
|
|
<el-button type="primary" @click="saveConfig">保存配置</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<el-collapse v-model="activePanels" class="config-collapse">
|
|
|
|
|
<el-collapse-item name="base">
|
|
|
|
|
<template #title>
|
|
|
|
|
<div class="card-header">基础配置</div>
|
|
|
|
|
</template>
|
|
|
|
|
<div class="panel-scroll">
|
|
|
|
|
<el-tabs v-model="activeTab" class="config-tabs">
|
|
|
|
|
<el-tab-pane label="基础配置" name="base">
|
|
|
|
|
<div class="tab-pane-content panel-scroll">
|
|
|
|
|
<el-form label-width="140px">
|
|
|
|
|
<el-row :gutter="16">
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
<el-form-item label="主div总长度(px)">
|
|
|
|
|
<el-input-number v-model="draft.totalWidth" :min="1" :max="screenWidth" :step="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
<el-form-item label="主div高度(px)">
|
|
|
|
|
<el-input-number v-model="draft.segmentHeight" :min="1" :step="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
<el-form-item label="标尺显示">
|
|
|
|
|
<el-switch v-model="draft.showRuler" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
</el-form>
|
|
|
|
|
</div>
|
|
|
|
|
</el-collapse-item>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
<el-collapse-item name="segments">
|
|
|
|
|
<template #title>
|
|
|
|
|
<div class="card-header row-between">
|
|
|
|
|
<el-tab-pane label="分段配置" name="segments">
|
|
|
|
|
<div class="tab-pane-content panel-scroll">
|
|
|
|
|
<div class="tab-pane-actions">
|
|
|
|
|
<span>分段列表(主屏宽度上限 {{ screenWidth }}px)</span>
|
|
|
|
|
<el-button type="primary" plain size="small" @click.stop="addSegment">添加分段</el-button>
|
|
|
|
|
<el-button type="primary" plain size="small" @click="addSegment">添加分段</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<div class="panel-scroll">
|
|
|
|
|
<el-table :data="draft.segments" border size="small">
|
|
|
|
|
<el-table-column type="index" label="#" width="56" />
|
|
|
|
|
<el-table-column label="段长度(px)" width="140">
|
|
|
|
|
@ -75,239 +72,389 @@
|
|
|
|
|
</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
<el-divider content-position="left">应用后预览</el-divider>
|
|
|
|
|
<el-table :data="appliedSegments" border size="small">
|
|
|
|
|
<el-table-column type="index" label="#" width="56" />
|
|
|
|
|
<el-table-column prop="sliceWidth" label="段长度(px)" width="140" />
|
|
|
|
|
<el-table-column label="左顶点坐标">
|
|
|
|
|
<template #default="{ row }">({{ row.left }}, {{ row.top }})</template>
|
|
|
|
|
</el-table-column>
|
|
|
|
|
</el-table>
|
|
|
|
|
</div>
|
|
|
|
|
</el-collapse-item>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
<el-collapse-item name="areas">
|
|
|
|
|
<template #title>
|
|
|
|
|
<div class="card-header row-between">
|
|
|
|
|
<span>子div窗口区域</span>
|
|
|
|
|
<el-button type="primary" plain size="small" @click.stop="addWindowArea">添加子div</el-button>
|
|
|
|
|
<el-tab-pane label="窗口区域" name="areas">
|
|
|
|
|
<div class="tab-pane-content panel-scroll panel-scroll--tall area-list">
|
|
|
|
|
<div class="tab-pane-actions tab-pane-actions--left">
|
|
|
|
|
<el-button type="primary" plain size="small" @click="addWindowArea">添加子div</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<el-empty v-if="draft.windowAreas.length === 0" description="暂无窗口区域,请先添加子div" />
|
|
|
|
|
<el-tabs
|
|
|
|
|
v-else
|
|
|
|
|
v-model="activeWindowAreaTab"
|
|
|
|
|
tab-position="left"
|
|
|
|
|
class="window-area-tabs"
|
|
|
|
|
>
|
|
|
|
|
<el-tab-pane
|
|
|
|
|
v-for="(area, index) in draft.windowAreas"
|
|
|
|
|
:key="area.id"
|
|
|
|
|
:name="area.id"
|
|
|
|
|
>
|
|
|
|
|
<template #label>
|
|
|
|
|
<div
|
|
|
|
|
class="editable-tab-label"
|
|
|
|
|
@dblclick.stop="startEditingWindowAreaTabLabel(area, index)"
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
v-if="editingWindowAreaTabId === area.id"
|
|
|
|
|
ref="windowAreaTabInputRef"
|
|
|
|
|
v-model="editingWindowAreaTabName"
|
|
|
|
|
class="editable-tab-input"
|
|
|
|
|
@click.stop
|
|
|
|
|
@blur="finishEditingWindowAreaTabLabel(area, true)"
|
|
|
|
|
@keyup.enter="finishEditingWindowAreaTabLabel(area, true)"
|
|
|
|
|
@keyup.esc="finishEditingWindowAreaTabLabel(area, false)"
|
|
|
|
|
/>
|
|
|
|
|
<span v-else>{{ resolveWindowAreaTabLabel(area, index) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<div class="panel-scroll panel-scroll--tall area-list">
|
|
|
|
|
<el-card v-for="(area, index) in draft.windowAreas" :key="area.id" class="area-item" shadow="never">
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="row-between">
|
|
|
|
|
<strong>子div {{ index + 1 }}</strong>
|
|
|
|
|
<div class="window-area-editor-header">
|
|
|
|
|
<span class="window-area-editor-title">编辑子div {{ index + 1 }}</span>
|
|
|
|
|
<el-button type="danger" link @click="removeWindowArea(index)">删除</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<el-form label-width="120px" size="small">
|
|
|
|
|
<el-row :gutter="12">
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-form label-width="120px" size="small" class="window-area-form-vertical">
|
|
|
|
|
<el-form-item label="窗口区域编号">
|
|
|
|
|
<el-input-number v-model="area.windowId" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-form-item label="时钟窗口">
|
|
|
|
|
<el-switch v-model="area.isClockWindow" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-form-item label="宽度">
|
|
|
|
|
<el-input-number v-model="area.width" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-form-item label="高度">
|
|
|
|
|
<el-input-number v-model="area.height" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-form-item label="X">
|
|
|
|
|
<el-input-number v-model="area.x" :min="0" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-form-item label="Y">
|
|
|
|
|
<el-input-number v-model="area.y" :min="0" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<el-divider content-position="left">窗口号区域(占比 1)</el-divider>
|
|
|
|
|
<el-row :gutter="12">
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-form-item label="窗口编号">
|
|
|
|
|
<el-input v-model="area.windowNumber" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
<el-form-item label="圆圈边框">
|
|
|
|
|
<el-switch v-model="area.windowNumberCircle" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
<el-form-item label="字号">
|
|
|
|
|
<el-input-number v-model="area.windowNumberStyle.fontSize" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="5">
|
|
|
|
|
<el-form-item label="颜色">
|
|
|
|
|
<el-input v-model="area.windowNumberStyle.color" />
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="area.windowNumberStyle.color"
|
|
|
|
|
@blur="validateColorOnBlur(area.windowNumberStyle, 'color', '#ffffff', '窗口号文字')"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="5">
|
|
|
|
|
<el-form-item label="粗细">
|
|
|
|
|
<el-input-number v-model="area.windowNumberStyle.fontWeight" :min="100" :step="100" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
<el-form-item label="圆圈大小">
|
|
|
|
|
<el-input-number v-model="area.windowNumberCircleStyle.size" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
<el-form-item label="边框粗细">
|
|
|
|
|
<el-input-number v-model="area.windowNumberCircleStyle.borderWidth" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
<el-form-item label="圆角半径">
|
|
|
|
|
<el-input-number v-model="area.windowNumberCircleStyle.borderRadius" :min="0" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
<el-divider content-position="left">文本区域(占比 2.5,静态/动态=1:1)</el-divider>
|
|
|
|
|
<el-row :gutter="12">
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-form-item label="静态文本">
|
|
|
|
|
<el-input v-model="area.staticText" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
<el-form-item label="静态字号">
|
|
|
|
|
<el-input-number v-model="area.staticTextStyle.fontSize" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="5">
|
|
|
|
|
<el-form-item label="静态颜色">
|
|
|
|
|
<el-input v-model="area.staticTextStyle.color" />
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="area.staticTextStyle.color"
|
|
|
|
|
@blur="validateColorOnBlur(area.staticTextStyle, 'color', '#ffffff', '静态文本')"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="5">
|
|
|
|
|
<el-form-item label="静态粗细">
|
|
|
|
|
<el-input-number v-model="area.staticTextStyle.fontWeight" :min="100" :step="100" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-row :gutter="12">
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-form-item label="动态文本">
|
|
|
|
|
<el-input v-model="area.dynamicText" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
<el-form-item label="动态字号">
|
|
|
|
|
<el-input-number v-model="area.dynamicTextStyle.fontSize" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="5">
|
|
|
|
|
<el-form-item label="动态颜色">
|
|
|
|
|
<el-input v-model="area.dynamicTextStyle.color" />
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="area.dynamicTextStyle.color"
|
|
|
|
|
@blur="validateColorOnBlur(area.dynamicTextStyle, 'color', '#ffffff', '动态文本')"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="5">
|
|
|
|
|
<el-form-item label="动态粗细">
|
|
|
|
|
<el-input-number v-model="area.dynamicTextStyle.fontWeight" :min="100" :step="100" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
</el-form>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
</el-tabs>
|
|
|
|
|
</div>
|
|
|
|
|
</el-collapse-item>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
<el-collapse-item name="subtitles">
|
|
|
|
|
<template #title>
|
|
|
|
|
<div class="card-header row-between">
|
|
|
|
|
<span>滚动字幕区域</span>
|
|
|
|
|
<el-button type="primary" plain size="small" @click.stop="addSubtitleArea">
|
|
|
|
|
<el-tab-pane label="滚动字幕" name="subtitles">
|
|
|
|
|
<div class="tab-pane-content panel-scroll panel-scroll--tall">
|
|
|
|
|
<div class="tab-pane-actions tab-pane-actions--left">
|
|
|
|
|
<el-button type="primary" plain size="small" @click="addSubtitleArea">
|
|
|
|
|
添加滚动字幕
|
|
|
|
|
</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<div class="panel-scroll area-list">
|
|
|
|
|
<el-card
|
|
|
|
|
<el-empty v-if="draft.subtitleAreas.length === 0" description="暂无滚动字幕区域,请先添加" />
|
|
|
|
|
<el-tabs
|
|
|
|
|
v-else
|
|
|
|
|
v-model="activeSubtitleAreaTab"
|
|
|
|
|
tab-position="left"
|
|
|
|
|
class="subtitle-area-tabs"
|
|
|
|
|
>
|
|
|
|
|
<el-tab-pane
|
|
|
|
|
v-for="(subtitle, index) in draft.subtitleAreas"
|
|
|
|
|
:key="subtitle.id"
|
|
|
|
|
class="area-item"
|
|
|
|
|
shadow="never"
|
|
|
|
|
:name="subtitle.id"
|
|
|
|
|
>
|
|
|
|
|
<template #header>
|
|
|
|
|
<div class="row-between">
|
|
|
|
|
<strong>字幕区域 {{ index + 1 }}</strong>
|
|
|
|
|
<el-button type="danger" link @click="removeSubtitleArea(index)">删除</el-button>
|
|
|
|
|
<template #label>
|
|
|
|
|
<div
|
|
|
|
|
class="editable-tab-label"
|
|
|
|
|
@dblclick.stop="startEditingSubtitleTabLabel(subtitle, index)"
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
v-if="editingSubtitleTabId === subtitle.id"
|
|
|
|
|
ref="subtitleTabInputRef"
|
|
|
|
|
v-model="editingSubtitleTabName"
|
|
|
|
|
class="editable-tab-input"
|
|
|
|
|
@click.stop
|
|
|
|
|
@blur="finishEditingSubtitleTabLabel(subtitle, true)"
|
|
|
|
|
@keyup.enter="finishEditingSubtitleTabLabel(subtitle, true)"
|
|
|
|
|
@keyup.esc="finishEditingSubtitleTabLabel(subtitle, false)"
|
|
|
|
|
/>
|
|
|
|
|
<span v-else>{{ resolveSubtitleAreaTabLabel(subtitle, index) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
<el-form label-width="120px" size="small">
|
|
|
|
|
<el-row :gutter="12">
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<div class="window-area-editor-header">
|
|
|
|
|
<span class="window-area-editor-title">编辑字幕区域 {{ index + 1 }}</span>
|
|
|
|
|
<el-button type="danger" link @click="removeSubtitleArea(index)">删除</el-button>
|
|
|
|
|
</div>
|
|
|
|
|
<el-form label-width="120px" size="small" class="window-area-form-vertical">
|
|
|
|
|
<el-form-item label="宽度">
|
|
|
|
|
<el-input-number v-model="subtitle.width" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-form-item label="高度">
|
|
|
|
|
<el-input-number v-model="subtitle.height" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-form-item label="X">
|
|
|
|
|
<el-input-number v-model="subtitle.x" :min="0" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
<el-form-item label="Y">
|
|
|
|
|
<el-input-number v-model="subtitle.y" :min="0" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
<el-row :gutter="12">
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
<el-form-item label="字幕文本">
|
|
|
|
|
<el-input v-model="subtitle.text" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
<el-form-item label="字号">
|
|
|
|
|
<el-input-number v-model="subtitle.textStyle.fontSize" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
<el-form-item label="颜色">
|
|
|
|
|
<el-input v-model="subtitle.textStyle.color" />
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="subtitle.textStyle.color"
|
|
|
|
|
@blur="validateColorOnBlur(subtitle.textStyle, 'color', '#ffffff', '字幕文本')"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
<el-form-item label="粗细">
|
|
|
|
|
<el-input-number v-model="subtitle.textStyle.fontWeight" :min="100" :step="100" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
<el-form-item label="滚动速度">
|
|
|
|
|
<el-input-number v-model="subtitle.speed" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-col>
|
|
|
|
|
</el-row>
|
|
|
|
|
</el-form>
|
|
|
|
|
</el-card>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
</el-tabs>
|
|
|
|
|
</div>
|
|
|
|
|
</el-collapse-item>
|
|
|
|
|
</el-collapse>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
<el-tab-pane label="综合屏配置" name="composite">
|
|
|
|
|
<div class="tab-pane-content panel-scroll panel-scroll--tall">
|
|
|
|
|
<el-form label-width="160px" size="small" class="window-area-form-vertical">
|
|
|
|
|
<el-form-item label="屏号">
|
|
|
|
|
<el-input-number v-model="draft.compositeScreen.screenId" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
<el-tabs
|
|
|
|
|
v-model="activeCompositeConfigTab"
|
|
|
|
|
tab-position="left"
|
|
|
|
|
class="composite-config-tabs"
|
|
|
|
|
>
|
|
|
|
|
<el-tab-pane label="头部区域" name="header">
|
|
|
|
|
<div class="window-area-editor-header">
|
|
|
|
|
<span class="window-area-editor-title">头部区域配置</span>
|
|
|
|
|
</div>
|
|
|
|
|
<el-form label-width="160px" size="small" class="window-area-form-vertical">
|
|
|
|
|
<el-form-item label="大厅名称">
|
|
|
|
|
<el-input v-model="draft.compositeScreen.hallName" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="头部背景色">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="draft.compositeScreen.headerBackgroundColor"
|
|
|
|
|
@blur="
|
|
|
|
|
validateColorOnBlur(
|
|
|
|
|
draft.compositeScreen,
|
|
|
|
|
'headerBackgroundColor',
|
|
|
|
|
'#03050a',
|
|
|
|
|
'头部背景',
|
|
|
|
|
)
|
|
|
|
|
"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="字号">
|
|
|
|
|
<el-input-number v-model="draft.compositeScreen.hallStyle.fontSize" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="颜色">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="draft.compositeScreen.hallStyle.color"
|
|
|
|
|
@blur="validateColorOnBlur(draft.compositeScreen.hallStyle, 'color', '#ffffff', '大厅名称')"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="粗细">
|
|
|
|
|
<el-input-number
|
|
|
|
|
v-model="draft.compositeScreen.hallStyle.fontWeight"
|
|
|
|
|
:min="100"
|
|
|
|
|
:step="100"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
<el-tab-pane label="底部字幕" name="footer">
|
|
|
|
|
<div class="window-area-editor-header">
|
|
|
|
|
<span class="window-area-editor-title">底部字幕配置</span>
|
|
|
|
|
</div>
|
|
|
|
|
<el-form label-width="160px" size="small" class="window-area-form-vertical">
|
|
|
|
|
<el-form-item label="滚动字幕文本">
|
|
|
|
|
<el-input v-model="draft.compositeScreen.footerSubtitle" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="底部背景色">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="draft.compositeScreen.footerBackgroundColor"
|
|
|
|
|
@blur="
|
|
|
|
|
validateColorOnBlur(
|
|
|
|
|
draft.compositeScreen,
|
|
|
|
|
'footerBackgroundColor',
|
|
|
|
|
'#03050a',
|
|
|
|
|
'底部背景',
|
|
|
|
|
)
|
|
|
|
|
"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="字号">
|
|
|
|
|
<el-input-number v-model="draft.compositeScreen.footerSubtitleStyle.fontSize" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="颜色">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="draft.compositeScreen.footerSubtitleStyle.color"
|
|
|
|
|
@blur="
|
|
|
|
|
validateColorOnBlur(
|
|
|
|
|
draft.compositeScreen.footerSubtitleStyle,
|
|
|
|
|
'color',
|
|
|
|
|
'#ffffff',
|
|
|
|
|
'底部字幕',
|
|
|
|
|
)
|
|
|
|
|
"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="粗细">
|
|
|
|
|
<el-input-number
|
|
|
|
|
v-model="draft.compositeScreen.footerSubtitleStyle.fontWeight"
|
|
|
|
|
:min="100"
|
|
|
|
|
:step="100"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="滚动速度(px/s)">
|
|
|
|
|
<el-input-number v-model="draft.compositeScreen.footerSubtitleSpeed" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
<el-tab-pane label="中间文本区域" name="middle-text">
|
|
|
|
|
<div class="window-area-editor-header">
|
|
|
|
|
<span class="window-area-editor-title">中间文本配置</span>
|
|
|
|
|
</div>
|
|
|
|
|
<el-form label-width="160px" size="small" class="window-area-form-vertical">
|
|
|
|
|
<el-form-item label="中间背景色">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="draft.compositeScreen.middleBackgroundColor"
|
|
|
|
|
@blur="
|
|
|
|
|
validateColorOnBlur(
|
|
|
|
|
draft.compositeScreen,
|
|
|
|
|
'middleBackgroundColor',
|
|
|
|
|
'#050a14',
|
|
|
|
|
'中间区域背景',
|
|
|
|
|
)
|
|
|
|
|
"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="文字字号">
|
|
|
|
|
<el-input-number v-model="draft.compositeScreen.middleTextStyle.fontSize" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="文字颜色">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="draft.compositeScreen.middleTextStyle.color"
|
|
|
|
|
@blur="
|
|
|
|
|
validateColorOnBlur(
|
|
|
|
|
draft.compositeScreen.middleTextStyle,
|
|
|
|
|
'color',
|
|
|
|
|
'#f5f7fa',
|
|
|
|
|
'中间文字',
|
|
|
|
|
)
|
|
|
|
|
"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="文字粗细">
|
|
|
|
|
<el-input-number
|
|
|
|
|
v-model="draft.compositeScreen.middleTextStyle.fontWeight"
|
|
|
|
|
:min="100"
|
|
|
|
|
:step="100"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="单列行数">
|
|
|
|
|
<el-input-number v-model="draft.compositeScreen.middleMaxLines" :min="1" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
|
|
|
|
<el-tab-pane label="中间视频区域" name="middle-video">
|
|
|
|
|
<div class="window-area-editor-header">
|
|
|
|
|
<span class="window-area-editor-title">中间视频配置</span>
|
|
|
|
|
</div>
|
|
|
|
|
<el-form label-width="160px" size="small" class="window-area-form-vertical">
|
|
|
|
|
<el-form-item label="显示视频区域">
|
|
|
|
|
<el-switch v-model="draft.compositeScreen.showVideo" />
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="视频URL(;分隔)">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="draft.compositeScreen.videoUrls"
|
|
|
|
|
:disabled="!draft.compositeScreen.showVideo"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
<el-form-item label="视频音量(0-100)">
|
|
|
|
|
<el-input-number
|
|
|
|
|
v-model="draft.compositeScreen.videoVolume"
|
|
|
|
|
:min="0"
|
|
|
|
|
:max="100"
|
|
|
|
|
:disabled="!draft.compositeScreen.showVideo"
|
|
|
|
|
/>
|
|
|
|
|
</el-form-item>
|
|
|
|
|
</el-form>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
</el-tabs>
|
|
|
|
|
</div>
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
</el-tabs>
|
|
|
|
|
|
|
|
|
|
<div class="actions-row">
|
|
|
|
|
<span class="version-text">版本号:V{{ appVersion }}</span>
|
|
|
|
|
@ -322,7 +469,7 @@
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { computed, onMounted, onUnmounted, reactive, ref, watch } from "vue";
|
|
|
|
|
import { nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
|
|
|
|
|
import { appWindow, currentMonitor } from "@tauri-apps/api/window";
|
|
|
|
|
import { invoke } from "@tauri-apps/api/tauri";
|
|
|
|
|
import { listen, type UnlistenFn } from "@tauri-apps/api/event";
|
|
|
|
|
@ -330,13 +477,13 @@ import { ElMessage } from "element-plus";
|
|
|
|
|
import type {
|
|
|
|
|
BroadcastConfig,
|
|
|
|
|
ChildWindowAreaConfig,
|
|
|
|
|
CompositeScreenConfig,
|
|
|
|
|
SegmentConfigItem,
|
|
|
|
|
SubtitleAreaConfig,
|
|
|
|
|
} from "../models/config";
|
|
|
|
|
import { DEFAULT_BROADCAST_CONFIG } from "../models/config";
|
|
|
|
|
import { useBroadcastConfig } from "../composables/useBroadcastConfig";
|
|
|
|
|
import {
|
|
|
|
|
buildSegmentsFromConfig,
|
|
|
|
|
normalizeScreenWidth,
|
|
|
|
|
normalizeSegmentConfigItem,
|
|
|
|
|
normalizeSegmentHeight,
|
|
|
|
|
@ -361,7 +508,7 @@ interface AptUpdateCheckResult {
|
|
|
|
|
const screenWidth = ref(normalizeScreenWidth(window.screen.width || 1920));
|
|
|
|
|
const { config, patchConfig } = useBroadcastConfig();
|
|
|
|
|
const saveMessage = ref("修改后请点击“保存配置”。");
|
|
|
|
|
const activePanels = ref(["base", "segments", "areas", "subtitles"]);
|
|
|
|
|
const activeTab = ref("base");
|
|
|
|
|
const socketRunning = ref(false);
|
|
|
|
|
const checkingUpdate = ref(false);
|
|
|
|
|
const appVersion = ref("0.1.0");
|
|
|
|
|
@ -373,16 +520,16 @@ const syncingFromStore = ref(false);
|
|
|
|
|
let socketStatusUnlisten: UnlistenFn | null = null;
|
|
|
|
|
|
|
|
|
|
const draft = reactive<BroadcastConfig>({ ...DEFAULT_BROADCAST_CONFIG });
|
|
|
|
|
|
|
|
|
|
// 按当前草稿配置预演最终会应用到同步屏的分段数据。
|
|
|
|
|
const appliedSegments = computed(() =>
|
|
|
|
|
buildSegmentsFromConfig(
|
|
|
|
|
draft.segments,
|
|
|
|
|
screenWidth.value,
|
|
|
|
|
Math.min(draft.totalWidth, screenWidth.value),
|
|
|
|
|
draft.segmentHeight,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
const selectedDeliveryTargets = ref<string[]>([]);
|
|
|
|
|
const activeWindowAreaTab = ref("");
|
|
|
|
|
const activeSubtitleAreaTab = ref("");
|
|
|
|
|
const activeCompositeConfigTab = ref("header");
|
|
|
|
|
const editingWindowAreaTabId = ref("");
|
|
|
|
|
const editingWindowAreaTabName = ref("");
|
|
|
|
|
const windowAreaTabInputRef = ref<HTMLInputElement | null>(null);
|
|
|
|
|
const editingSubtitleTabId = ref("");
|
|
|
|
|
const editingSubtitleTabName = ref("");
|
|
|
|
|
const subtitleTabInputRef = ref<HTMLInputElement | null>(null);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 深拷贝分段数组,避免引用同一对象导致联动副作用。
|
|
|
|
|
@ -408,6 +555,15 @@ function cloneSubtitleAreas(areas: SubtitleAreaConfig[]) {
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function cloneCompositeScreen(config: CompositeScreenConfig): CompositeScreenConfig {
|
|
|
|
|
return {
|
|
|
|
|
...config,
|
|
|
|
|
hallStyle: { ...config.hallStyle },
|
|
|
|
|
footerSubtitleStyle: { ...config.footerSubtitleStyle },
|
|
|
|
|
middleTextStyle: { ...config.middleTextStyle },
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeStyle(style: { fontSize: number; color: string; fontWeight: number }) {
|
|
|
|
|
return {
|
|
|
|
|
fontSize: Number.isFinite(style.fontSize) && style.fontSize > 0 ? Math.floor(style.fontSize) : 14,
|
|
|
|
|
@ -417,9 +573,29 @@ function normalizeStyle(style: { fontSize: number; color: string; fontWeight: nu
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const HEX_COLOR_PATTERN = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
|
|
|
|
|
|
|
|
function validateColorOnBlur(
|
|
|
|
|
target: Record<string, unknown>,
|
|
|
|
|
key: string,
|
|
|
|
|
fallback: string,
|
|
|
|
|
label: string,
|
|
|
|
|
) {
|
|
|
|
|
const raw = typeof target[key] === "string" ? String(target[key]) : "";
|
|
|
|
|
const value = raw.trim();
|
|
|
|
|
if (HEX_COLOR_PATTERN.test(value)) {
|
|
|
|
|
target[key] = value;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
target[key] = fallback;
|
|
|
|
|
ElMessage.warning(`${label}颜色格式无效,已回退为 ${fallback}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createDefaultWindowArea(index: number): ChildWindowAreaConfig {
|
|
|
|
|
return {
|
|
|
|
|
id: `area-${Date.now()}-${index}`,
|
|
|
|
|
title: "",
|
|
|
|
|
windowId: index + 1,
|
|
|
|
|
isClockWindow: false,
|
|
|
|
|
width: 220,
|
|
|
|
|
@ -456,6 +632,7 @@ function createDefaultWindowArea(index: number): ChildWindowAreaConfig {
|
|
|
|
|
function createDefaultSubtitleArea(index: number): SubtitleAreaConfig {
|
|
|
|
|
return {
|
|
|
|
|
id: `subtitle-${Date.now()}-${index}`,
|
|
|
|
|
title: "",
|
|
|
|
|
width: 420,
|
|
|
|
|
height: 28,
|
|
|
|
|
x: 0,
|
|
|
|
|
@ -470,6 +647,40 @@ function createDefaultSubtitleArea(index: number): SubtitleAreaConfig {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveWindowAreaTabLabel(area: ChildWindowAreaConfig, index: number): string {
|
|
|
|
|
const title = typeof area.title === "string" ? area.title.trim() : "";
|
|
|
|
|
if (title) {
|
|
|
|
|
return title;
|
|
|
|
|
}
|
|
|
|
|
const staticText = typeof area.staticText === "string" ? area.staticText.trim() : "";
|
|
|
|
|
if (staticText) {
|
|
|
|
|
return staticText;
|
|
|
|
|
}
|
|
|
|
|
const windowId =
|
|
|
|
|
Number.isFinite(area.windowId) && area.windowId > 0 ? Math.floor(area.windowId) : index + 1;
|
|
|
|
|
return `${windowId}窗口`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startEditingWindowAreaTabLabel(area: ChildWindowAreaConfig, index: number) {
|
|
|
|
|
editingWindowAreaTabId.value = area.id;
|
|
|
|
|
editingWindowAreaTabName.value = resolveWindowAreaTabLabel(area, index);
|
|
|
|
|
void nextTick(() => {
|
|
|
|
|
windowAreaTabInputRef.value?.focus();
|
|
|
|
|
windowAreaTabInputRef.value?.select();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function finishEditingWindowAreaTabLabel(area: ChildWindowAreaConfig, save: boolean) {
|
|
|
|
|
if (editingWindowAreaTabId.value !== area.id) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (save) {
|
|
|
|
|
area.title = editingWindowAreaTabName.value.trim();
|
|
|
|
|
}
|
|
|
|
|
editingWindowAreaTabId.value = "";
|
|
|
|
|
editingWindowAreaTabName.value = "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 添加一个新分段,默认放在当前末段之后。
|
|
|
|
|
*/
|
|
|
|
|
@ -499,25 +710,78 @@ function removeSegment(index: number) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addWindowArea() {
|
|
|
|
|
draft.windowAreas.push(createDefaultWindowArea(draft.windowAreas.length));
|
|
|
|
|
const next = createDefaultWindowArea(draft.windowAreas.length);
|
|
|
|
|
draft.windowAreas.push(next);
|
|
|
|
|
activeWindowAreaTab.value = next.id;
|
|
|
|
|
saveMessage.value = "已修改,点击“保存配置”后生效。";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeWindowArea(index: number) {
|
|
|
|
|
const removed = draft.windowAreas[index];
|
|
|
|
|
draft.windowAreas.splice(index, 1);
|
|
|
|
|
if (removed && editingWindowAreaTabId.value === removed.id) {
|
|
|
|
|
editingWindowAreaTabId.value = "";
|
|
|
|
|
editingWindowAreaTabName.value = "";
|
|
|
|
|
}
|
|
|
|
|
if (removed && activeWindowAreaTab.value === removed.id) {
|
|
|
|
|
activeWindowAreaTab.value = draft.windowAreas[index]?.id ?? draft.windowAreas[index - 1]?.id ?? "";
|
|
|
|
|
}
|
|
|
|
|
saveMessage.value = "已修改,点击“保存配置”后生效。";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addSubtitleArea() {
|
|
|
|
|
draft.subtitleAreas.push(createDefaultSubtitleArea(draft.subtitleAreas.length));
|
|
|
|
|
const next = createDefaultSubtitleArea(draft.subtitleAreas.length);
|
|
|
|
|
draft.subtitleAreas.push(next);
|
|
|
|
|
activeSubtitleAreaTab.value = next.id;
|
|
|
|
|
saveMessage.value = "已修改,点击“保存配置”后生效。";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeSubtitleArea(index: number) {
|
|
|
|
|
const removed = draft.subtitleAreas[index];
|
|
|
|
|
draft.subtitleAreas.splice(index, 1);
|
|
|
|
|
if (removed && editingSubtitleTabId.value === removed.id) {
|
|
|
|
|
editingSubtitleTabId.value = "";
|
|
|
|
|
editingSubtitleTabName.value = "";
|
|
|
|
|
}
|
|
|
|
|
if (removed && activeSubtitleAreaTab.value === removed.id) {
|
|
|
|
|
activeSubtitleAreaTab.value =
|
|
|
|
|
draft.subtitleAreas[index]?.id ?? draft.subtitleAreas[index - 1]?.id ?? "";
|
|
|
|
|
}
|
|
|
|
|
saveMessage.value = "已修改,点击“保存配置”后生效。";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveSubtitleAreaTabLabel(area: SubtitleAreaConfig, index: number): string {
|
|
|
|
|
const title = typeof area.title === "string" ? area.title.trim() : "";
|
|
|
|
|
if (title) {
|
|
|
|
|
return title;
|
|
|
|
|
}
|
|
|
|
|
const text = typeof area.text === "string" ? area.text.trim() : "";
|
|
|
|
|
if (text) {
|
|
|
|
|
return text;
|
|
|
|
|
}
|
|
|
|
|
return `字幕${index + 1}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startEditingSubtitleTabLabel(area: SubtitleAreaConfig, index: number) {
|
|
|
|
|
editingSubtitleTabId.value = area.id;
|
|
|
|
|
editingSubtitleTabName.value = resolveSubtitleAreaTabLabel(area, index);
|
|
|
|
|
void nextTick(() => {
|
|
|
|
|
subtitleTabInputRef.value?.focus();
|
|
|
|
|
subtitleTabInputRef.value?.select();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function finishEditingSubtitleTabLabel(area: SubtitleAreaConfig, save: boolean) {
|
|
|
|
|
if (editingSubtitleTabId.value !== area.id) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (save) {
|
|
|
|
|
area.title = editingSubtitleTabName.value.trim();
|
|
|
|
|
}
|
|
|
|
|
editingSubtitleTabId.value = "";
|
|
|
|
|
editingSubtitleTabName.value = "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function startSocketService() {
|
|
|
|
|
try {
|
|
|
|
|
const result = await invoke<SocketStatusPayload>("start_socket_service");
|
|
|
|
|
@ -614,6 +878,7 @@ async function saveConfig() {
|
|
|
|
|
const circleStyle = area.windowNumberCircleStyle ?? { size: 36, borderWidth: 1, borderRadius: 18 };
|
|
|
|
|
return {
|
|
|
|
|
id: area.id || `area-${index + 1}`,
|
|
|
|
|
title: typeof area.title === "string" ? area.title.trim() : "",
|
|
|
|
|
windowId:
|
|
|
|
|
Number.isFinite(area.windowId) && area.windowId > 0 ? Math.floor(area.windowId) : index + 1,
|
|
|
|
|
width: Number.isFinite(area.width) && area.width > 0 ? Math.floor(area.width) : 220,
|
|
|
|
|
@ -643,6 +908,7 @@ async function saveConfig() {
|
|
|
|
|
});
|
|
|
|
|
const subtitleAreas = draft.subtitleAreas.map((area, index) => ({
|
|
|
|
|
id: area.id || `subtitle-${index + 1}`,
|
|
|
|
|
title: typeof area.title === "string" ? area.title.trim() : "",
|
|
|
|
|
width: Number.isFinite(area.width) && area.width > 0 ? Math.floor(area.width) : 420,
|
|
|
|
|
height: Number.isFinite(area.height) && area.height > 0 ? Math.floor(area.height) : 28,
|
|
|
|
|
x: Number.isFinite(area.x) ? Math.max(0, Math.floor(area.x)) : 0,
|
|
|
|
|
@ -651,13 +917,69 @@ async function saveConfig() {
|
|
|
|
|
textStyle: normalizeStyle(area.textStyle),
|
|
|
|
|
speed: Number.isFinite(area.speed) && area.speed > 0 ? area.speed : 80,
|
|
|
|
|
}));
|
|
|
|
|
const compositeScreen = {
|
|
|
|
|
screenId:
|
|
|
|
|
Number.isFinite(draft.compositeScreen.screenId) && draft.compositeScreen.screenId > 0
|
|
|
|
|
? Math.floor(draft.compositeScreen.screenId)
|
|
|
|
|
: DEFAULT_BROADCAST_CONFIG.compositeScreen.screenId,
|
|
|
|
|
hallName:
|
|
|
|
|
typeof draft.compositeScreen.hallName === "string" && draft.compositeScreen.hallName.trim()
|
|
|
|
|
? draft.compositeScreen.hallName.trim()
|
|
|
|
|
: DEFAULT_BROADCAST_CONFIG.compositeScreen.hallName,
|
|
|
|
|
headerBackgroundColor:
|
|
|
|
|
typeof draft.compositeScreen.headerBackgroundColor === "string" &&
|
|
|
|
|
draft.compositeScreen.headerBackgroundColor.trim()
|
|
|
|
|
? draft.compositeScreen.headerBackgroundColor.trim()
|
|
|
|
|
: DEFAULT_BROADCAST_CONFIG.compositeScreen.headerBackgroundColor,
|
|
|
|
|
hallStyle: normalizeStyle(draft.compositeScreen.hallStyle),
|
|
|
|
|
footerSubtitle:
|
|
|
|
|
typeof draft.compositeScreen.footerSubtitle === "string"
|
|
|
|
|
? draft.compositeScreen.footerSubtitle
|
|
|
|
|
: DEFAULT_BROADCAST_CONFIG.compositeScreen.footerSubtitle,
|
|
|
|
|
footerBackgroundColor:
|
|
|
|
|
typeof draft.compositeScreen.footerBackgroundColor === "string" &&
|
|
|
|
|
draft.compositeScreen.footerBackgroundColor.trim()
|
|
|
|
|
? draft.compositeScreen.footerBackgroundColor.trim()
|
|
|
|
|
: DEFAULT_BROADCAST_CONFIG.compositeScreen.footerBackgroundColor,
|
|
|
|
|
footerSubtitleStyle: normalizeStyle(draft.compositeScreen.footerSubtitleStyle),
|
|
|
|
|
footerSubtitleSpeed:
|
|
|
|
|
Number.isFinite(draft.compositeScreen.footerSubtitleSpeed) &&
|
|
|
|
|
draft.compositeScreen.footerSubtitleSpeed > 0
|
|
|
|
|
? draft.compositeScreen.footerSubtitleSpeed
|
|
|
|
|
: DEFAULT_BROADCAST_CONFIG.compositeScreen.footerSubtitleSpeed,
|
|
|
|
|
middleBackgroundColor:
|
|
|
|
|
typeof draft.compositeScreen.middleBackgroundColor === "string" &&
|
|
|
|
|
draft.compositeScreen.middleBackgroundColor.trim()
|
|
|
|
|
? draft.compositeScreen.middleBackgroundColor.trim()
|
|
|
|
|
: DEFAULT_BROADCAST_CONFIG.compositeScreen.middleBackgroundColor,
|
|
|
|
|
middleTextStyle: normalizeStyle(draft.compositeScreen.middleTextStyle),
|
|
|
|
|
middleMaxLines:
|
|
|
|
|
Number.isFinite(draft.compositeScreen.middleMaxLines) && draft.compositeScreen.middleMaxLines > 0
|
|
|
|
|
? Math.floor(draft.compositeScreen.middleMaxLines)
|
|
|
|
|
: DEFAULT_BROADCAST_CONFIG.compositeScreen.middleMaxLines,
|
|
|
|
|
showVideo: draft.compositeScreen.showVideo === true,
|
|
|
|
|
videoUrls:
|
|
|
|
|
typeof draft.compositeScreen.videoUrls === "string" ? draft.compositeScreen.videoUrls.trim() : "",
|
|
|
|
|
videoVolume:
|
|
|
|
|
Number.isFinite(draft.compositeScreen.videoVolume)
|
|
|
|
|
? Math.min(100, Math.max(0, Math.floor(draft.compositeScreen.videoVolume)))
|
|
|
|
|
: DEFAULT_BROADCAST_CONFIG.compositeScreen.videoVolume,
|
|
|
|
|
};
|
|
|
|
|
const selectedTargetSet = new Set(selectedDeliveryTargets.value);
|
|
|
|
|
await patchConfig({
|
|
|
|
|
totalWidth,
|
|
|
|
|
segmentHeight,
|
|
|
|
|
showRuler: draft.showRuler,
|
|
|
|
|
autoStartSocket: draft.autoStartSocket === true,
|
|
|
|
|
deliveryTargets: {
|
|
|
|
|
syncScreen: selectedTargetSet.has("syncScreen"),
|
|
|
|
|
compositeScreen: selectedTargetSet.has("compositeScreen"),
|
|
|
|
|
voiceBroadcast: selectedTargetSet.has("voiceBroadcast"),
|
|
|
|
|
},
|
|
|
|
|
segments,
|
|
|
|
|
windowAreas,
|
|
|
|
|
subtitleAreas,
|
|
|
|
|
compositeScreen,
|
|
|
|
|
});
|
|
|
|
|
saveMessage.value = "保存成功,已写入配置文件并实时应用。";
|
|
|
|
|
}
|
|
|
|
|
@ -669,9 +991,19 @@ watch(
|
|
|
|
|
draft.totalWidth = Math.min(next.totalWidth, screenWidth.value);
|
|
|
|
|
draft.segmentHeight = next.segmentHeight;
|
|
|
|
|
draft.showRuler = next.showRuler;
|
|
|
|
|
draft.autoStartSocket = next.autoStartSocket;
|
|
|
|
|
draft.deliveryTargets = { ...next.deliveryTargets };
|
|
|
|
|
selectedDeliveryTargets.value = [
|
|
|
|
|
...(next.deliveryTargets.syncScreen ? ["syncScreen"] : []),
|
|
|
|
|
...(next.deliveryTargets.compositeScreen ? ["compositeScreen"] : []),
|
|
|
|
|
...(next.deliveryTargets.voiceBroadcast ? ["voiceBroadcast"] : []),
|
|
|
|
|
];
|
|
|
|
|
draft.segments = cloneSegments(next.segments);
|
|
|
|
|
draft.windowAreas = cloneWindowAreas(next.windowAreas);
|
|
|
|
|
activeWindowAreaTab.value = draft.windowAreas[0]?.id ?? "";
|
|
|
|
|
draft.subtitleAreas = cloneSubtitleAreas(next.subtitleAreas);
|
|
|
|
|
activeSubtitleAreaTab.value = draft.subtitleAreas[0]?.id ?? "";
|
|
|
|
|
draft.compositeScreen = cloneCompositeScreen(next.compositeScreen);
|
|
|
|
|
syncingFromStore.value = false;
|
|
|
|
|
},
|
|
|
|
|
{ immediate: true, deep: true },
|
|
|
|
|
@ -682,6 +1014,9 @@ watch(
|
|
|
|
|
totalWidth: draft.totalWidth,
|
|
|
|
|
segmentHeight: draft.segmentHeight,
|
|
|
|
|
showRuler: draft.showRuler,
|
|
|
|
|
autoStartSocket: draft.autoStartSocket,
|
|
|
|
|
deliveryTargets: { ...draft.deliveryTargets },
|
|
|
|
|
selectedDeliveryTargets: [...selectedDeliveryTargets.value],
|
|
|
|
|
segments: draft.segments.map((item) => ({ ...item })),
|
|
|
|
|
windowAreas: draft.windowAreas.map((item) => ({
|
|
|
|
|
...item,
|
|
|
|
|
@ -690,10 +1025,19 @@ watch(
|
|
|
|
|
staticTextStyle: { ...item.staticTextStyle },
|
|
|
|
|
dynamicTextStyle: { ...item.dynamicTextStyle },
|
|
|
|
|
})),
|
|
|
|
|
activeWindowAreaTab: activeWindowAreaTab.value,
|
|
|
|
|
subtitleAreas: draft.subtitleAreas.map((item) => ({
|
|
|
|
|
...item,
|
|
|
|
|
textStyle: { ...item.textStyle },
|
|
|
|
|
})),
|
|
|
|
|
activeSubtitleAreaTab: activeSubtitleAreaTab.value,
|
|
|
|
|
activeCompositeConfigTab: activeCompositeConfigTab.value,
|
|
|
|
|
compositeScreen: {
|
|
|
|
|
...draft.compositeScreen,
|
|
|
|
|
hallStyle: { ...draft.compositeScreen.hallStyle },
|
|
|
|
|
footerSubtitleStyle: { ...draft.compositeScreen.footerSubtitleStyle },
|
|
|
|
|
middleTextStyle: { ...draft.compositeScreen.middleTextStyle },
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
() => {
|
|
|
|
|
if (!syncingFromStore.value) {
|
|
|
|
|
|