|
|
<template>
|
|
|
<main class="config-root config-page">
|
|
|
<div class="config-top-status row-between">
|
|
|
<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-button type="success" plain :disabled="socketRunning" @click="startSocketService">
|
|
|
启动 Socket 服务
|
|
|
</el-button>
|
|
|
<el-button type="danger" plain :disabled="!socketRunning" @click="stopSocketService">
|
|
|
停止 Socket 服务
|
|
|
</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-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-collapse-item name="segments">
|
|
|
<template #title>
|
|
|
<div class="card-header row-between">
|
|
|
<span>分段列表(主屏宽度上限 {{ screenWidth }}px)</span>
|
|
|
<el-button type="primary" plain size="small" @click.stop="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">
|
|
|
<template #default="{ row }">
|
|
|
<el-input-number v-model="row.length" :min="1" :max="screenWidth" :step="1" />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="X" width="120">
|
|
|
<template #default="{ row }">
|
|
|
<el-input-number v-model="row.x" :min="0" :step="1" />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="Y" width="120">
|
|
|
<template #default="{ row }">
|
|
|
<el-input-number v-model="row.y" :min="0" :step="1" />
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
<el-table-column label="操作" width="90">
|
|
|
<template #default="{ $index }">
|
|
|
<el-button type="danger" link @click="removeSegment($index)">删除</el-button>
|
|
|
</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-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>
|
|
|
</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>
|
|
|
<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-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-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-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-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>
|
|
|
</div>
|
|
|
</el-collapse-item>
|
|
|
|
|
|
<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-button>
|
|
|
</div>
|
|
|
</template>
|
|
|
<div class="panel-scroll area-list">
|
|
|
<el-card
|
|
|
v-for="(subtitle, index) in draft.subtitleAreas"
|
|
|
:key="subtitle.id"
|
|
|
class="area-item"
|
|
|
shadow="never"
|
|
|
>
|
|
|
<template #header>
|
|
|
<div class="row-between">
|
|
|
<strong>字幕区域 {{ index + 1 }}</strong>
|
|
|
<el-button type="danger" link @click="removeSubtitleArea(index)">删除</el-button>
|
|
|
</div>
|
|
|
</template>
|
|
|
<el-form label-width="120px" size="small">
|
|
|
<el-row :gutter="12">
|
|
|
<el-col :span="6">
|
|
|
<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-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>
|
|
|
</div>
|
|
|
</el-collapse-item>
|
|
|
</el-collapse>
|
|
|
|
|
|
<div class="actions-row">
|
|
|
<span class="version-text">版本号:V{{ appVersion }}</span>
|
|
|
<el-button type="primary" plain :loading="checkingUpdate" @click="handleCheckUpdate">
|
|
|
检查更新
|
|
|
</el-button>
|
|
|
<el-button @click="closeConfigWindow">关闭配置窗口</el-button>
|
|
|
<el-button type="danger" @click="quitApplication">退出程序</el-button>
|
|
|
<span class="save-hint">{{ saveMessage }}</span>
|
|
|
</div>
|
|
|
</main>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
import { computed, 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";
|
|
|
import { ElMessage } from "element-plus";
|
|
|
import type {
|
|
|
BroadcastConfig,
|
|
|
ChildWindowAreaConfig,
|
|
|
SegmentConfigItem,
|
|
|
SubtitleAreaConfig,
|
|
|
} from "../models/config";
|
|
|
import { DEFAULT_BROADCAST_CONFIG } from "../models/config";
|
|
|
import { useBroadcastConfig } from "../composables/useBroadcastConfig";
|
|
|
import {
|
|
|
buildSegmentsFromConfig,
|
|
|
normalizeScreenWidth,
|
|
|
normalizeSegmentConfigItem,
|
|
|
normalizeSegmentHeight,
|
|
|
} from "../services/segmentService";
|
|
|
|
|
|
interface SocketStatusPayload {
|
|
|
running: boolean;
|
|
|
port: number;
|
|
|
}
|
|
|
|
|
|
interface AptUpdateCheckResult {
|
|
|
packageName: string;
|
|
|
currentVersion: string;
|
|
|
installedVersion: string;
|
|
|
candidateVersion: string;
|
|
|
hasUpdate: boolean;
|
|
|
sourceAvailable: boolean;
|
|
|
updateCommand: string;
|
|
|
}
|
|
|
|
|
|
// 当前主屏宽度(用于限制总长度与分段长度)。
|
|
|
const screenWidth = ref(normalizeScreenWidth(window.screen.width || 1920));
|
|
|
const { config, patchConfig } = useBroadcastConfig();
|
|
|
const saveMessage = ref("修改后请点击“保存配置”。");
|
|
|
const activePanels = ref(["base", "segments", "areas", "subtitles"]);
|
|
|
const socketRunning = ref(false);
|
|
|
const checkingUpdate = ref(false);
|
|
|
const appVersion = ref("0.1.0");
|
|
|
const APT_SOURCE_ENTRY =
|
|
|
"deb [arch=amd64 signed-by=/usr/share/keyrings/zyyun-archive-keyring.gpg] http://80.12.140.29:80/apt v10 main";
|
|
|
const APT_SOURCE_SETUP_COMMAND = `echo "${APT_SOURCE_ENTRY}" | sudo tee /etc/apt/sources.list.d/zyyun.list && sudo apt update`;
|
|
|
// 避免 store -> draft 回填触发“未保存”提示。
|
|
|
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,
|
|
|
),
|
|
|
);
|
|
|
|
|
|
/**
|
|
|
* 深拷贝分段数组,避免引用同一对象导致联动副作用。
|
|
|
*/
|
|
|
function cloneSegments(segments: SegmentConfigItem[]) {
|
|
|
return segments.map((item) => ({ ...item }));
|
|
|
}
|
|
|
|
|
|
function cloneWindowAreas(areas: ChildWindowAreaConfig[]) {
|
|
|
return areas.map((item) => ({
|
|
|
...item,
|
|
|
windowNumberCircleStyle: { ...item.windowNumberCircleStyle },
|
|
|
windowNumberStyle: { ...item.windowNumberStyle },
|
|
|
staticTextStyle: { ...item.staticTextStyle },
|
|
|
dynamicTextStyle: { ...item.dynamicTextStyle },
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
function cloneSubtitleAreas(areas: SubtitleAreaConfig[]) {
|
|
|
return areas.map((item) => ({
|
|
|
...item,
|
|
|
textStyle: { ...item.textStyle },
|
|
|
}));
|
|
|
}
|
|
|
|
|
|
function normalizeStyle(style: { fontSize: number; color: string; fontWeight: number }) {
|
|
|
return {
|
|
|
fontSize: Number.isFinite(style.fontSize) && style.fontSize > 0 ? Math.floor(style.fontSize) : 14,
|
|
|
color: typeof style.color === "string" && style.color.trim() ? style.color.trim() : "#ffffff",
|
|
|
fontWeight:
|
|
|
Number.isFinite(style.fontWeight) && style.fontWeight >= 100 ? Math.floor(style.fontWeight) : 500,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
function createDefaultWindowArea(index: number): ChildWindowAreaConfig {
|
|
|
return {
|
|
|
id: `area-${Date.now()}-${index}`,
|
|
|
windowId: index + 1,
|
|
|
isClockWindow: false,
|
|
|
width: 220,
|
|
|
height: 48,
|
|
|
x: 0,
|
|
|
y: index * 50,
|
|
|
windowNumber: String(index + 1),
|
|
|
windowNumberCircle: false,
|
|
|
windowNumberCircleStyle: {
|
|
|
size: 36,
|
|
|
borderWidth: 1,
|
|
|
borderRadius: 18,
|
|
|
},
|
|
|
windowNumberStyle: {
|
|
|
fontSize: 16,
|
|
|
color: "#ffffff",
|
|
|
fontWeight: 700,
|
|
|
},
|
|
|
staticText: "静态文本",
|
|
|
staticTextStyle: {
|
|
|
fontSize: 14,
|
|
|
color: "#ffffff",
|
|
|
fontWeight: 500,
|
|
|
},
|
|
|
dynamicText: "动态文本",
|
|
|
dynamicTextStyle: {
|
|
|
fontSize: 14,
|
|
|
color: "#ffffff",
|
|
|
fontWeight: 500,
|
|
|
},
|
|
|
};
|
|
|
}
|
|
|
|
|
|
function createDefaultSubtitleArea(index: number): SubtitleAreaConfig {
|
|
|
return {
|
|
|
id: `subtitle-${Date.now()}-${index}`,
|
|
|
width: 420,
|
|
|
height: 28,
|
|
|
x: 0,
|
|
|
y: 0,
|
|
|
text: "欢迎使用同步屏系统",
|
|
|
textStyle: {
|
|
|
fontSize: 16,
|
|
|
color: "#ffffff",
|
|
|
fontWeight: 500,
|
|
|
},
|
|
|
speed: 80,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 添加一个新分段,默认放在当前末段之后。
|
|
|
*/
|
|
|
function addSegment() {
|
|
|
const nextIndex = draft.segments.length;
|
|
|
draft.segments.push(
|
|
|
normalizeSegmentConfigItem(
|
|
|
{
|
|
|
length: Math.min(screenWidth.value, draft.totalWidth),
|
|
|
x: 0,
|
|
|
y: nextIndex * normalizeSegmentHeight(draft.segmentHeight),
|
|
|
},
|
|
|
nextIndex,
|
|
|
screenWidth.value,
|
|
|
draft.segmentHeight,
|
|
|
),
|
|
|
);
|
|
|
saveMessage.value = "已修改,点击“保存配置”后生效。";
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 删除指定下标的分段配置。
|
|
|
*/
|
|
|
function removeSegment(index: number) {
|
|
|
draft.segments.splice(index, 1);
|
|
|
saveMessage.value = "已修改,点击“保存配置”后生效。";
|
|
|
}
|
|
|
|
|
|
function addWindowArea() {
|
|
|
draft.windowAreas.push(createDefaultWindowArea(draft.windowAreas.length));
|
|
|
saveMessage.value = "已修改,点击“保存配置”后生效。";
|
|
|
}
|
|
|
|
|
|
function removeWindowArea(index: number) {
|
|
|
draft.windowAreas.splice(index, 1);
|
|
|
saveMessage.value = "已修改,点击“保存配置”后生效。";
|
|
|
}
|
|
|
|
|
|
function addSubtitleArea() {
|
|
|
draft.subtitleAreas.push(createDefaultSubtitleArea(draft.subtitleAreas.length));
|
|
|
saveMessage.value = "已修改,点击“保存配置”后生效。";
|
|
|
}
|
|
|
|
|
|
function removeSubtitleArea(index: number) {
|
|
|
draft.subtitleAreas.splice(index, 1);
|
|
|
saveMessage.value = "已修改,点击“保存配置”后生效。";
|
|
|
}
|
|
|
|
|
|
async function startSocketService() {
|
|
|
try {
|
|
|
const result = await invoke<SocketStatusPayload>("start_socket_service");
|
|
|
socketRunning.value = result.running;
|
|
|
saveMessage.value = "Socket 服务已启动,正在监听 9501 端口。";
|
|
|
} catch (error) {
|
|
|
saveMessage.value = `Socket 启动失败: ${String(error)}`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function stopSocketService() {
|
|
|
try {
|
|
|
const result = await invoke<SocketStatusPayload>("stop_socket_service");
|
|
|
socketRunning.value = result.running;
|
|
|
saveMessage.value = "Socket 服务已停止。";
|
|
|
} catch (error) {
|
|
|
saveMessage.value = `Socket 停止失败: ${String(error)}`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function quitApplication() {
|
|
|
try {
|
|
|
await invoke("quit_app");
|
|
|
} catch (error) {
|
|
|
saveMessage.value = `退出失败: ${String(error)}`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function closeConfigWindow() {
|
|
|
try {
|
|
|
await appWindow.close();
|
|
|
} catch (error) {
|
|
|
saveMessage.value = `关闭配置窗口失败: ${String(error)}`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function handleCheckUpdate() {
|
|
|
if (checkingUpdate.value) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
checkingUpdate.value = true;
|
|
|
try {
|
|
|
const result = await invoke<AptUpdateCheckResult>("check_apt_update", {
|
|
|
packageName: "broadcast-client",
|
|
|
currentVersion: "0.1.0",
|
|
|
});
|
|
|
|
|
|
if (!result.sourceAvailable) {
|
|
|
ElMessage.warning(
|
|
|
`未检测到可用更新源,请先执行:${APT_SOURCE_SETUP_COMMAND}`,
|
|
|
);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (!result.hasUpdate) {
|
|
|
ElMessage.success(`当前已是最新版本(${result.installedVersion})`);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
ElMessage.warning(
|
|
|
`发现新版本 ${result.candidateVersion},请在终端执行:${result.updateCommand}`,
|
|
|
);
|
|
|
} catch (error) {
|
|
|
ElMessage.error(`检查更新失败:${String(error)}`);
|
|
|
} finally {
|
|
|
checkingUpdate.value = false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 手动保存配置到持久化存储,并广播给同步屏窗口实时生效。
|
|
|
*/
|
|
|
async function saveConfig() {
|
|
|
const totalWidth = Math.min(Math.max(1, Math.floor(draft.totalWidth)), screenWidth.value);
|
|
|
const segmentHeight = normalizeSegmentHeight(draft.segmentHeight);
|
|
|
const segments = draft.segments.map((item, index) =>
|
|
|
normalizeSegmentConfigItem(item, index, screenWidth.value, segmentHeight),
|
|
|
);
|
|
|
const windowAreas = draft.windowAreas.map((area, index) => {
|
|
|
const circleStyle = area.windowNumberCircleStyle ?? { size: 36, borderWidth: 1, borderRadius: 18 };
|
|
|
return {
|
|
|
id: area.id || `area-${index + 1}`,
|
|
|
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,
|
|
|
height: Number.isFinite(area.height) && area.height > 0 ? Math.floor(area.height) : 48,
|
|
|
x: Number.isFinite(area.x) ? Math.max(0, Math.floor(area.x)) : 0,
|
|
|
y: Number.isFinite(area.y) ? Math.max(0, Math.floor(area.y)) : 0,
|
|
|
windowNumber: area.windowNumber || String(index + 1),
|
|
|
isClockWindow: area.isClockWindow === true,
|
|
|
windowNumberCircle: area.windowNumberCircle === true,
|
|
|
windowNumberCircleStyle: {
|
|
|
size: Number.isFinite(circleStyle.size) && circleStyle.size > 0 ? Math.floor(circleStyle.size) : 36,
|
|
|
borderWidth:
|
|
|
Number.isFinite(circleStyle.borderWidth) && circleStyle.borderWidth > 0
|
|
|
? Math.floor(circleStyle.borderWidth)
|
|
|
: 1,
|
|
|
borderRadius:
|
|
|
Number.isFinite(circleStyle.borderRadius) && circleStyle.borderRadius >= 0
|
|
|
? Math.floor(circleStyle.borderRadius)
|
|
|
: 18,
|
|
|
},
|
|
|
windowNumberStyle: normalizeStyle(area.windowNumberStyle),
|
|
|
staticText: area.staticText || "静态文本",
|
|
|
staticTextStyle: normalizeStyle(area.staticTextStyle),
|
|
|
dynamicText: area.dynamicText || "动态文本",
|
|
|
dynamicTextStyle: normalizeStyle(area.dynamicTextStyle),
|
|
|
};
|
|
|
});
|
|
|
const subtitleAreas = draft.subtitleAreas.map((area, index) => ({
|
|
|
id: area.id || `subtitle-${index + 1}`,
|
|
|
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,
|
|
|
y: Number.isFinite(area.y) ? Math.max(0, Math.floor(area.y)) : 0,
|
|
|
text: area.text || "欢迎使用同步屏系统",
|
|
|
textStyle: normalizeStyle(area.textStyle),
|
|
|
speed: Number.isFinite(area.speed) && area.speed > 0 ? area.speed : 80,
|
|
|
}));
|
|
|
await patchConfig({
|
|
|
totalWidth,
|
|
|
segmentHeight,
|
|
|
showRuler: draft.showRuler,
|
|
|
segments,
|
|
|
windowAreas,
|
|
|
subtitleAreas,
|
|
|
});
|
|
|
saveMessage.value = "保存成功,已写入配置文件并实时应用。";
|
|
|
}
|
|
|
|
|
|
watch(
|
|
|
config,
|
|
|
(next) => {
|
|
|
syncingFromStore.value = true;
|
|
|
draft.totalWidth = Math.min(next.totalWidth, screenWidth.value);
|
|
|
draft.segmentHeight = next.segmentHeight;
|
|
|
draft.showRuler = next.showRuler;
|
|
|
draft.segments = cloneSegments(next.segments);
|
|
|
draft.windowAreas = cloneWindowAreas(next.windowAreas);
|
|
|
draft.subtitleAreas = cloneSubtitleAreas(next.subtitleAreas);
|
|
|
syncingFromStore.value = false;
|
|
|
},
|
|
|
{ immediate: true, deep: true },
|
|
|
);
|
|
|
|
|
|
watch(
|
|
|
() => ({
|
|
|
totalWidth: draft.totalWidth,
|
|
|
segmentHeight: draft.segmentHeight,
|
|
|
showRuler: draft.showRuler,
|
|
|
segments: draft.segments.map((item) => ({ ...item })),
|
|
|
windowAreas: draft.windowAreas.map((item) => ({
|
|
|
...item,
|
|
|
windowNumberCircleStyle: { ...item.windowNumberCircleStyle },
|
|
|
windowNumberStyle: { ...item.windowNumberStyle },
|
|
|
staticTextStyle: { ...item.staticTextStyle },
|
|
|
dynamicTextStyle: { ...item.dynamicTextStyle },
|
|
|
})),
|
|
|
subtitleAreas: draft.subtitleAreas.map((item) => ({
|
|
|
...item,
|
|
|
textStyle: { ...item.textStyle },
|
|
|
})),
|
|
|
}),
|
|
|
() => {
|
|
|
if (!syncingFromStore.value) {
|
|
|
saveMessage.value = "已修改,点击“保存配置”后生效。";
|
|
|
}
|
|
|
},
|
|
|
{ deep: true },
|
|
|
);
|
|
|
|
|
|
onMounted(async () => {
|
|
|
try {
|
|
|
const monitor = await currentMonitor();
|
|
|
const width = monitor?.size?.width;
|
|
|
if (typeof width === "number" && Number.isFinite(width) && width > 0) {
|
|
|
screenWidth.value = normalizeScreenWidth(width);
|
|
|
if (draft.totalWidth > screenWidth.value) {
|
|
|
draft.totalWidth = screenWidth.value;
|
|
|
}
|
|
|
}
|
|
|
} catch {
|
|
|
// 浏览器模式下忽略主屏信息读取失败。
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const status = await invoke<SocketStatusPayload>("get_socket_service_status");
|
|
|
socketRunning.value = status.running;
|
|
|
} catch {
|
|
|
socketRunning.value = false;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
socketStatusUnlisten = await listen<SocketStatusPayload>("socket-status", (event) => {
|
|
|
socketRunning.value = event.payload.running;
|
|
|
});
|
|
|
} catch {
|
|
|
socketStatusUnlisten = null;
|
|
|
}
|
|
|
});
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
socketStatusUnlisten?.();
|
|
|
socketStatusUnlisten = null;
|
|
|
});
|
|
|
</script>
|