wanshenmean
2026-03-19 c493779a8504fe1eb548c865ff268a7f7436ec01
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="正在启动实例,请稍候..."
  >
    <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> = {}
  // 轮询刷新时,保留用户正在编辑的字段,避免输入值被覆盖。
  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>