From c493779a8504fe1eb548c865ff268a7f7436ec01 Mon Sep 17 00:00:00 2001
From: wanshenmean <cathay_xy@163.com>
Date: 星期四, 19 三月 2026 11:43:36 +0800
Subject: [PATCH] feat: 集成机械手客户端并重构模拟器前端工作台

---
 Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/DetailsView.vue |  297 +++++++++++++++++++++++++++++++++++++----------------------
 1 files changed, 186 insertions(+), 111 deletions(-)

diff --git a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/DetailsView.vue b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/DetailsView.vue
index a472d99..83ea82c 100644
--- a/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/DetailsView.vue
+++ b/Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/DetailsView.vue
@@ -1,5 +1,9 @@
 锘�<template>
-  <div>
+  <div
+    class="admin-page"
+    v-loading.fullscreen.lock="startActionLoading"
+    element-loading-text="姝e湪鍚姩瀹炰緥锛岃绋嶅��..."
+  >
     <div v-if="loading" class="loading-container">
       <el-icon class="loading-icon" :size="40"><Loading /></el-icon>
       <p>鍔犺浇涓�...</p>
@@ -28,44 +32,64 @@
         </el-button>
       </div>
 
-      <el-row :gutter="20" class="status-cards">
-        <el-col :xs="12" :sm="6">
-          <el-card shadow="hover" class="status-card">
-            <el-statistic title="鐘舵��">
-              <template #default>
-                <el-tag :type="getStatusTagType(instance.status)" size="large">
-                  {{ getStatusText(instance.status) }}
-                </el-tag>
-              </template>
-            </el-statistic>
-          </el-card>
-        </el-col>
-        <el-col :xs="12" :sm="6">
-          <el-card shadow="hover" class="status-card">
-            <el-statistic title="杩炴帴瀹㈡埛绔�" :value="instance.clientCount">
-              <template #suffix>
-                <el-icon><User /></el-icon>
-              </template>
-            </el-statistic>
-          </el-card>
-        </el-col>
-        <el-col :xs="12" :sm="6">
-          <el-card shadow="hover" class="status-card">
-            <el-statistic title="鎬昏姹傛暟" :value="instance.totalRequests" />
-          </el-card>
-        </el-col>
-        <el-col :xs="12" :sm="6">
-          <el-card shadow="hover" class="status-card">
-            <el-statistic title="绔彛" :value="instance.port" />
-          </el-card>
-        </el-col>
-      </el-row>
+      <section class="section-block">
+        <div class="section-head">
+          <div>
+            <h3 class="section-title">淇℃伅鍖�</h3>
+            <p class="section-desc">瀹炰緥杩愯鐘舵�佷笌杩炴帴姒傝</p>
+          </div>
+        </div>
+        <div class="section-body">
+          <el-row :gutter="12" class="status-cards">
+            <el-col :xs="12" :sm="6">
+              <el-card shadow="hover" class="status-card panel-card">
+                <el-statistic title="鐘舵��">
+                  <template #default>
+                    <el-tag :type="getStatusTagType(instance.status)" size="large">
+                      {{ getStatusText(instance.status) }}
+                    </el-tag>
+                  </template>
+                </el-statistic>
+              </el-card>
+            </el-col>
+            <el-col :xs="12" :sm="6">
+              <el-card shadow="hover" class="status-card panel-card">
+                <el-statistic title="杩炴帴瀹㈡埛绔�" :value="instance.clientCount">
+                  <template #suffix>
+                    <el-icon><User /></el-icon>
+                  </template>
+                </el-statistic>
+              </el-card>
+            </el-col>
+            <el-col :xs="12" :sm="6">
+              <el-card shadow="hover" class="status-card panel-card">
+                <el-statistic title="鎬昏姹傛暟" :value="instance.totalRequests" />
+              </el-card>
+            </el-col>
+            <el-col :xs="12" :sm="6">
+              <el-card shadow="hover" class="status-card panel-card">
+                <el-statistic title="绔彛" :value="instance.port" />
+              </el-card>
+            </el-col>
+          </el-row>
+        </div>
+      </section>
 
-      <el-card class="mt-4" shadow="never">
+      <section class="section-block detail-section">
+        <div class="section-head">
+          <div>
+            <h3 class="section-title">鎿嶄綔鍖�</h3>
+            <p class="section-desc">宸︿晶瀹炰緥淇℃伅涓庢搷浣滐紝鍙充晶瀹炴椂 DB 鏁版嵁</p>
+          </div>
+        </div>
+        <div class="section-body">
+      <el-row :gutter="16" class="detail-main">
+        <el-col :xs="24" :lg="8">
+      <el-card class="panel-card" shadow="never">
         <template #header>
           <span class="card-header-title">鍩烘湰淇℃伅</span>
         </template>
-        <el-descriptions :column="2" border>
+        <el-descriptions :column="1" border>
           <el-descriptions-item label="瀹炰緥ID">{{ instance.instanceId }}</el-descriptions-item>
           <el-descriptions-item label="瀹炰緥鍚嶇О">{{ instance.name }}</el-descriptions-item>
           <el-descriptions-item label="PLC鍨嬪彿">{{ getPlcTypeText(instance.plcType) }}</el-descriptions-item>
@@ -76,17 +100,18 @@
           <el-descriptions-item v-if="instance.lastActivityTime" label="鏈�鍚庢椿鍔ㄦ椂闂�">
             {{ formatDate(instance.lastActivityTime) }}
           </el-descriptions-item>
-          <el-descriptions-item v-if="instance.errorMessage" label="閿欒淇℃伅" :span="2">
+          <el-descriptions-item v-if="instance.errorMessage" label="閿欒淇℃伅">
             <el-text type="danger">{{ instance.errorMessage }}</el-text>
           </el-descriptions-item>
         </el-descriptions>
       </el-card>
 
-      <el-card class="mt-4" shadow="never">
+      <el-card class="panel-card left-actions" shadow="never">
         <div class="action-buttons">
           <el-button
             v-if="instance.status === 'Stopped' || instance.status === 'Error'"
             type="success"
+           
             @click="handleStart"
           >
             <el-icon><VideoPlay /></el-icon>
@@ -95,6 +120,7 @@
           <el-button
             v-if="instance.status === 'Running'"
             type="warning"
+           
             @click="handleStop"
           >
             <el-icon><VideoPause /></el-icon>
@@ -111,13 +137,15 @@
         </div>
       </el-card>
 
-      <el-card class="mt-4" shadow="never">
+        </el-col>
+        <el-col :xs="24" :lg="16">
+      <el-card class="panel-card" shadow="never">
         <template #header>
           <div class="db-header">
             <span class="card-header-title">DB鍧楀疄鏃舵暟鎹�</span>
             <div class="db-toolbar">
               <el-switch v-model="autoRefreshDb" active-text="鑷姩鍒锋柊" />
-              <el-button size="small" @click="loadMemoryData(true)">鎵嬪姩鍒锋柊</el-button>
+              <el-button @click="loadMemoryData(true)">鎵嬪姩鍒锋柊</el-button>
             </div>
           </div>
         </template>
@@ -128,7 +156,7 @@
             <template #default>
               <div v-if="deviceDbViews.length === 0" class="text-muted">褰撳墠璁惧妯℃澘鏈尮閰嶅埌鍙樉绀虹殑DB鍧�</div>
               <div v-else>
-                <el-tabs type="border-card" class="db-tabs">
+                    <el-tabs type="border-card" class="db-tabs">
                   <el-tab-pane
                     v-for="view in deviceDbViews"
                     :key="view.templateDbNumber"
@@ -146,46 +174,52 @@
                         :key="`${view.templateDbNumber}-${group.key}`"
                       >
                         <template #label>
-                          <el-tag :type="getFieldGroupTagType(group.key)" size="small">{{ group.key }}</el-tag>
+                          <el-tag :type="getFieldGroupTagType(group.key)">{{ group.key }}</el-tag>
                         </template>
+                        <div class="field-table-wrap">
                         <el-table
                           :data="group.fields"
                           border
-                          size="small"
+                          class="field-table"
+                          table-layout="auto"
                           empty-text="褰撳墠鍒嗙粍鏃犲瓧娈垫槧灏�"
                         >
-                          <el-table-column prop="fieldKey" label="瀛楁" min-width="140" />
-                          <el-table-column prop="address" label="鍦板潃" width="130" />
-                          <el-table-column prop="mappedDb" label="鏄犲皠鍧�" width="120">
+                          <el-table-column prop="fieldKey" label="瀛楁" />
+                          <el-table-column prop="address" label="鍦板潃" />
+                          <el-table-column prop="mappedDb" label="鏄犲皠鍧�">
                             <template #default="{ row }">
-                              <el-tag :type="getDbTagType(row.mappedDb)" size="small">{{ row.mappedDb }}</el-tag>
+                              <el-tag :type="getDbTagType(row.mappedDb)">{{ row.mappedDb }}</el-tag>
                             </template>
                           </el-table-column>
-                          <el-table-column prop="dataType" label="绫诲瀷" width="90" />
-                          <el-table-column prop="direction" label="鏂瑰悜" width="130" />
-                          <el-table-column prop="value" label="褰撳墠鍊�" min-width="220" />
-                          <el-table-column label="淇敼鍊�" min-width="220">
+                          <el-table-column prop="dataType" label="绫诲瀷" />
+                          <el-table-column prop="direction" label="鏂瑰悜" />
+                          <el-table-column prop="value" label="褰撳墠鍊�" />
+                          <el-table-column label="淇敼鍊�">
                             <template #default="{ row }">
                               <el-switch
                                 v-if="row.dataType === 'Bool'"
                                 v-model="fieldEditValues[getFieldEditKey(row)]"
+                                @change="markFieldDirty(row)"
                                 :disabled="!isFieldWritable(row)"
                               />
                               <el-input-number
                                 v-else-if="row.dataType === 'Int' || row.dataType === 'DInt' || row.dataType === 'Byte'"
                                 v-model="fieldEditValues[getFieldEditKey(row)]"
+                                @change="markFieldDirty(row)"
                                 :disabled="!isFieldWritable(row)"
                                 :controls="false"
-                                style="width: 100%"
+                                class="editable-control"
                               />
                               <el-input
                                 v-else
                                 v-model="fieldEditValues[getFieldEditKey(row)]"
+                                @input="markFieldDirty(row)"
                                 :disabled="!isFieldWritable(row)"
+                                class="editable-control"
                               />
                             </template>
                           </el-table-column>
-                          <el-table-column label="鎿嶄綔" width="90" fixed="right">
+                          <el-table-column label="鎿嶄綔" width="88" fixed="right">
                             <template #default="{ row }">
                               <el-button
                                 type="primary"
@@ -199,47 +233,54 @@
                             </template>
                           </el-table-column>
                         </el-table>
+                        </div>
                       </el-tab-pane>
                     </el-tabs>
+                    <template v-else-if="view.fields.length > 0">
+                    <div class="field-table-wrap">
                     <el-table
-                      v-else-if="view.fields.length > 0"
                       :data="view.fields"
                       border
-                      size="small"
+                      class="field-table"
+                      table-layout="auto"
                       empty-text="褰撳墠DB鍧楁棤瀛楁鏄犲皠"
                     >
-                      <el-table-column prop="fieldKey" label="瀛楁" min-width="140" />
-                      <el-table-column prop="address" label="鍦板潃" width="130" />
-                      <el-table-column prop="mappedDb" label="鏄犲皠鍧�" width="120">
+                      <el-table-column prop="fieldKey" label="瀛楁" />
+                      <el-table-column prop="address" label="鍦板潃" />
+                      <el-table-column prop="mappedDb" label="鏄犲皠鍧�">
                         <template #default="{ row }">
-                          <el-tag :type="getDbTagType(row.mappedDb)" size="small">{{ row.mappedDb }}</el-tag>
+                          <el-tag :type="getDbTagType(row.mappedDb)">{{ row.mappedDb }}</el-tag>
                         </template>
                       </el-table-column>
-                      <el-table-column prop="dataType" label="绫诲瀷" width="90" />
-                      <el-table-column prop="direction" label="鏂瑰悜" width="130" />
-                      <el-table-column prop="value" label="褰撳墠鍊�" min-width="220" />
-                      <el-table-column label="淇敼鍊�" min-width="220">
+                      <el-table-column prop="dataType" label="绫诲瀷" />
+                      <el-table-column prop="direction" label="鏂瑰悜" />
+                      <el-table-column prop="value" label="褰撳墠鍊�" />
+                      <el-table-column label="淇敼鍊�">
                         <template #default="{ row }">
                           <el-switch
                             v-if="row.dataType === 'Bool'"
                             v-model="fieldEditValues[getFieldEditKey(row)]"
+                            @change="markFieldDirty(row)"
                             :disabled="!isFieldWritable(row)"
                           />
                           <el-input-number
                             v-else-if="row.dataType === 'Int' || row.dataType === 'DInt' || row.dataType === 'Byte'"
                             v-model="fieldEditValues[getFieldEditKey(row)]"
+                            @change="markFieldDirty(row)"
                             :disabled="!isFieldWritable(row)"
                             :controls="false"
-                            style="width: 100%"
+                            class="editable-control"
                           />
                           <el-input
                             v-else
                             v-model="fieldEditValues[getFieldEditKey(row)]"
+                            @input="markFieldDirty(row)"
                             :disabled="!isFieldWritable(row)"
+                            class="editable-control"
                           />
                         </template>
                       </el-table-column>
-                      <el-table-column label="鎿嶄綔" width="90" fixed="right">
+                      <el-table-column label="鎿嶄綔" width="88" fixed="right">
                         <template #default="{ row }">
                           <el-button
                             type="primary"
@@ -253,6 +294,8 @@
                         </template>
                       </el-table-column>
                     </el-table>
+                    </div>
+                    </template>
                     <div v-else class="text-muted">褰撳墠DB鍧楁棤瀛楁鏄犲皠</div>
 
                     <div class="card-header-title field-title">鍘熷鏁版嵁</div>
@@ -291,6 +334,10 @@
           />
         </div>
       </el-card>
+        </el-col>
+      </el-row>
+        </div>
+      </section>
     </div>
   </div>
 </template>
@@ -311,6 +358,7 @@
 const protocolTemplate = ref<ProtocolTemplate | null>(null)
 const loading = ref(true)
 const errorMsg = ref('')
+const startActionLoading = ref(false)
 
 const memoryLoading = ref(false)
 const autoRefreshDb = ref(true)
@@ -322,6 +370,7 @@
 const expandedDbViews = ref<Record<number, boolean>>({})
 const fieldEditValues = ref<Record<string, string | number | boolean>>({})
 const writingFieldKeys = ref<Record<string, boolean>>({})
+const dirtyFieldKeys = ref<Record<string, boolean>>({})
 
 let refreshTimer: number | null = null
 
@@ -413,11 +462,29 @@
 })
 
 watch(parsedFields, () => {
-  const next: Record<string, string | number | boolean> = {}
+  // 杞鍒锋柊鏃讹紝淇濈暀鐢ㄦ埛姝e湪缂栬緫鐨勫瓧娈碉紝閬垮厤杈撳叆鍊艰瑕嗙洊銆�
+  const next: Record<string, string | number | boolean> = { ...fieldEditValues.value }
+  const validKeys = new Set<string>()
   for (const field of parsedFields.value) {
     const key = buildFieldEditKey(field.templateDbNumber, field.fieldKey)
+    validKeys.add(key)
+    if (dirtyFieldKeys.value[key] === true) {
+      continue
+    }
     next[key] = coerceEditValueByType(field.dataType, field.value)
   }
+
+  for (const key of Object.keys(next)) {
+    if (!validKeys.has(key)) {
+      delete next[key]
+    }
+  }
+  for (const key of Object.keys(dirtyFieldKeys.value)) {
+    if (!validKeys.has(key)) {
+      delete dirtyFieldKeys.value[key]
+    }
+  }
+
   fieldEditValues.value = next
 }, { immediate: true })
 
@@ -530,6 +597,8 @@
       cancelButtonText: '鍙栨秷',
       type: 'info'
     })
+
+    startActionLoading.value = true
     await api.startInstance(id)
     await loadInstance()
     await loadMemoryData(true)
@@ -539,6 +608,8 @@
       console.error('鍚姩瀹炰緥澶辫触:', err)
       ElMessage.error('鍚姩澶辫触锛岃鏌ョ湅鎺у埗鍙�')
     }
+  } finally {
+    startActionLoading.value = false
   }
 }
 
@@ -662,6 +733,14 @@
   return buildFieldEditKey(row.templateDbNumber, row.fieldKey)
 }
 
+function markFieldDirty(row: { templateDbNumber: number; fieldKey: string }): void {
+  dirtyFieldKeys.value[getFieldEditKey(row)] = true
+}
+
+function clearFieldDirty(row: { templateDbNumber: number; fieldKey: string }): void {
+  delete dirtyFieldKeys.value[getFieldEditKey(row)]
+}
+
 function isFieldWritable(row: { resolvedDbNumber: number | null }): boolean {
   return row.resolvedDbNumber !== null
 }
@@ -727,6 +806,7 @@
     const dbBlockCount = instanceConfig.value?.memoryConfig.dbBlockCount || dbBlockNumbers.length || 1
     const dbBlockSize = instanceConfig.value?.memoryConfig.dbBlockSize || nextBytes.length
     dbBlocks.value = splitDbBlocks(nextBytes, dbBlockCount, dbBlockSize, dbBlockNumbers)
+    clearFieldDirty(row)
     ElMessage.success(`瀛楁 ${row.fieldKey} 鍐欏叆鎴愬姛`)
   } finally {
     writingFieldKeys.value[editKey] = false
@@ -1023,48 +1103,8 @@
 </script>
 
 <style scoped>
-.page-header {
-  display: flex;
-  justify-content: space-between;
-  align-items: flex-start;
-  margin-bottom: 20px;
-  flex-wrap: wrap;
-  gap: 16px;
-}
-
-.header-left h2 {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  margin: 0 0 8px 0;
-}
-
-.text-muted {
-  color: #909399;
-  margin: 0;
-}
-
-.loading-container {
-  text-align: center;
-  padding: 60px 0;
-  color: #909399;
-}
-
-.loading-icon {
-  animation: spin 1s linear infinite;
-}
-
-@keyframes spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}
-
 .status-cards {
-  margin-bottom: 20px;
+  margin-bottom: 8px;
 }
 
 .status-card {
@@ -1077,13 +1117,17 @@
 }
 
 .mt-4 {
-  margin-top: 16px;
+  margin-top: 14px;
 }
 
 .action-buttons {
   display: flex;
-  gap: 12px;
+  gap: 10px;
   flex-wrap: wrap;
+}
+
+.left-actions {
+  margin-top: 14px;
 }
 
 .db-header {
@@ -1100,7 +1144,7 @@
 
 .db-content {
   margin: 0;
-  max-height: 180px;
+  max-height: 168px;
   overflow: auto;
   white-space: pre-wrap;
   font-family: Consolas, Monaco, 'Courier New', monospace;
@@ -1126,11 +1170,11 @@
 }
 
 .db-tabs {
-  margin-top: 8px;
+  margin-top: 10px;
 }
 
 .data-view-tabs {
-  margin-top: 8px;
+  margin-top: 10px;
 }
 
 .db-raw-toolbar {
@@ -1147,4 +1191,35 @@
   margin-top: 12px;
   margin-bottom: 8px;
 }
+
+.field-table-wrap {
+  width: 100%;
+  overflow-x: auto;
+}
+
+:deep(.field-table) {
+  width: max-content;
+  min-width: 100%;
+}
+
+:deep(.editable-control) {
+  width: 100%;
+}
+
+:deep(.status-card .el-card__body) {
+  padding: 14px 16px;
+}
+
+:deep(.action-buttons .el-button) {
+  min-width: 82px;
+}
+
+:deep(.panel-card > .el-card__header) {
+  padding: 14px 18px;
+}
+
+:deep(.panel-card > .el-card__body) {
+  padding: 14px 18px;
+}
 </style>
+

--
Gitblit v1.9.3