完成开发,后续进行优化
This commit is contained in:
parent
24bbadd931
commit
32c3d30844
|
|
@ -0,0 +1,76 @@
|
||||||
|
// src/api/config.ts
|
||||||
|
import axios from "axios";
|
||||||
|
import { apiClient, handleApiError } from "./client";
|
||||||
|
|
||||||
|
// [!! 新增 !!] 为 config.json 定义一个接口
|
||||||
|
export interface MainConfig {
|
||||||
|
device_id: string;
|
||||||
|
config_base_path: string;
|
||||||
|
mqtt_broker: string;
|
||||||
|
mqtt_client_id_prefix: string;
|
||||||
|
data_storage_db_path: string;
|
||||||
|
data_cache_db_path: string;
|
||||||
|
tcp_server_ports: number[]; // 这是数字数组
|
||||||
|
web_server_port: number; // 这是数字
|
||||||
|
log_level: string;
|
||||||
|
alarm_rules_path: string;
|
||||||
|
piper_executable_path: string;
|
||||||
|
piper_model_path: string;
|
||||||
|
video_config_path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief [!! 修改 !!] 从 C++ 后端获取主 config.json 并 *解析* 为对象
|
||||||
|
* (模仿 video.ts 的 getVideoConfig)
|
||||||
|
*/
|
||||||
|
export const getMainConfig = async (): Promise<MainConfig> => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<string | MainConfig>("/api/config");
|
||||||
|
|
||||||
|
if (typeof response.data === "string") {
|
||||||
|
try {
|
||||||
|
// C++ 后端按计划返回字符串,我们在此解析
|
||||||
|
return JSON.parse(response.data) as MainConfig;
|
||||||
|
} catch (e: any) {
|
||||||
|
throw new Error(`Failed to parse config JSON: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(
|
||||||
|
"getMainConfig: /api/config 返回了对象,这与预期不符,但仍会处理。"
|
||||||
|
);
|
||||||
|
return response.data as MainConfig;
|
||||||
|
} catch (error) {
|
||||||
|
throw handleApiError(error, "getMainConfig");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief [!! 修改 !!] 将主配置 *对象* POST 到 C++ 后端
|
||||||
|
* (模仿 video.ts 的 postVideoConfig)
|
||||||
|
* @param configObject 包含配置的 *对象*
|
||||||
|
*/
|
||||||
|
export const postMainConfig = async (
|
||||||
|
configObject: MainConfig
|
||||||
|
): Promise<any> => {
|
||||||
|
try {
|
||||||
|
// 关键:我们发送的是序列化后的字符串
|
||||||
|
// 这与 postVideoConfig 的实现完全一致
|
||||||
|
const jsonString = JSON.stringify(configObject, null, 2);
|
||||||
|
|
||||||
|
const response = await apiClient.post("/api/config", jsonString, {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
// 捕获来自 C++ 后端的特定验证错误
|
||||||
|
if (axios.isAxiosError(error) && error.response?.status === 400) {
|
||||||
|
const errorMsg =
|
||||||
|
error.response.data?.message || "JSON 格式或 schema 无效";
|
||||||
|
throw new Error(`保存失败 (400): ${errorMsg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他 API 错误
|
||||||
|
throw handleApiError(error, "postMainConfig");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref } from 'vue'
|
|
||||||
|
|
||||||
defineProps<{ msg: string }>()
|
|
||||||
|
|
||||||
const count = ref(0)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<button type="button" @click="count++">count is {{ count }}</button>
|
|
||||||
<p>
|
|
||||||
Edit
|
|
||||||
<code>components/HelloWorld.vue</code> to test HMR
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Check out
|
|
||||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
|
||||||
>create-vue</a
|
|
||||||
>, the official Vue + Vite starter
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Install
|
|
||||||
<a href="https://github.com/vuejs/language-tools" target="_blank">Volar</a>
|
|
||||||
in your IDE for a better DX
|
|
||||||
</p>
|
|
||||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -64,11 +64,12 @@ import {
|
||||||
<span>视频参数配置</span>
|
<span>视频参数配置</span>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
<li class="disabled">
|
|
||||||
<a href="#">
|
<li>
|
||||||
|
<RouterLink to="/configManager">
|
||||||
<Setting class="nav-icon" />
|
<Setting class="nav-icon" />
|
||||||
<span>其他配置</span>
|
<span>主参数配置</span>
|
||||||
</a>
|
</RouterLink>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import DevicesView from "../views/DevicesView.vue";
|
||||||
import DeviceManager from "../views/DeviceManager.vue";
|
import DeviceManager from "../views/DeviceManager.vue";
|
||||||
import VideoStreams from "../views/VideoStreams.vue";
|
import VideoStreams from "../views/VideoStreams.vue";
|
||||||
import VideoConfigManager from "../views/VideoConfigManager.vue";
|
import VideoConfigManager from "../views/VideoConfigManager.vue";
|
||||||
|
import MainConfigManager from "../views/MainConfigManager.vue";
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
|
|
@ -45,6 +46,11 @@ const routes = [
|
||||||
name: "VideoConfigManager",
|
name: "VideoConfigManager",
|
||||||
component: VideoConfigManager,
|
component: VideoConfigManager,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/configManager",
|
||||||
|
name: "MainConfigManager",
|
||||||
|
component: MainConfigManager,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,303 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from "vue";
|
||||||
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
|
import { getMainConfig, postMainConfig, type MainConfig } from "../api/config"; // [!! 修改 !!] 导入新函数和接口
|
||||||
|
import { postServiceReload } from "../api/video";
|
||||||
|
import { RefreshLeft } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
// --- [!! 修改 !!] 状态变量 ---
|
||||||
|
// 模仿 VideoConfigManager 的 originalStreams 和 editableStreams
|
||||||
|
const originalConfig = ref<MainConfig | null>(null);
|
||||||
|
const editableConfig = ref<MainConfig | null>(null);
|
||||||
|
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isSaving = ref(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief [!! 新增 !!] 处理 TCP 端口数组的辅助计算属性
|
||||||
|
* 因为 el-input 只能绑定字符串
|
||||||
|
*/
|
||||||
|
const editableTcpPortsString = computed({
|
||||||
|
get: () => {
|
||||||
|
return editableConfig.value?.tcp_server_ports.join(", ") || "";
|
||||||
|
},
|
||||||
|
set: (newValue: string) => {
|
||||||
|
if (editableConfig.value) {
|
||||||
|
// 将 "123, 456" 转换回 [123, 456]
|
||||||
|
editableConfig.value.tcp_server_ports = newValue
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter((s) => s.length > 0)
|
||||||
|
.map((s) => parseInt(s, 10))
|
||||||
|
.filter((n) => !isNaN(n)); // 过滤掉无效数字
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- [!! 修改 !!] 刷新逻辑 ---
|
||||||
|
// 模仿 VideoConfigManager 的 handleRefresh
|
||||||
|
const handleRefresh = async (confirm = false) => {
|
||||||
|
if (isDirty.value && confirm) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
"您有未保存的更改。刷新将从服务器重新加载配置,并丢弃您当前的所有更改。",
|
||||||
|
"确认刷新?",
|
||||||
|
{ type: "warning" }
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return; // 用户取消
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const configData = await getMainConfig();
|
||||||
|
|
||||||
|
// 存储原始状态 (深拷贝)
|
||||||
|
originalConfig.value = JSON.parse(JSON.stringify(configData));
|
||||||
|
// 存储用于编辑的状态 (深拷贝)
|
||||||
|
editableConfig.value = JSON.parse(JSON.stringify(configData));
|
||||||
|
|
||||||
|
ElMessage.success("主配置已刷新");
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(`刷新失败: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
handleRefresh(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- [!! 修改 !!] 脏检查逻辑 ---
|
||||||
|
// 模仿 VideoConfigManager 的 isDirty
|
||||||
|
const isDirty = computed(() => {
|
||||||
|
if (!originalConfig.value || !editableConfig.value) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 比较原始对象和当前编辑的对象
|
||||||
|
return (
|
||||||
|
JSON.stringify(originalConfig.value) !==
|
||||||
|
JSON.stringify(editableConfig.value)
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- [!! 修改 !!] 保存并应用 ---
|
||||||
|
// 模仿 VideoConfigManager 的 handleSaveAndApply
|
||||||
|
const handleSaveAndApply = async () => {
|
||||||
|
if (!editableConfig.value) {
|
||||||
|
ElMessage.error("配置数据未加载,无法保存。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSaving.value = true;
|
||||||
|
try {
|
||||||
|
// 1. 发送配置对象
|
||||||
|
await postMainConfig(editableConfig.value);
|
||||||
|
ElMessage.success("主配置保存成功!");
|
||||||
|
|
||||||
|
// 2. 发送重启命令
|
||||||
|
await postServiceReload();
|
||||||
|
ElMessage.info("服务重启命令已发送,请稍候...");
|
||||||
|
|
||||||
|
// 3. 保存成功后,更新 "原始" 状态
|
||||||
|
// 模仿 VideoConfigManager 更新 originalStreams
|
||||||
|
originalConfig.value = JSON.parse(JSON.stringify(editableConfig.value));
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(`操作失败: ${error.message}`);
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="config-manager-page">
|
||||||
|
<header class="page-header">
|
||||||
|
<h1>
|
||||||
|
主参数配置 (config.json)
|
||||||
|
<el-tag
|
||||||
|
v-if="isDirty"
|
||||||
|
type="warning"
|
||||||
|
effect="dark"
|
||||||
|
size="small"
|
||||||
|
style="margin-left: 10px"
|
||||||
|
>
|
||||||
|
* 未保存
|
||||||
|
</el-tag>
|
||||||
|
</h1>
|
||||||
|
<div class="button-group">
|
||||||
|
<el-button
|
||||||
|
@click="handleRefresh(true)"
|
||||||
|
:loading="isLoading"
|
||||||
|
:icon="RefreshLeft"
|
||||||
|
>
|
||||||
|
刷新 (撤销更改)
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleSaveAndApply"
|
||||||
|
:loading="isSaving"
|
||||||
|
:disabled="!isDirty"
|
||||||
|
>
|
||||||
|
保存并应用 (重启服务)
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<el-card
|
||||||
|
class="config-editor-card"
|
||||||
|
v-loading="isLoading || !editableConfig"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>编辑 config.json</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-alert
|
||||||
|
title="警告:修改主配置文件"
|
||||||
|
type="warning"
|
||||||
|
show-icon
|
||||||
|
:closable="false"
|
||||||
|
style="margin-bottom: 20px"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
更改此文件将影响服务核心功能。保存后必须
|
||||||
|
<strong>"保存并应用"</strong>
|
||||||
|
以触发服务重启,新配置才能生效。
|
||||||
|
</p>
|
||||||
|
</el-alert>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
v-if="editableConfig"
|
||||||
|
:model="editableConfig"
|
||||||
|
label-position="right"
|
||||||
|
label-width="200px"
|
||||||
|
>
|
||||||
|
<el-form-item label="设备 ID (device_id)" prop="device_id">
|
||||||
|
<el-input v-model="editableConfig.device_id" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item
|
||||||
|
label="配置根路径 (config_base_path)"
|
||||||
|
prop="config_base_path"
|
||||||
|
>
|
||||||
|
<el-input v-model="editableConfig.config_base_path" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="日志级别 (log_level)" prop="log_level">
|
||||||
|
<el-select v-model="editableConfig.log_level" placeholder="Select">
|
||||||
|
<el-option label="Trace" value="trace" />
|
||||||
|
<el-option label="Debug" value="debug" />
|
||||||
|
<el-option label="Info" value="info" />
|
||||||
|
<el-option label="Warning" value="warn" />
|
||||||
|
<el-option label="Error" value="error" />
|
||||||
|
<el-option label="Critical" value="critical" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider content-position="left">网络与服务</el-divider>
|
||||||
|
<el-form-item label="MQTT Broker URL" prop="mqtt_broker">
|
||||||
|
<el-input
|
||||||
|
v-model="editableConfig.mqtt_broker"
|
||||||
|
placeholder="tcp://localhost:1883"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="MQTT Client ID 前缀" prop="mqtt_client_id_prefix">
|
||||||
|
<el-input v-model="editableConfig.mqtt_client_id_prefix" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Web 服务端口" prop="web_server_port">
|
||||||
|
<el-input-number
|
||||||
|
v-model="editableConfig.web_server_port"
|
||||||
|
:min="1"
|
||||||
|
:max="65535"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="TCP 服务端口 (逗号分隔)" prop="tcp_server_ports">
|
||||||
|
<el-input
|
||||||
|
v-model="editableTcpPortsString"
|
||||||
|
placeholder="例如: 12345, 12346"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-divider content-position="left">文件路径</el-divider>
|
||||||
|
<el-form-item
|
||||||
|
label="数据存储DB (data_storage_db_path)"
|
||||||
|
prop="data_storage_db_path"
|
||||||
|
>
|
||||||
|
<el-input v-model="editableConfig.data_storage_db_path" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item
|
||||||
|
label="数据缓存DB (data_cache_db_path)"
|
||||||
|
prop="data_cache_db_path"
|
||||||
|
>
|
||||||
|
<el-input v-model="editableConfig.data_cache_db_path" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item
|
||||||
|
label="告警规则 (alarm_rules_path)"
|
||||||
|
prop="alarm_rules_path"
|
||||||
|
>
|
||||||
|
<el-input v-model="editableConfig.alarm_rules_path" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item
|
||||||
|
label="视频配置 (video_config_path)"
|
||||||
|
prop="video_config_path"
|
||||||
|
>
|
||||||
|
<el-input v-model="editableConfig.video_config_path" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Piper 可执行文件" prop="piper_executable_path">
|
||||||
|
<el-input v-model="editableConfig.piper_executable_path" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Piper 模型" prop="piper_model_path">
|
||||||
|
<el-input v-model="editableConfig.piper_model_path" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 样式与之前相同,但我们确保卡片可以滚动 */
|
||||||
|
.config-manager-page {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-editor-card {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden; /* 确保卡片可以滚动 */
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: auto; /* [!! 关键 !!] 使表单内容可滚动 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 限制表单宽度以便于阅读 */
|
||||||
|
.el-form {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -10,25 +10,25 @@ import {
|
||||||
} from "../api/video";
|
} from "../api/video";
|
||||||
import { Plus, Edit, Delete, RefreshLeft } from "@element-plus/icons-vue";
|
import { Plus, Edit, Delete, RefreshLeft } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
// --- [修改] 状态变量 ---
|
// --- 状态变量 ---
|
||||||
|
|
||||||
// 用于存储 video_service.enabled
|
|
||||||
const videoServiceEnabled = ref(true);
|
const videoServiceEnabled = ref(true);
|
||||||
// 用于存储原始的 video_streams 数组
|
// [!! 修复 1 !!] 新增一个 ref 来存储 *原始* 的启用状态
|
||||||
|
const originalVideoServiceEnabled = ref(true);
|
||||||
|
|
||||||
const originalStreams = ref<VideoStreamConfig[]>([]);
|
const originalStreams = ref<VideoStreamConfig[]>([]);
|
||||||
// 用于表格编辑的 video_streams 数组
|
|
||||||
const editableStreams = ref<VideoStreamConfig[]>([]);
|
const editableStreams = ref<VideoStreamConfig[]>([]);
|
||||||
|
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
const isReloading = ref(false);
|
// [!! 移除 2 !!] 移除了 isReloading
|
||||||
const isDialogVisible = ref(false);
|
const isDialogVisible = ref(false);
|
||||||
const dialogMode = ref<"add" | "edit">("add");
|
const dialogMode = ref<"add" | "edit">("add");
|
||||||
const currentEditingStream = ref<Partial<VideoStreamConfig>>({});
|
const currentEditingStream = ref<Partial<VideoStreamConfig>>({});
|
||||||
const currentEditingIndex = ref(-1);
|
const currentEditingIndex = ref(-1);
|
||||||
const formRef = ref<FormInstance>();
|
const formRef = ref<FormInstance>();
|
||||||
|
|
||||||
// --- [修改] 刷新逻辑 ---
|
// --- 刷新逻辑 ---
|
||||||
const handleRefresh = async (confirm = false) => {
|
const handleRefresh = async (confirm = false) => {
|
||||||
if (isDirty.value && confirm) {
|
if (isDirty.value && confirm) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -43,14 +43,14 @@ const handleRefresh = async (confirm = false) => {
|
||||||
}
|
}
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
// 1. 获取完整的配置对象
|
|
||||||
const fullConfig = await getVideoConfig();
|
const fullConfig = await getVideoConfig();
|
||||||
|
|
||||||
// 2. 扁平化:拆分顶层设置和流数组
|
// [!! 修复 1 !!] 同时设置当前值和原始值
|
||||||
videoServiceEnabled.value = fullConfig.video_service?.enabled ?? false;
|
const enabledState = fullConfig.video_service?.enabled ?? false;
|
||||||
originalStreams.value = fullConfig.video_streams || [];
|
videoServiceEnabled.value = enabledState;
|
||||||
|
originalVideoServiceEnabled.value = enabledState; // 存储原始状态
|
||||||
|
|
||||||
// 深度复制用于编辑
|
originalStreams.value = fullConfig.video_streams || [];
|
||||||
editableStreams.value = JSON.parse(JSON.stringify(originalStreams.value));
|
editableStreams.value = JSON.parse(JSON.stringify(originalStreams.value));
|
||||||
|
|
||||||
ElMessage.success("视频配置已刷新");
|
ElMessage.success("视频配置已刷新");
|
||||||
|
|
@ -65,25 +65,26 @@ onMounted(() => {
|
||||||
handleRefresh(false);
|
handleRefresh(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- [修改] 脏检查逻辑 ---
|
// --- [!! 修复 1 !!] 脏检查逻辑 ---
|
||||||
const isDirty = computed(() => {
|
const isDirty = computed(() => {
|
||||||
// 必须同时检查顶层开关和流数组
|
|
||||||
try {
|
try {
|
||||||
|
// 原始配置使用 *original* 状态
|
||||||
const originalConfig = {
|
const originalConfig = {
|
||||||
video_service: { enabled: videoServiceEnabled.value }, // 使用加载时的值
|
video_service: { enabled: originalVideoServiceEnabled.value }, // <-- 使用原始值
|
||||||
video_streams: originalStreams.value,
|
video_streams: originalStreams.value,
|
||||||
};
|
};
|
||||||
|
// 当前配置使用 *v-model* 绑定的当前值
|
||||||
const currentConfig = {
|
const currentConfig = {
|
||||||
video_service: { enabled: videoServiceEnabled.value }, // 使用当前的值
|
video_service: { enabled: videoServiceEnabled.value },
|
||||||
video_streams: editableStreams.value,
|
video_streams: editableStreams.value,
|
||||||
};
|
};
|
||||||
return JSON.stringify(originalConfig) !== JSON.stringify(currentConfig);
|
return JSON.stringify(originalConfig) !== JSON.stringify(currentConfig);
|
||||||
} catch {
|
} catch {
|
||||||
return true;
|
return true; // 发生错误时,假定为 dirty 以防万一
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- [修改] 弹窗逻辑 (适配 VideoStreamConfig) ---
|
// --- 弹窗逻辑 (不变) ---
|
||||||
const openAddDialog = () => {
|
const openAddDialog = () => {
|
||||||
dialogMode.value = "add";
|
dialogMode.value = "add";
|
||||||
currentEditingStream.value = {
|
currentEditingStream.value = {
|
||||||
|
|
@ -106,7 +107,6 @@ const openAddDialog = () => {
|
||||||
const openEditDialog = (row: VideoStreamConfig, index: number) => {
|
const openEditDialog = (row: VideoStreamConfig, index: number) => {
|
||||||
dialogMode.value = "edit";
|
dialogMode.value = "edit";
|
||||||
currentEditingIndex.value = index;
|
currentEditingIndex.value = index;
|
||||||
// 关键:module_config 必须是字符串才能在 textarea 中编辑
|
|
||||||
currentEditingStream.value = {
|
currentEditingStream.value = {
|
||||||
...JSON.parse(JSON.stringify(row)),
|
...JSON.parse(JSON.stringify(row)),
|
||||||
module_config: JSON.stringify(row.module_config, null, 2),
|
module_config: JSON.stringify(row.module_config, null, 2),
|
||||||
|
|
@ -115,13 +115,12 @@ const openEditDialog = (row: VideoStreamConfig, index: number) => {
|
||||||
nextTick(() => formRef.value?.clearValidate());
|
nextTick(() => formRef.value?.clearValidate());
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- [修改] 弹窗确认 (适配 VideoStreamConfig) ---
|
// --- 弹窗确认 (不变) ---
|
||||||
const handleDialogConfirm = async () => {
|
const handleDialogConfirm = async () => {
|
||||||
if (!formRef.value) return;
|
if (!formRef.value) return;
|
||||||
await formRef.value.validate((valid) => {
|
await formRef.value.validate((valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
try {
|
try {
|
||||||
// 关键:将 module_config 字符串转换回 JSON 对象
|
|
||||||
const streamToSave = {
|
const streamToSave = {
|
||||||
...currentEditingStream.value,
|
...currentEditingStream.value,
|
||||||
module_config: JSON.parse(
|
module_config: JSON.parse(
|
||||||
|
|
@ -146,7 +145,7 @@ const handleDialogConfirm = async () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- [修改] 删除逻辑 (适配 editableStreams) ---
|
// --- 删除逻辑 (不变) ---
|
||||||
const deleteRule = (index: number) => {
|
const deleteRule = (index: number) => {
|
||||||
ElMessageBox.confirm(
|
ElMessageBox.confirm(
|
||||||
`您确定要删除流 '${editableStreams.value[index].id}' 吗?`,
|
`您确定要删除流 '${editableStreams.value[index].id}' 吗?`,
|
||||||
|
|
@ -159,11 +158,10 @@ const deleteRule = (index: number) => {
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- [修改] 保存并应用 (适配 video API) ---
|
// --- 保存并应用 (不变) ---
|
||||||
const handleSaveAndApply = async () => {
|
const handleSaveAndApply = async () => {
|
||||||
isSaving.value = true;
|
isSaving.value = true;
|
||||||
try {
|
try {
|
||||||
// 1. 合并:将顶层设置和流数组重新组装
|
|
||||||
const fullConfigToSave: VideoConfig = {
|
const fullConfigToSave: VideoConfig = {
|
||||||
video_service: {
|
video_service: {
|
||||||
enabled: videoServiceEnabled.value,
|
enabled: videoServiceEnabled.value,
|
||||||
|
|
@ -171,16 +169,15 @@ const handleSaveAndApply = async () => {
|
||||||
video_streams: editableStreams.value,
|
video_streams: editableStreams.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. 保存配置
|
|
||||||
await postVideoConfig(fullConfigToSave);
|
await postVideoConfig(fullConfigToSave);
|
||||||
ElMessage.success("配置保存成功!");
|
ElMessage.success("配置保存成功!");
|
||||||
|
|
||||||
// 3. 应用配置 (后端重启)
|
|
||||||
await postServiceReload();
|
await postServiceReload();
|
||||||
ElMessage.info("服务重启命令已发送,请稍候...");
|
ElMessage.info("服务重启命令已发送,请稍候...");
|
||||||
|
|
||||||
// 4. 更新本地 "original" 状态
|
// [!! 修复 1 !!] 保存成功后,更新 *所有* 原始状态
|
||||||
originalStreams.value = JSON.parse(JSON.stringify(editableStreams.value));
|
originalStreams.value = JSON.parse(JSON.stringify(editableStreams.value));
|
||||||
|
originalVideoServiceEnabled.value = videoServiceEnabled.value;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
ElMessage.error(`操作失败: ${error.message}`);
|
ElMessage.error(`操作失败: ${error.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -188,18 +185,7 @@ const handleSaveAndApply = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- [修改] 仅应用 (适配 video API) ---
|
// [!! 移除 2 !!] 移除了 handleApplyOnly 函数
|
||||||
const handleApplyOnly = async () => {
|
|
||||||
isReloading.value = true;
|
|
||||||
try {
|
|
||||||
await postServiceReload();
|
|
||||||
ElMessage.success("服务重启命令已发送");
|
|
||||||
} catch (error: any) {
|
|
||||||
ElMessage.error(`应用失败: ${error.message}`);
|
|
||||||
} finally {
|
|
||||||
isReloading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -228,13 +214,6 @@ const handleApplyOnly = async () => {
|
||||||
>
|
>
|
||||||
刷新 (撤销更改)
|
刷新 (撤销更改)
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
|
||||||
@click="handleApplyOnly"
|
|
||||||
:loading="isReloading"
|
|
||||||
type="warning"
|
|
||||||
>
|
|
||||||
仅应用 (重启服务)
|
|
||||||
</el-button>
|
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="handleSaveAndApply"
|
@click="handleSaveAndApply"
|
||||||
|
|
@ -387,7 +366,7 @@ const handleApplyOnly = async () => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 仿照 AlarmManager.vue 的 Flex 布局 */
|
/* 样式 (不变) */
|
||||||
.config-manager-page {
|
.config-manager-page {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
@ -395,7 +374,6 @@ const handleApplyOnly = async () => {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 头部样式 */
|
|
||||||
.page-header {
|
.page-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
@ -412,13 +390,11 @@ h1 {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* [新增] 顶部卡片样式 */
|
|
||||||
.top-config-card {
|
.top-config-card {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
flex-shrink: 0; /* 不压缩 */
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 表格容器样式 (与 AlarmManager.vue 相同) */
|
|
||||||
.table-container {
|
.table-container {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue