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