|
|
@@ -0,0 +1,313 @@
|
|
|
+<template>
|
|
|
+ <div class="advanced-config-wrap">
|
|
|
+ <div class="config-container">
|
|
|
+ <div class="config-header">
|
|
|
+ <span class="header-title">动态参数设置</span>
|
|
|
+ <el-button text @click="refreshAll" :loading="loading">
|
|
|
+ <el-icon><Refresh /></el-icon>
|
|
|
+ 刷新
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="content-area" v-loading="loading">
|
|
|
+ <div v-if="dynamicParams.length === 0 && !loading" class="empty-tip">
|
|
|
+ <el-icon class="empty-icon"><FolderOpened /></el-icon>
|
|
|
+ <span>暂无可配置的动态参数</span>
|
|
|
+ </div>
|
|
|
+ <div class="param-grid" v-else>
|
|
|
+ <div v-for="(param, index) in dynamicParams" :key="index" class="param-card">
|
|
|
+ <div class="param-header">
|
|
|
+ <span class="param-name">{{ param.tips || `参数${param.addr}` }}</span>
|
|
|
+ <el-tag v-if="param.readonly" type="info" size="small" effect="plain">只读</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="param-body">
|
|
|
+ <el-input-number
|
|
|
+ v-if="param.type === 'float'"
|
|
|
+ v-model="param.value"
|
|
|
+ :precision="param.precision || 1"
|
|
|
+ :step="Math.pow(0.1, param.precision || 1)"
|
|
|
+ :disabled="param.readonly"
|
|
|
+ controls-position="right"
|
|
|
+ size="default"
|
|
|
+ placeholder="请输入"
|
|
|
+ />
|
|
|
+ <el-input
|
|
|
+ v-else
|
|
|
+ v-model="param.value"
|
|
|
+ :disabled="param.readonly"
|
|
|
+ size="default"
|
|
|
+ placeholder="请输入"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="content-footer" v-if="dynamicParams.length > 0">
|
|
|
+ <span class="param-count">共 {{ dynamicParams.length }} 个参数</span>
|
|
|
+ <el-button type="primary" @click="saveDynamicParams" :loading="saving">
|
|
|
+ <el-icon v-if="!saving"><Check /></el-icon>
|
|
|
+ 保存参数
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, onMounted } from 'vue';
|
|
|
+import { ElMessage } from 'element-plus';
|
|
|
+import { Refresh, Check, FolderOpened } from '@element-plus/icons-vue';
|
|
|
+import socket from "@/stores/modules/socket";
|
|
|
+import client from "@/stores/modules/client";
|
|
|
+import icpList from '@/utils/ipc';
|
|
|
+
|
|
|
+const socketStore = socket();
|
|
|
+const clientStore = client();
|
|
|
+
|
|
|
+// 动态参数列表
|
|
|
+const dynamicParams = ref<any[]>([]);
|
|
|
+
|
|
|
+// 加载状态
|
|
|
+const loading = ref(false);
|
|
|
+const saving = ref(false);
|
|
|
+
|
|
|
+// 获取动态参数配置
|
|
|
+async function fetchDynamicConfig() {
|
|
|
+ return new Promise<void>((resolve) => {
|
|
|
+ socketStore.sendMessage({
|
|
|
+ type: 'get_dynamic_config',
|
|
|
+ data: {}
|
|
|
+ });
|
|
|
+
|
|
|
+ const handler = (event: any, result: any) => {
|
|
|
+ if (result.code === 0 && result.data) {
|
|
|
+ dynamicParams.value = Object.keys(result.data).map(key => ({
|
|
|
+ addr: result.data[key].addr,
|
|
|
+ tips: result.data[key].tips,
|
|
|
+ readonly: result.data[key].readonly,
|
|
|
+ type: result.data[key].type,
|
|
|
+ precision: result.data[key].precision,
|
|
|
+ value: result.data[key].value
|
|
|
+ }));
|
|
|
+ } else if (result.msg) {
|
|
|
+ ElMessage.error(result.msg);
|
|
|
+ }
|
|
|
+ clientStore.ipc.removeAllListeners(icpList.socket.message + '_get_dynamic_config');
|
|
|
+ resolve();
|
|
|
+ };
|
|
|
+
|
|
|
+ clientStore.ipc.once(icpList.socket.message + '_get_dynamic_config', handler);
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ clientStore.ipc.removeAllListeners(icpList.socket.message + '_get_dynamic_config');
|
|
|
+ resolve();
|
|
|
+ }, 10000);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// 保存动态参数
|
|
|
+async function saveDynamicParams() {
|
|
|
+ saving.value = true;
|
|
|
+
|
|
|
+ const paramsToSave = dynamicParams.value.filter(p => !p.readonly);
|
|
|
+
|
|
|
+ if (paramsToSave.length === 0) {
|
|
|
+ ElMessage.warning('没有可保存的参数');
|
|
|
+ saving.value = false;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let successCount = 0;
|
|
|
+ let failCount = 0;
|
|
|
+
|
|
|
+ for (const param of paramsToSave) {
|
|
|
+ const success = await saveSingleParam(param);
|
|
|
+ if (success) {
|
|
|
+ successCount++;
|
|
|
+ } else {
|
|
|
+ failCount++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (failCount === 0) {
|
|
|
+ ElMessage.success(`保存成功 (${successCount}项)`);
|
|
|
+ } else {
|
|
|
+ ElMessage.warning(`部分保存成功 (成功${successCount}项, 失败${failCount}项)`);
|
|
|
+ }
|
|
|
+
|
|
|
+ saving.value = false;
|
|
|
+}
|
|
|
+
|
|
|
+// 保存单个参数
|
|
|
+function saveSingleParam(param: any): Promise<boolean> {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ socketStore.sendMessage({
|
|
|
+ type: 'set_dynamic_config',
|
|
|
+ data: {
|
|
|
+ addr: param.addr,
|
|
|
+ tips: param.tips,
|
|
|
+ readonly: param.readonly,
|
|
|
+ type: param.type,
|
|
|
+ precision: param.precision,
|
|
|
+ value: String(param.value)
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const handler = (event: any, result: any) => {
|
|
|
+ clientStore.ipc.removeAllListeners(icpList.socket.message + '_set_dynamic_config');
|
|
|
+ resolve(result.code === 0);
|
|
|
+ };
|
|
|
+
|
|
|
+ clientStore.ipc.once(icpList.socket.message + '_set_dynamic_config', handler);
|
|
|
+
|
|
|
+ setTimeout(() => {
|
|
|
+ clientStore.ipc.removeAllListeners(icpList.socket.message + '_set_dynamic_config');
|
|
|
+ resolve(false);
|
|
|
+ }, 5000);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// 刷新数据
|
|
|
+async function refreshAll() {
|
|
|
+ loading.value = true;
|
|
|
+ await fetchDynamicConfig();
|
|
|
+ loading.value = false;
|
|
|
+}
|
|
|
+
|
|
|
+// 组件挂载
|
|
|
+onMounted(() => {
|
|
|
+ refreshAll();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+$primary-color: #409eff;
|
|
|
+$primary-light: #ecf5ff;
|
|
|
+$bg-color: #f5f7fa;
|
|
|
+$border-color: #e4e7ed;
|
|
|
+$text-primary: #303133;
|
|
|
+$text-secondary: #909399;
|
|
|
+
|
|
|
+.advanced-config-wrap {
|
|
|
+ padding: 0;
|
|
|
+ min-height: calc(100vh - 120px);
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.config-container {
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.config-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 16px 20px;
|
|
|
+ background: #fff;
|
|
|
+ border-bottom: 1px solid $border-color;
|
|
|
+
|
|
|
+ .header-title {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: $text-primary;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-button {
|
|
|
+ color: $text-secondary;
|
|
|
+ font-size: 13px;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: $primary-color;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.content-area {
|
|
|
+ background: #fff;
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.content-footer {
|
|
|
+ margin-top: 24px;
|
|
|
+ padding-top: 20px;
|
|
|
+ border-top: 1px dashed $border-color;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+
|
|
|
+ .param-count {
|
|
|
+ font-size: 13px;
|
|
|
+ color: $text-secondary;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-button {
|
|
|
+ min-width: 120px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.param-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.param-card {
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid $border-color;
|
|
|
+ border-radius: 10px;
|
|
|
+ padding: 16px;
|
|
|
+ transition: all 0.3s;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: $primary-color;
|
|
|
+ box-shadow: 0 4px 12px rgba(64, 158, 255, 0.1);
|
|
|
+ }
|
|
|
+
|
|
|
+ .param-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 12px;
|
|
|
+
|
|
|
+ .param-name {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ color: $text-primary;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .param-body {
|
|
|
+ :deep(.el-input-number) {
|
|
|
+ width: 100% !important;
|
|
|
+
|
|
|
+ .el-input__wrapper {
|
|
|
+ border-radius: 6px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-input) {
|
|
|
+ width: 100%;
|
|
|
+
|
|
|
+ .el-input__wrapper {
|
|
|
+ border-radius: 6px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.empty-tip {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 60px 20px;
|
|
|
+ color: $text-secondary;
|
|
|
+ font-size: 14px;
|
|
|
+
|
|
|
+ .empty-icon {
|
|
|
+ font-size: 48px;
|
|
|
+ color: #dcdfe6;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|