From 32c3d30844c91ac74f8f050c3d10afbbf47b4485 Mon Sep 17 00:00:00 2001 From: guanyuankai Date: Wed, 5 Nov 2025 15:08:42 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=BC=80=E5=8F=91=EF=BC=8C?= =?UTF-8?q?=E5=90=8E=E7=BB=AD=E8=BF=9B=E8=A1=8C=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/config.ts | 76 ++++++++ src/components/HelloWorld.vue | 38 ---- src/components/Sidebar.vue | 9 +- src/router/index.ts | 6 + src/views/MainConfigManager.vue | 303 +++++++++++++++++++++++++++++++ src/views/VideoConfigManager.vue | 78 +++----- 6 files changed, 417 insertions(+), 93 deletions(-) create mode 100644 src/api/config.ts delete mode 100644 src/components/HelloWorld.vue create mode 100644 src/views/MainConfigManager.vue diff --git a/src/api/config.ts b/src/api/config.ts new file mode 100644 index 0000000..a9aaf06 --- /dev/null +++ b/src/api/config.ts @@ -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 => { + try { + const response = await apiClient.get("/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 => { + 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"); + } +}; \ No newline at end of file diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue deleted file mode 100644 index 7b25f3f..0000000 --- a/src/components/HelloWorld.vue +++ /dev/null @@ -1,38 +0,0 @@ - - - - - diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue index 129f1ce..897d517 100644 --- a/src/components/Sidebar.vue +++ b/src/components/Sidebar.vue @@ -64,11 +64,12 @@ import { 视频参数配置 -
  • - + +
  • + - 其他配置 - + 主参数配置 +
  • diff --git a/src/router/index.ts b/src/router/index.ts index 955a700..c5d83d1 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -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({ diff --git a/src/views/MainConfigManager.vue b/src/views/MainConfigManager.vue new file mode 100644 index 0000000..ef1b787 --- /dev/null +++ b/src/views/MainConfigManager.vue @@ -0,0 +1,303 @@ + + + + + diff --git a/src/views/VideoConfigManager.vue b/src/views/VideoConfigManager.vue index 4173d78..29b0858 100644 --- a/src/views/VideoConfigManager.vue +++ b/src/views/VideoConfigManager.vue @@ -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([]); -// 用于表格编辑的 video_streams 数组 const editableStreams = ref([]); 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>({}); const currentEditingIndex = ref(-1); const formRef = ref(); -// --- [修改] 刷新逻辑 --- +// --- 刷新逻辑 --- 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 函数 + \ No newline at end of file