|
|
@@ -30,6 +30,7 @@
|
|
|
controls-position="right"
|
|
|
size="default"
|
|
|
placeholder="请输入"
|
|
|
+ class="param-input"
|
|
|
/>
|
|
|
<el-input
|
|
|
v-else
|
|
|
@@ -37,17 +38,32 @@
|
|
|
:disabled="param.readonly"
|
|
|
size="default"
|
|
|
placeholder="请输入"
|
|
|
+ class="param-input"
|
|
|
/>
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ size="default"
|
|
|
+ :loading="savingIndex === index"
|
|
|
+ :disabled="param.readonly"
|
|
|
+ @click="saveParam(param, index)"
|
|
|
+ class="param-save-btn"
|
|
|
+ >
|
|
|
+ <el-icon v-if="savingIndex !== index"><Check /></el-icon>
|
|
|
+ 保存
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-if="param.saveStatus"
|
|
|
+ class="param-status"
|
|
|
+ :class="`status-${param.saveStatus}`"
|
|
|
+ >
|
|
|
+ <el-icon>
|
|
|
+ <component :is="param.saveStatus === 'success' ? CircleCheck : CircleClose" />
|
|
|
+ </el-icon>
|
|
|
+ <span>{{ param.saveStatus === 'success' ? '已保存' : '保存失败' }}</span>
|
|
|
</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>
|
|
|
@@ -56,7 +72,7 @@
|
|
|
<script setup lang="ts">
|
|
|
import { ref, onMounted } from 'vue';
|
|
|
import { ElMessage } from 'element-plus';
|
|
|
-import { Refresh, Check, FolderOpened } from '@element-plus/icons-vue';
|
|
|
+import { Refresh, Check, FolderOpened, CircleCheck, CircleClose } from '@element-plus/icons-vue';
|
|
|
import socket from "@/stores/modules/socket";
|
|
|
import client from "@/stores/modules/client";
|
|
|
import icpList from '@/utils/ipc';
|
|
|
@@ -69,7 +85,10 @@ const dynamicParams = ref<any[]>([]);
|
|
|
|
|
|
// 加载状态
|
|
|
const loading = ref(false);
|
|
|
-const saving = ref(false);
|
|
|
+const savingIndex = ref<number>(-1);
|
|
|
+
|
|
|
+// 状态自动清除定时器
|
|
|
+const statusTimers = new Map<number, number>();
|
|
|
|
|
|
// 获取动态参数配置
|
|
|
async function fetchDynamicConfig() {
|
|
|
@@ -87,7 +106,8 @@ async function fetchDynamicConfig() {
|
|
|
readonly: result.data[key].readonly,
|
|
|
type: result.data[key].type,
|
|
|
precision: result.data[key].precision,
|
|
|
- value: result.data[key].value
|
|
|
+ value: result.data[key].value,
|
|
|
+ saveStatus: ''
|
|
|
}));
|
|
|
} else if (result.msg) {
|
|
|
ElMessage.error(result.msg);
|
|
|
@@ -105,71 +125,76 @@ async function fetchDynamicConfig() {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-// 保存动态参数
|
|
|
-async function saveDynamicParams() {
|
|
|
- saving.value = true;
|
|
|
-
|
|
|
- const paramsToSave = dynamicParams.value.filter(p => !p.readonly);
|
|
|
-
|
|
|
- if (paramsToSave.length === 0) {
|
|
|
- ElMessage.warning('没有可保存的参数');
|
|
|
- saving.value = false;
|
|
|
+// 保存单个参数
|
|
|
+function saveParam(param: any, index: number) {
|
|
|
+ if (param.readonly || savingIndex.value !== -1) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- let successCount = 0;
|
|
|
- let failCount = 0;
|
|
|
-
|
|
|
- for (const param of paramsToSave) {
|
|
|
- const success = await saveSingleParam(param);
|
|
|
- if (success) {
|
|
|
- successCount++;
|
|
|
- } else {
|
|
|
- failCount++;
|
|
|
+ savingIndex.value = index;
|
|
|
+ param.saveStatus = '';
|
|
|
+
|
|
|
+ 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)
|
|
|
}
|
|
|
- }
|
|
|
+ });
|
|
|
|
|
|
- if (failCount === 0) {
|
|
|
- ElMessage.success(`保存成功 (${successCount}项)`);
|
|
|
- } else {
|
|
|
- ElMessage.warning(`部分保存成功 (成功${successCount}项, 失败${failCount}项)`);
|
|
|
- }
|
|
|
+ const handler = (event: any, result: any) => {
|
|
|
+ clientStore.ipc.removeAllListeners(icpList.socket.message + '_set_dynamic_config');
|
|
|
+ savingIndex.value = -1;
|
|
|
|
|
|
- saving.value = false;
|
|
|
-}
|
|
|
+ const name = param.tips || `参数${param.addr}`;
|
|
|
+ if (result.code === 0) {
|
|
|
+ param.saveStatus = 'success';
|
|
|
+ ElMessage.success(`${name} 保存成功`);
|
|
|
+ } else {
|
|
|
+ param.saveStatus = 'fail';
|
|
|
+ ElMessage.error(result.msg || `${name} 保存失败`);
|
|
|
+ }
|
|
|
|
|
|
-// 保存单个参数
|
|
|
-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)
|
|
|
- }
|
|
|
- });
|
|
|
+ scheduleClearStatus(index);
|
|
|
+ };
|
|
|
|
|
|
- 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);
|
|
|
|
|
|
- clientStore.ipc.once(icpList.socket.message + '_set_dynamic_config', handler);
|
|
|
+ setTimeout(() => {
|
|
|
+ clientStore.ipc.removeAllListeners(icpList.socket.message + '_set_dynamic_config');
|
|
|
+ if (savingIndex.value === index) {
|
|
|
+ savingIndex.value = -1;
|
|
|
+ param.saveStatus = 'fail';
|
|
|
+ ElMessage.error('保存超时');
|
|
|
+ scheduleClearStatus(index);
|
|
|
+ }
|
|
|
+ }, 5000);
|
|
|
+}
|
|
|
|
|
|
- setTimeout(() => {
|
|
|
- clientStore.ipc.removeAllListeners(icpList.socket.message + '_set_dynamic_config');
|
|
|
- resolve(false);
|
|
|
- }, 5000);
|
|
|
- });
|
|
|
+// 延时清除保存状态
|
|
|
+function scheduleClearStatus(index: number) {
|
|
|
+ const oldTimer = statusTimers.get(index);
|
|
|
+ if (oldTimer) {
|
|
|
+ clearTimeout(oldTimer);
|
|
|
+ }
|
|
|
+ const timer = window.setTimeout(() => {
|
|
|
+ if (dynamicParams.value[index]) {
|
|
|
+ dynamicParams.value[index].saveStatus = '';
|
|
|
+ }
|
|
|
+ statusTimers.delete(index);
|
|
|
+ }, 3000);
|
|
|
+ statusTimers.set(index, timer);
|
|
|
}
|
|
|
|
|
|
// 刷新数据
|
|
|
async function refreshAll() {
|
|
|
loading.value = true;
|
|
|
+ statusTimers.forEach(timer => clearTimeout(timer));
|
|
|
+ statusTimers.clear();
|
|
|
await fetchDynamicConfig();
|
|
|
loading.value = false;
|
|
|
}
|
|
|
@@ -227,27 +252,9 @@ $text-secondary: #909399;
|
|
|
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));
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
|
|
gap: 16px;
|
|
|
}
|
|
|
|
|
|
@@ -277,6 +284,20 @@ $text-secondary: #909399;
|
|
|
}
|
|
|
|
|
|
.param-body {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+
|
|
|
+ .param-input {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .param-save-btn {
|
|
|
+ flex-shrink: 0;
|
|
|
+ min-width: 80px;
|
|
|
+ }
|
|
|
+
|
|
|
:deep(.el-input-number) {
|
|
|
width: 100% !important;
|
|
|
|
|
|
@@ -293,6 +314,26 @@ $text-secondary: #909399;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ .param-status {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ margin-top: 10px;
|
|
|
+ font-size: 12px;
|
|
|
+
|
|
|
+ &.status-success {
|
|
|
+ color: #67c23a;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.status-fail {
|
|
|
+ color: #f56c6c;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-icon {
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
.empty-tip {
|
|
|
@@ -310,4 +351,4 @@ $text-secondary: #909399;
|
|
|
color: #dcdfe6;
|
|
|
}
|
|
|
}
|
|
|
-</style>
|
|
|
+</style>
|