完成开发,后续进行优化
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>
|
||||
</RouterLink>
|
||||
</li>
|
||||
<li class="disabled">
|
||||
<a href="#">
|
||||
|
||||
<li>
|
||||
<RouterLink to="/configManager">
|
||||
<Setting class="nav-icon" />
|
||||
<span>其他配置</span>
|
||||
</a>
|
||||
<span>主参数配置</span>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import DevicesView from "../views/DevicesView.vue";
|
|||
import DeviceManager from "../views/DeviceManager.vue";
|
||||
import VideoStreams from "../views/VideoStreams.vue";
|
||||
import VideoConfigManager from "../views/VideoConfigManager.vue";
|
||||
import MainConfigManager from "../views/MainConfigManager.vue";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
|
|
@ -45,6 +46,11 @@ const routes = [
|
|||
name: "VideoConfigManager",
|
||||
component: VideoConfigManager,
|
||||
},
|
||||
{
|
||||
path: "/configManager",
|
||||
name: "MainConfigManager",
|
||||
component: MainConfigManager,
|
||||
},
|
||||
];
|
||||
|
||||
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";
|
||||
import { Plus, Edit, Delete, RefreshLeft } from "@element-plus/icons-vue";
|
||||
|
||||
// --- [修改] 状态变量 ---
|
||||
// --- 状态变量 ---
|
||||
|
||||
// 用于存储 video_service.enabled
|
||||
const videoServiceEnabled = ref(true);
|
||||
// 用于存储原始的 video_streams 数组
|
||||
// [!! 修复 1 !!] 新增一个 ref 来存储 *原始* 的启用状态
|
||||
const originalVideoServiceEnabled = ref(true);
|
||||
|
||||
const originalStreams = ref<VideoStreamConfig[]>([]);
|
||||
// 用于表格编辑的 video_streams 数组
|
||||
const editableStreams = ref<VideoStreamConfig[]>([]);
|
||||
|
||||
const isLoading = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const isReloading = ref(false);
|
||||
// [!! 移除 2 !!] 移除了 isReloading
|
||||
const isDialogVisible = ref(false);
|
||||
const dialogMode = ref<"add" | "edit">("add");
|
||||
const currentEditingStream = ref<Partial<VideoStreamConfig>>({});
|
||||
const currentEditingIndex = ref(-1);
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
// --- [修改] 刷新逻辑 ---
|
||||
// --- 刷新逻辑 ---
|
||||
const handleRefresh = async (confirm = false) => {
|
||||
if (isDirty.value && confirm) {
|
||||
try {
|
||||
|
|
@ -43,14 +43,14 @@ const handleRefresh = async (confirm = false) => {
|
|||
}
|
||||
isLoading.value = true;
|
||||
try {
|
||||
// 1. 获取完整的配置对象
|
||||
const fullConfig = await getVideoConfig();
|
||||
|
||||
// 2. 扁平化:拆分顶层设置和流数组
|
||||
videoServiceEnabled.value = fullConfig.video_service?.enabled ?? false;
|
||||
originalStreams.value = fullConfig.video_streams || [];
|
||||
// [!! 修复 1 !!] 同时设置当前值和原始值
|
||||
const enabledState = fullConfig.video_service?.enabled ?? false;
|
||||
videoServiceEnabled.value = enabledState;
|
||||
originalVideoServiceEnabled.value = enabledState; // 存储原始状态
|
||||
|
||||
// 深度复制用于编辑
|
||||
originalStreams.value = fullConfig.video_streams || [];
|
||||
editableStreams.value = JSON.parse(JSON.stringify(originalStreams.value));
|
||||
|
||||
ElMessage.success("视频配置已刷新");
|
||||
|
|
@ -65,25 +65,26 @@ onMounted(() => {
|
|||
handleRefresh(false);
|
||||
});
|
||||
|
||||
// --- [修改] 脏检查逻辑 ---
|
||||
// --- [!! 修复 1 !!] 脏检查逻辑 ---
|
||||
const isDirty = computed(() => {
|
||||
// 必须同时检查顶层开关和流数组
|
||||
try {
|
||||
// 原始配置使用 *original* 状态
|
||||
const originalConfig = {
|
||||
video_service: { enabled: videoServiceEnabled.value }, // 使用加载时的值
|
||||
video_service: { enabled: originalVideoServiceEnabled.value }, // <-- 使用原始值
|
||||
video_streams: originalStreams.value,
|
||||
};
|
||||
// 当前配置使用 *v-model* 绑定的当前值
|
||||
const currentConfig = {
|
||||
video_service: { enabled: videoServiceEnabled.value }, // 使用当前的值
|
||||
video_service: { enabled: videoServiceEnabled.value },
|
||||
video_streams: editableStreams.value,
|
||||
};
|
||||
return JSON.stringify(originalConfig) !== JSON.stringify(currentConfig);
|
||||
} catch {
|
||||
return true;
|
||||
return true; // 发生错误时,假定为 dirty 以防万一
|
||||
}
|
||||
});
|
||||
|
||||
// --- [修改] 弹窗逻辑 (适配 VideoStreamConfig) ---
|
||||
// --- 弹窗逻辑 (不变) ---
|
||||
const openAddDialog = () => {
|
||||
dialogMode.value = "add";
|
||||
currentEditingStream.value = {
|
||||
|
|
@ -106,7 +107,6 @@ const openAddDialog = () => {
|
|||
const openEditDialog = (row: VideoStreamConfig, index: number) => {
|
||||
dialogMode.value = "edit";
|
||||
currentEditingIndex.value = index;
|
||||
// 关键:module_config 必须是字符串才能在 textarea 中编辑
|
||||
currentEditingStream.value = {
|
||||
...JSON.parse(JSON.stringify(row)),
|
||||
module_config: JSON.stringify(row.module_config, null, 2),
|
||||
|
|
@ -115,13 +115,12 @@ const openEditDialog = (row: VideoStreamConfig, index: number) => {
|
|||
nextTick(() => formRef.value?.clearValidate());
|
||||
};
|
||||
|
||||
// --- [修改] 弹窗确认 (适配 VideoStreamConfig) ---
|
||||
// --- 弹窗确认 (不变) ---
|
||||
const handleDialogConfirm = async () => {
|
||||
if (!formRef.value) return;
|
||||
await formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
// 关键:将 module_config 字符串转换回 JSON 对象
|
||||
const streamToSave = {
|
||||
...currentEditingStream.value,
|
||||
module_config: JSON.parse(
|
||||
|
|
@ -146,7 +145,7 @@ const handleDialogConfirm = async () => {
|
|||
});
|
||||
};
|
||||
|
||||
// --- [修改] 删除逻辑 (适配 editableStreams) ---
|
||||
// --- 删除逻辑 (不变) ---
|
||||
const deleteRule = (index: number) => {
|
||||
ElMessageBox.confirm(
|
||||
`您确定要删除流 '${editableStreams.value[index].id}' 吗?`,
|
||||
|
|
@ -159,11 +158,10 @@ const deleteRule = (index: number) => {
|
|||
.catch(() => {});
|
||||
};
|
||||
|
||||
// --- [修改] 保存并应用 (适配 video API) ---
|
||||
// --- 保存并应用 (不变) ---
|
||||
const handleSaveAndApply = async () => {
|
||||
isSaving.value = true;
|
||||
try {
|
||||
// 1. 合并:将顶层设置和流数组重新组装
|
||||
const fullConfigToSave: VideoConfig = {
|
||||
video_service: {
|
||||
enabled: videoServiceEnabled.value,
|
||||
|
|
@ -171,16 +169,15 @@ const handleSaveAndApply = async () => {
|
|||
video_streams: editableStreams.value,
|
||||
};
|
||||
|
||||
// 2. 保存配置
|
||||
await postVideoConfig(fullConfigToSave);
|
||||
ElMessage.success("配置保存成功!");
|
||||
|
||||
// 3. 应用配置 (后端重启)
|
||||
await postServiceReload();
|
||||
ElMessage.info("服务重启命令已发送,请稍候...");
|
||||
|
||||
// 4. 更新本地 "original" 状态
|
||||
// [!! 修复 1 !!] 保存成功后,更新 *所有* 原始状态
|
||||
originalStreams.value = JSON.parse(JSON.stringify(editableStreams.value));
|
||||
originalVideoServiceEnabled.value = videoServiceEnabled.value;
|
||||
} catch (error: any) {
|
||||
ElMessage.error(`操作失败: ${error.message}`);
|
||||
} finally {
|
||||
|
|
@ -188,18 +185,7 @@ const handleSaveAndApply = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
// --- [修改] 仅应用 (适配 video API) ---
|
||||
const handleApplyOnly = async () => {
|
||||
isReloading.value = true;
|
||||
try {
|
||||
await postServiceReload();
|
||||
ElMessage.success("服务重启命令已发送");
|
||||
} catch (error: any) {
|
||||
ElMessage.error(`应用失败: ${error.message}`);
|
||||
} finally {
|
||||
isReloading.value = false;
|
||||
}
|
||||
};
|
||||
// [!! 移除 2 !!] 移除了 handleApplyOnly 函数
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -228,13 +214,6 @@ const handleApplyOnly = async () => {
|
|||
>
|
||||
刷新 (撤销更改)
|
||||
</el-button>
|
||||
<el-button
|
||||
@click="handleApplyOnly"
|
||||
:loading="isReloading"
|
||||
type="warning"
|
||||
>
|
||||
仅应用 (重启服务)
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSaveAndApply"
|
||||
|
|
@ -387,7 +366,7 @@ const handleApplyOnly = async () => {
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 仿照 AlarmManager.vue 的 Flex 布局 */
|
||||
/* 样式 (不变) */
|
||||
.config-manager-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
@ -395,7 +374,6 @@ const handleApplyOnly = async () => {
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -412,16 +390,14 @@ h1 {
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
/* [新增] 顶部卡片样式 */
|
||||
.top-config-card {
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0; /* 不压缩 */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 表格容器样式 (与 AlarmManager.vue 相同) */
|
||||
.table-container {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
Loading…
Reference in New Issue