From 737dec3c384f394fd6f9849b4480b697d1ba35d5 Mon Sep 17 00:00:00 2001
From: wanshenmean <cathay_xy@163.com>
Date: 星期二, 17 三月 2026 09:16:44 +0800
Subject: [PATCH] chore: 提交所有当前改动
---
Code/WCS/WIDESEAWCS_S7Simulator/WIDESEAWCS_S7Simulator.Web/src/views/DetailsView.vue | 900 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 files changed, 864 insertions(+), 36 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 cf643a5..a472d99 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,4 +1,4 @@
-<template>
+锘�<template>
<div>
<div v-if="loading" class="loading-container">
<el-icon class="loading-icon" :size="40"><Loading /></el-icon>
@@ -28,7 +28,6 @@
</el-button>
</div>
- <!-- 鐘舵�佸崱鐗� -->
<el-row :gutter="20" class="status-cards">
<el-col :xs="12" :sm="6">
<el-card shadow="hover" class="status-card">
@@ -62,7 +61,6 @@
</el-col>
</el-row>
- <!-- 璇︾粏淇℃伅 -->
<el-card class="mt-4" shadow="never">
<template #header>
<span class="card-header-title">鍩烘湰淇℃伅</span>
@@ -84,7 +82,6 @@
</el-descriptions>
</el-card>
- <!-- 鎿嶄綔鎸夐挳 -->
<el-card class="mt-4" shadow="never">
<div class="action-buttons">
<el-button
@@ -113,56 +110,409 @@
</el-button>
</div>
</el-card>
+
+ <el-card class="mt-4" 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>
+ </div>
+ </div>
+ </template>
+
+ <div v-if="dbBlocks.length === 0" class="text-muted">鏆傛棤DB鏁版嵁</div>
+ <div v-else>
+ <el-skeleton :loading="memoryLoading" animated>
+ <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-tab-pane
+ v-for="view in deviceDbViews"
+ :key="view.templateDbNumber"
+ :label="`DB${view.templateDbNumber}`"
+ >
+ <div class="db-block-title">
+ <span v-if="view.resolvedDbNumber">闈為浂瀛楄妭: {{ view.nonZeroCount }}</span>
+ <span v-else>鏈姞杞藉埌瀹炰緥鍐呭瓨</span>
+ </div>
+
+ <div class="card-header-title field-title">瀛楁瑙i噴</div>
+ <el-tabs v-if="view.fieldGroupEnabled && view.fieldGroups.length > 0" class="field-tabs">
+ <el-tab-pane
+ v-for="group in view.fieldGroups"
+ :key="`${view.templateDbNumber}-${group.key}`"
+ >
+ <template #label>
+ <el-tag :type="getFieldGroupTagType(group.key)" size="small">{{ group.key }}</el-tag>
+ </template>
+ <el-table
+ :data="group.fields"
+ border
+ size="small"
+ 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">
+ <template #default="{ row }">
+ <el-tag :type="getDbTagType(row.mappedDb)" size="small">{{ 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">
+ <template #default="{ row }">
+ <el-switch
+ v-if="row.dataType === 'Bool'"
+ v-model="fieldEditValues[getFieldEditKey(row)]"
+ :disabled="!isFieldWritable(row)"
+ />
+ <el-input-number
+ v-else-if="row.dataType === 'Int' || row.dataType === 'DInt' || row.dataType === 'Byte'"
+ v-model="fieldEditValues[getFieldEditKey(row)]"
+ :disabled="!isFieldWritable(row)"
+ :controls="false"
+ style="width: 100%"
+ />
+ <el-input
+ v-else
+ v-model="fieldEditValues[getFieldEditKey(row)]"
+ :disabled="!isFieldWritable(row)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="90" fixed="right">
+ <template #default="{ row }">
+ <el-button
+ type="primary"
+ link
+ :loading="isWritingField(row)"
+ :disabled="!isFieldWritable(row)"
+ @click="handleWriteField(row)"
+ >
+ 鍐欏叆
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-tab-pane>
+ </el-tabs>
+ <el-table
+ v-else-if="view.fields.length > 0"
+ :data="view.fields"
+ border
+ size="small"
+ 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">
+ <template #default="{ row }">
+ <el-tag :type="getDbTagType(row.mappedDb)" size="small">{{ 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">
+ <template #default="{ row }">
+ <el-switch
+ v-if="row.dataType === 'Bool'"
+ v-model="fieldEditValues[getFieldEditKey(row)]"
+ :disabled="!isFieldWritable(row)"
+ />
+ <el-input-number
+ v-else-if="row.dataType === 'Int' || row.dataType === 'DInt' || row.dataType === 'Byte'"
+ v-model="fieldEditValues[getFieldEditKey(row)]"
+ :disabled="!isFieldWritable(row)"
+ :controls="false"
+ style="width: 100%"
+ />
+ <el-input
+ v-else
+ v-model="fieldEditValues[getFieldEditKey(row)]"
+ :disabled="!isFieldWritable(row)"
+ />
+ </template>
+ </el-table-column>
+ <el-table-column label="鎿嶄綔" width="90" fixed="right">
+ <template #default="{ row }">
+ <el-button
+ type="primary"
+ link
+ :loading="isWritingField(row)"
+ :disabled="!isFieldWritable(row)"
+ @click="handleWriteField(row)"
+ >
+ 鍐欏叆
+ </el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div v-else class="text-muted">褰撳墠DB鍧楁棤瀛楁鏄犲皠</div>
+
+ <div class="card-header-title field-title">鍘熷鏁版嵁</div>
+ <div class="db-raw-toolbar">
+ <el-button
+ v-if="needsExpand(view)"
+ link
+ type="primary"
+ class="expand-btn"
+ @click="toggleExpanded(view.templateDbNumber)"
+ >
+ {{ isExpanded(view.templateDbNumber) ? '鏀惰捣' : '灞曞紑鍏ㄩ儴' }}
+ </el-button>
+ </div>
+ <el-tabs class="data-view-tabs">
+ <el-tab-pane label="鍗佸叚杩涘埗">
+ <pre class="db-content">{{ getDisplayText(view.hex, view.templateDbNumber) }}</pre>
+ </el-tab-pane>
+ <el-tab-pane label="ASCII">
+ <pre class="db-content">{{ getDisplayText(view.ascii, view.templateDbNumber) }}</pre>
+ </el-tab-pane>
+ </el-tabs>
+ </el-tab-pane>
+ </el-tabs>
+ </div>
+ </template>
+ </el-skeleton>
+
+ <el-alert
+ v-if="unmappedFields.length > 0"
+ type="warning"
+ show-icon
+ :closable="false"
+ style="margin-top: 12px"
+ :title="`鏈� ${unmappedFields.length} 涓瓧娈垫湭鏄犲皠鍒板疄渚嬪唴瀛樺潡锛岃妫�鏌ュ疄渚婦B鍧楅厤缃笌妯℃澘DB鍙枫�俙"
+ />
+ </div>
+ </el-card>
</div>
</div>
</template>
<script setup lang="ts">
-import { ref, onMounted, onUnmounted } from 'vue'
+import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
-import {
- InfoFilled,
- Back,
- Loading,
- User,
- VideoPlay,
- VideoPause,
- Edit
-} from '@element-plus/icons-vue'
+import { InfoFilled, Back, Loading, User, VideoPlay, VideoPause, Edit } from '@element-plus/icons-vue'
import * as api from '../api'
-import type { InstanceState, InstanceStatus } from '../types'
+import type { InstanceConfig, InstanceState, InstanceStatus, ProtocolTemplate } from '../types'
const route = useRoute()
+const id = route.params.id as string
const instance = ref<InstanceState | null>(null)
+const instanceConfig = ref<InstanceConfig | null>(null)
+const protocolTemplate = ref<ProtocolTemplate | null>(null)
const loading = ref(true)
const errorMsg = ref('')
+const memoryLoading = ref(false)
+const autoRefreshDb = ref(true)
+const dbBlocks = ref<Array<{ dbNumber: number; start: number; end: number; nonZeroCount: number }>>([])
+const dbBytes = ref<Uint8Array>(new Uint8Array())
+const lastDbBase64 = ref('')
+const loadingInstanceRef = ref(false)
+const loadingMemoryRef = ref(false)
+const expandedDbViews = ref<Record<number, boolean>>({})
+const fieldEditValues = ref<Record<string, string | number | boolean>>({})
+const writingFieldKeys = ref<Record<string, boolean>>({})
+
let refreshTimer: number | null = null
-const id = route.params.id as string
+const parsedFields = computed(() => {
+ if (!protocolTemplate.value || !instanceConfig.value) return []
+ const blockSize = instanceConfig.value.memoryConfig.dbBlockSize || 0
+ if (blockSize <= 0) return []
+ return protocolTemplate.value.fields
+ .map(field => {
+ const normalizedType = normalizeDataType(field.dataType)
+ const mappedDbNumber = resolveMemoryBlockByTemplateDb(field.dbNumber)
+ return {
+ fieldKey: field.fieldKey,
+ templateDbNumber: field.dbNumber,
+ address: buildAddress(field.dbNumber, field.offset, normalizedType, field.bit ?? 0),
+ mappedDb: mappedDbNumber ? `DB${mappedDbNumber}` : '鏈槧灏�',
+ dataType: normalizedType,
+ direction: normalizeDirection(field.direction),
+ offset: field.offset,
+ bit: field.bit ?? 0,
+ length: field.length,
+ resolvedDbNumber: mappedDbNumber,
+ value: parseFieldValue(
+ field.dbNumber,
+ mappedDbNumber,
+ field.offset,
+ normalizedType,
+ field.length,
+ field.bit ?? 0
+ )
+ }
+ })
+ .sort((a, b) => a.offset - b.offset)
+})
+
+const unmappedFields = computed(() => parsedFields.value.filter(field => !field.resolvedDbNumber))
+
+const deviceDbViews = computed(() => {
+ const templateDbNumbers = Array.from(new Set(parsedFields.value.map(field => field.templateDbNumber))).sort((a, b) => a - b)
+ if (templateDbNumbers.length === 0) {
+ return []
+ }
+
+ return templateDbNumbers
+ .map(templateDbNumber => {
+ const block = dbBlocks.value.find(x => x.dbNumber === templateDbNumber)
+ const chunk = block ? dbBytes.value.slice(block.start, block.end) : new Uint8Array()
+ return {
+ templateDbNumber,
+ resolvedDbNumber: block?.dbNumber ?? null,
+ nonZeroCount: block?.nonZeroCount ?? 0,
+ hex: block ? formatHex(chunk) : '-',
+ ascii: block ? formatAscii(chunk) : '-',
+ fields: parsedFields.value
+ .filter(field => field.templateDbNumber === templateDbNumber)
+ .map(field => ({
+ fieldKey: field.fieldKey,
+ address: field.address,
+ mappedDb: field.mappedDb,
+ dataType: field.dataType,
+ direction: field.direction,
+ value: field.value,
+ templateDbNumber,
+ resolvedDbNumber: field.resolvedDbNumber,
+ offset: field.offset,
+ bit: field.bit,
+ length: field.length
+ })),
+ ...groupFieldsByPrefix(
+ parsedFields.value
+ .filter(field => field.templateDbNumber === templateDbNumber)
+ .map(field => ({
+ fieldKey: field.fieldKey,
+ address: field.address,
+ mappedDb: field.mappedDb,
+ dataType: field.dataType,
+ direction: field.direction,
+ value: field.value,
+ templateDbNumber,
+ resolvedDbNumber: field.resolvedDbNumber,
+ offset: field.offset,
+ bit: field.bit,
+ length: field.length
+ }))
+ )
+ }
+ })
+ .sort((a, b) => a.templateDbNumber - b.templateDbNumber)
+})
+
+watch(parsedFields, () => {
+ const next: Record<string, string | number | boolean> = {}
+ for (const field of parsedFields.value) {
+ const key = buildFieldEditKey(field.templateDbNumber, field.fieldKey)
+ next[key] = coerceEditValueByType(field.dataType, field.value)
+ }
+ fieldEditValues.value = next
+}, { immediate: true })
async function loadInstance() {
+ if (loadingInstanceRef.value) return
+ loadingInstanceRef.value = true
try {
- instance.value = await api.getInstance(id)
- if (!instance.value) {
+ const latestInstance = await api.getInstance(id)
+ const latestConfig = await api.getInstanceConfig(id)
+ if (latestInstance) {
+ instance.value = latestInstance
+ }
+ if (latestConfig) {
+ const shouldReloadTemplate = latestConfig.protocolTemplateId !== instanceConfig.value?.protocolTemplateId
+ instanceConfig.value = latestConfig
+ if (shouldReloadTemplate || !protocolTemplate.value) {
+ await loadProtocolTemplateForInstance()
+ }
+ }
+ if (!latestInstance) {
errorMsg.value = `瀹炰緥 "${id}" 涓嶅瓨鍦╜
}
} catch (err) {
console.error('鍔犺浇瀹炰緥澶辫触:', err)
errorMsg.value = '鍔犺浇瀹炰緥澶辫触锛岃鏌ョ湅鎺у埗鍙�'
} finally {
+ loadingInstanceRef.value = false
loading.value = false
}
}
-onMounted(() => {
- loadInstance()
- // 姣�2绉掑埛鏂颁竴娆$姸鎬�
+async function loadProtocolTemplateForInstance() {
+ if (instanceConfig.value?.protocolTemplateId) {
+ protocolTemplate.value = await api.getProtocolTemplate(instanceConfig.value.protocolTemplateId)
+ if (protocolTemplate.value) {
+ return
+ }
+ }
+
+ const templates = await api.getProtocolTemplates()
+ protocolTemplate.value =
+ templates.find(t => t.id === 'wcs-line-v260202') ??
+ (templates.length > 0 ? templates[0] : null)
+}
+
+async function loadMemoryData(showLoading = false) {
+ if (!instance.value || loadingMemoryRef.value) return
+ loadingMemoryRef.value = true
+
+ if (showLoading) {
+ memoryLoading.value = true
+ }
+ try {
+ const memory = await api.readMemory(id)
+ const dbBase64 = memory.DB || memory.db
+ if (!dbBase64) {
+ dbBlocks.value = []
+ dbBytes.value = new Uint8Array()
+ lastDbBase64.value = ''
+ return
+ }
+
+ if (dbBase64 === lastDbBase64.value) {
+ return
+ }
+
+ lastDbBase64.value = dbBase64
+ const decoded = decodeBase64(dbBase64)
+ dbBytes.value = decoded
+ const dbBlockNumbers = instanceConfig.value?.memoryConfig.dbBlockNumbers || []
+ const dbBlockCount = instanceConfig.value?.memoryConfig.dbBlockCount || dbBlockNumbers.length || 1
+ const dbBlockSize = instanceConfig.value?.memoryConfig.dbBlockSize || decoded.length
+ dbBlocks.value = splitDbBlocks(decoded, dbBlockCount, dbBlockSize, dbBlockNumbers)
+
+ } catch (err) {
+ console.error('鍔犺浇DB鏁版嵁澶辫触:', err)
+ ElMessage.error('鍔犺浇DB鏁版嵁澶辫触')
+ } finally {
+ loadingMemoryRef.value = false
+ if (showLoading) {
+ memoryLoading.value = false
+ }
+ }
+}
+
+onMounted(async () => {
+ await loadInstance()
+ await loadMemoryData(true)
+
refreshTimer = window.setInterval(() => {
if (instance.value && instance.value.status !== 'Stopped') {
loadInstance()
+ if (autoRefreshDb.value) {
+ loadMemoryData(false)
+ }
}
}, 2000)
})
@@ -182,6 +532,7 @@
})
await api.startInstance(id)
await loadInstance()
+ await loadMemoryData(true)
ElMessage.success('鍚姩鍛戒护宸插彂閫�')
} catch (err) {
if (err !== 'cancel') {
@@ -200,6 +551,7 @@
})
await api.stopInstance(id)
await loadInstance()
+ await loadMemoryData(true)
ElMessage.success('鍋滄鍛戒护宸插彂閫�')
} catch (err) {
if (err !== 'cancel') {
@@ -209,35 +561,449 @@
}
}
+function decodeBase64(base64: string): Uint8Array {
+ const raw = atob(base64)
+ const result = new Uint8Array(raw.length)
+ for (let i = 0; i < raw.length; i++) {
+ result[i] = raw.charCodeAt(i)
+ }
+ return result
+}
+
+function encodeBase64(bytes: Uint8Array): string {
+ let binary = ''
+ const step = 0x8000
+ for (let i = 0; i < bytes.length; i += step) {
+ const chunk = bytes.subarray(i, Math.min(i + step, bytes.length))
+ binary += String.fromCharCode(...chunk)
+ }
+ return btoa(binary)
+}
+
+function splitDbBlocks(bytes: Uint8Array, blockCount: number, blockSize: number, blockNumbers: number[]) {
+ const normalizedDbNumbers = blockNumbers.length > 0
+ ? blockNumbers.filter(x => Number.isInteger(x) && x > 0)
+ : Array.from({ length: blockCount }, (_, idx) => idx + 1)
+
+ const blocks: Array<{ dbNumber: number; start: number; end: number; nonZeroCount: number }> = []
+ for (let i = 0; i < normalizedDbNumbers.length; i++) {
+ const start = i * blockSize
+ const end = Math.min(start + blockSize, bytes.length)
+ if (start >= bytes.length) break
+ const chunk = bytes.slice(start, end)
+ blocks.push({
+ dbNumber: normalizedDbNumbers[i],
+ start,
+ end,
+ nonZeroCount: chunk.filter(x => x !== 0).length
+ })
+ }
+ return blocks
+}
+
+function formatHex(data: Uint8Array): string {
+ const lines: string[] = []
+ for (let i = 0; i < data.length; i += 16) {
+ const line = Array.from(data.slice(i, i + 16))
+ .map(x => x.toString(16).padStart(2, '0').toUpperCase())
+ .join(' ')
+ lines.push(line)
+ }
+ return lines.join('\n')
+}
+
+function formatAscii(data: Uint8Array): string {
+ const lines: string[] = []
+ for (let i = 0; i < data.length; i += 16) {
+ const chunk = data.slice(i, i + 16)
+ const line = Array.from(chunk)
+ .map(x => (x >= 32 && x <= 126 ? String.fromCharCode(x) : '路'))
+ .join('')
+ lines.push(line)
+ }
+ return lines.join('\n')
+}
+
+function countLines(text: string): number {
+ if (!text) return 0
+ return text.split('\n').length
+}
+
+function needsExpand(view: { hex: string; ascii: string }): boolean {
+ return Math.max(countLines(view.hex), countLines(view.ascii)) > 64
+}
+
+function isExpanded(dbNumber: number): boolean {
+ return expandedDbViews.value[dbNumber] === true
+}
+
+function toggleExpanded(dbNumber: number): void {
+ expandedDbViews.value[dbNumber] = !isExpanded(dbNumber)
+}
+
+function getDisplayText(text: string, dbNumber: number): string {
+ if (isExpanded(dbNumber)) {
+ return text
+ }
+
+ const lines = text.split('\n')
+ if (lines.length <= 64) {
+ return text
+ }
+
+ return lines.slice(0, 64).join('\n')
+}
+
+function buildFieldEditKey(templateDbNumber: number, fieldKey: string): string {
+ return `${templateDbNumber}:${fieldKey}`
+}
+
+function getFieldEditKey(row: { templateDbNumber: number; fieldKey: string }): string {
+ return buildFieldEditKey(row.templateDbNumber, row.fieldKey)
+}
+
+function isFieldWritable(row: { resolvedDbNumber: number | null }): boolean {
+ return row.resolvedDbNumber !== null
+}
+
+function isWritingField(row: { templateDbNumber: number; fieldKey: string }): boolean {
+ return writingFieldKeys.value[getFieldEditKey(row)] === true
+}
+
+function coerceEditValueByType(dataType: string, value: string): string | number | boolean {
+ if (dataType === 'Bool') {
+ return value === 'true'
+ }
+
+ if (dataType === 'Int' || dataType === 'DInt' || dataType === 'Byte') {
+ const num = Number(value)
+ return Number.isFinite(num) ? num : 0
+ }
+
+ return value === '(绌�)' ? '' : value
+}
+
+async function handleWriteField(row: {
+ templateDbNumber: number
+ resolvedDbNumber: number | null
+ fieldKey: string
+ dataType: string
+ offset: number
+ bit: number
+ length: number
+}) {
+ if (!row.resolvedDbNumber) {
+ ElMessage.warning('璇ュ瓧娈垫湭鏄犲皠鍒板綋鍓嶅疄渚� DB 鍧楋紝鏃犳硶鍐欏叆')
+ return
+ }
+
+ const editKey = getFieldEditKey(row)
+ const editedValue = fieldEditValues.value[editKey]
+ const targetBlock = dbBlocks.value.find(x => x.dbNumber === row.resolvedDbNumber)
+ if (!targetBlock) {
+ ElMessage.error('鏈壘鍒扮洰鏍� DB 鍧楋紝鏃犳硶鍐欏叆')
+ return
+ }
+
+ const nextBytes = new Uint8Array(dbBytes.value)
+ const absolute = targetBlock.start + row.offset
+ const blockEnd = targetBlock.end
+ if (!tryWriteFieldToBytes(nextBytes, row, editedValue, absolute, blockEnd)) {
+ return
+ }
+
+ const dbBase64 = encodeBase64(nextBytes)
+ writingFieldKeys.value[editKey] = true
+ try {
+ const ok = await api.writeMemory(id, { DB: dbBase64 })
+ if (!ok) {
+ ElMessage.error('鍐欏叆澶辫触锛岃鏌ョ湅鍚庡彴鏃ュ織')
+ return
+ }
+
+ dbBytes.value = nextBytes
+ lastDbBase64.value = dbBase64
+ const dbBlockNumbers = instanceConfig.value?.memoryConfig.dbBlockNumbers || []
+ const dbBlockCount = instanceConfig.value?.memoryConfig.dbBlockCount || dbBlockNumbers.length || 1
+ const dbBlockSize = instanceConfig.value?.memoryConfig.dbBlockSize || nextBytes.length
+ dbBlocks.value = splitDbBlocks(nextBytes, dbBlockCount, dbBlockSize, dbBlockNumbers)
+ ElMessage.success(`瀛楁 ${row.fieldKey} 鍐欏叆鎴愬姛`)
+ } finally {
+ writingFieldKeys.value[editKey] = false
+ }
+}
+
+function tryWriteFieldToBytes(
+ bytes: Uint8Array,
+ row: { dataType: string; fieldKey: string; bit: number; length: number },
+ editedValue: string | number | boolean | undefined,
+ absolute: number,
+ blockEnd: number
+): boolean {
+ if (absolute < 0 || absolute >= bytes.length || absolute >= blockEnd) {
+ ElMessage.error(`瀛楁 ${row.fieldKey} 鍋忕Щ瓒婄晫`)
+ return false
+ }
+
+ if (row.dataType === 'Bool') {
+ if (row.bit < 0 || row.bit > 7) {
+ ElMessage.error(`瀛楁 ${row.fieldKey} 鐨勪綅鍋忕Щ鏃犳晥`)
+ return false
+ }
+ const boolValue = Boolean(editedValue)
+ if (boolValue) {
+ bytes[absolute] = bytes[absolute] | (1 << row.bit)
+ } else {
+ bytes[absolute] = bytes[absolute] & ~(1 << row.bit)
+ }
+ return true
+ }
+
+ if (row.dataType === 'Byte') {
+ const value = Number(editedValue)
+ if (!Number.isInteger(value) || value < 0 || value > 255) {
+ ElMessage.error(`瀛楁 ${row.fieldKey} 浠呮敮鎸� 0-255`)
+ return false
+ }
+ bytes[absolute] = value
+ return true
+ }
+
+ if (row.dataType === 'Int') {
+ const value = Number(editedValue)
+ if (!Number.isInteger(value) || value < -32768 || value > 32767) {
+ ElMessage.error(`瀛楁 ${row.fieldKey} 浠呮敮鎸� -32768 鍒� 32767`)
+ return false
+ }
+ if (absolute + 1 >= blockEnd) {
+ ElMessage.error(`瀛楁 ${row.fieldKey} 瓒呭嚭 DB 鍧楄寖鍥碻)
+ return false
+ }
+ const unsigned = value < 0 ? value + 0x10000 : value
+ bytes[absolute] = (unsigned >> 8) & 0xff
+ bytes[absolute + 1] = unsigned & 0xff
+ return true
+ }
+
+ if (row.dataType === 'DInt') {
+ const value = Number(editedValue)
+ if (!Number.isInteger(value) || value < -2147483648 || value > 2147483647) {
+ ElMessage.error(`瀛楁 ${row.fieldKey} 浠呮敮鎸� 32 浣嶆湁绗﹀彿鏁存暟`)
+ return false
+ }
+ if (absolute + 3 >= blockEnd) {
+ ElMessage.error(`瀛楁 ${row.fieldKey} 瓒呭嚭 DB 鍧楄寖鍥碻)
+ return false
+ }
+ const unsigned = value < 0 ? value + 0x100000000 : value
+ bytes[absolute] = (unsigned >>> 24) & 0xff
+ bytes[absolute + 1] = (unsigned >>> 16) & 0xff
+ bytes[absolute + 2] = (unsigned >>> 8) & 0xff
+ bytes[absolute + 3] = unsigned & 0xff
+ return true
+ }
+
+ const text = String(editedValue ?? '')
+ const maxLength = Math.max(1, row.length || 32)
+ if (absolute + maxLength > blockEnd) {
+ ElMessage.error(`瀛楁 ${row.fieldKey} 瓒呭嚭 DB 鍧楄寖鍥碻)
+ return false
+ }
+ for (let i = 0; i < maxLength; i++) {
+ bytes[absolute + i] = i < text.length ? text.charCodeAt(i) & 0xff : 0
+ }
+ return true
+}
+
+function getDbTagType(dbTag: string): 'success' | 'info' | 'warning' | 'danger' {
+ if (dbTag === '鏈槧灏�') {
+ return 'warning'
+ }
+
+ const num = Number(dbTag.replace('DB', ''))
+ if (!Number.isFinite(num) || num <= 0) {
+ return 'info'
+ }
+
+ const palette: Array<'success' | 'info' | 'danger'> = ['success', 'info', 'danger']
+ return palette[num % palette.length]
+}
+
+function getFieldGroupTagType(groupKey: string): 'success' | 'info' | 'warning' | 'danger' {
+ const num = Number(groupKey)
+ if (!Number.isFinite(num) || num <= 0) {
+ return 'info'
+ }
+
+ const palette: Array<'success' | 'warning' | 'danger'> = ['success', 'warning', 'danger']
+ return palette[num % palette.length]
+}
+
+function groupFieldsByPrefix(fields: Array<{
+ fieldKey: string
+ address: string
+ mappedDb: string
+ dataType: string
+ direction: string
+ value: string
+}>) {
+ const hasNumericPrefix = fields.some(field => /^(\d+)_/.test(field.fieldKey))
+ if (!hasNumericPrefix) {
+ return {
+ fieldGroupEnabled: false,
+ fieldGroups: []
+ }
+ }
+
+ const groups = new Map<string, typeof fields>()
+
+ for (const field of fields) {
+ const groupKey = resolveFieldGroupKey(field.fieldKey)
+ const current = groups.get(groupKey) ?? []
+ current.push(field)
+ groups.set(groupKey, current)
+ }
+
+ const fieldGroups = Array.from(groups.entries())
+ .map(([key, groupFields]) => ({
+ key,
+ fields: groupFields
+ }))
+ .sort((a, b) => a.key.localeCompare(b.key, 'zh-CN'))
+
+ return {
+ fieldGroupEnabled: true,
+ fieldGroups
+ }
+}
+
+function resolveFieldGroupKey(fieldKey: string): string {
+ const match = fieldKey.match(/^(\d+)_/)
+ if (match && match[1]) {
+ return match[1]
+ }
+
+ return '鍏朵粬'
+}
+
+function resolveMemoryBlockByTemplateDb(templateDbNumber: number): number | null {
+ return dbBlocks.value.some(x => x.dbNumber === templateDbNumber) ? templateDbNumber : null
+}
+
+function buildAddress(dbNumber: number, offset: number, dataType: string, bit: number): string {
+ switch (dataType) {
+ case 'Int':
+ return `DB${dbNumber}.DBW${offset}`
+ case 'DInt':
+ return `DB${dbNumber}.DBD${offset}`
+ case 'Bool':
+ return `DB${dbNumber}.DBX${offset}.${bit}`
+ default:
+ return `DB${dbNumber}.DBB${offset}`
+ }
+}
+
+function parseFieldValue(
+ templateDbNumber: number,
+ resolvedDbNumber: number | null,
+ offset: number,
+ dataType: string,
+ length: number,
+ bit: number
+): string {
+ if (!resolvedDbNumber) {
+ return `妯℃澘鍦板潃 DB${templateDbNumber} 鏈槧灏勫埌褰撳墠瀹炰緥鍐呭瓨鍧梎
+ }
+
+ const block = dbBlocks.value.find(x => x.dbNumber === resolvedDbNumber)
+ if (!block) {
+ return `妯℃澘鍦板潃 DB${templateDbNumber} 鏈槧灏勫埌褰撳墠瀹炰緥鍐呭瓨鍧梎
+ }
+
+ const absolute = block.start + offset
+ if (absolute < 0 || absolute >= dbBytes.value.length) return '-'
+
+ if (dataType === 'Bool') {
+ if (bit < 0 || bit > 7) return '-'
+ return ((dbBytes.value[absolute] >> bit) & 0x01) === 1 ? 'true' : 'false'
+ }
+
+ if (dataType === 'Byte') {
+ return String(dbBytes.value[absolute])
+ }
+
+ if (dataType === 'Int') {
+ if (absolute + 1 >= dbBytes.value.length) return '-'
+ const value = (dbBytes.value[absolute] << 8) | dbBytes.value[absolute + 1]
+ return String(value > 0x7fff ? value - 0x10000 : value)
+ }
+
+ if (dataType === 'DInt') {
+ if (absolute + 3 >= dbBytes.value.length) return '-'
+ const value =
+ (dbBytes.value[absolute] << 24) |
+ (dbBytes.value[absolute + 1] << 16) |
+ (dbBytes.value[absolute + 2] << 8) |
+ dbBytes.value[absolute + 3]
+ return String(value)
+ }
+
+ if (dataType === 'String') {
+ const len = Math.max(1, length || 32)
+ const end = Math.min(absolute + len, dbBytes.value.length)
+ const chars = Array.from(dbBytes.value.slice(absolute, end)).map(x =>
+ x >= 32 && x <= 126 ? String.fromCharCode(x) : ''
+ )
+ const text = chars.join('').trim()
+ return text || '(绌�)'
+ }
+
+ return '-'
+}
+
+function normalizeDataType(input: string | number): 'Byte' | 'Int' | 'DInt' | 'String' | 'Bool' {
+ if (typeof input === 'number') {
+ return input === 1 ? 'Int' : input === 2 ? 'DInt' : input === 3 ? 'String' : input === 4 ? 'Bool' : 'Byte'
+ }
+ return input as 'Byte' | 'Int' | 'DInt' | 'String' | 'Bool'
+}
+
+function normalizeDirection(input: string | number): string {
+ if (typeof input === 'number') {
+ return input === 1 ? 'PlcToWcs' : input === 2 ? 'Bidirectional' : 'WcsToPlc'
+ }
+ return input
+}
+
function getStatusTagType(status: InstanceStatus): 'success' | 'info' | 'warning' | 'danger' {
const map: Record<InstanceStatus, 'success' | 'info' | 'warning' | 'danger'> = {
- 'Stopped': 'info',
- 'Starting': 'info',
- 'Running': 'success',
- 'Stopping': 'warning',
- 'Error': 'danger'
+ Stopped: 'info',
+ Starting: 'info',
+ Running: 'success',
+ Stopping: 'warning',
+ Error: 'danger'
}
return map[status] || 'info'
}
function getStatusText(status: InstanceStatus): string {
const map: Record<InstanceStatus, string> = {
- 'Stopped': '宸插仠姝�',
- 'Starting': '鍚姩涓�',
- 'Running': '杩愯涓�',
- 'Stopping': '鍋滄涓�',
- 'Error': '閿欒'
+ Stopped: '宸插仠姝�',
+ Starting: '鍚姩涓�',
+ Running: '杩愯涓�',
+ Stopping: '鍋滄涓�',
+ Error: '閿欒'
}
return map[status] || status
}
function getPlcTypeText(plcType: string): string {
const map: Record<string, string> = {
- 'S7200Smart': 'S7-200 Smart',
- 'S71200': 'S7-1200',
- 'S71500': 'S7-1500',
- 'S7300': 'S7-300',
- 'S7400': 'S7-400'
+ S7200Smart: 'S7-200 Smart',
+ S71200: 'S7-1200',
+ S71500: 'S7-1500',
+ S7300: 'S7-300',
+ S7400: 'S7-400'
}
return map[plcType] || plcType
}
@@ -319,4 +1085,66 @@
gap: 12px;
flex-wrap: wrap;
}
+
+.db-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.db-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.db-content {
+ margin: 0;
+ max-height: 180px;
+ overflow: auto;
+ white-space: pre-wrap;
+ font-family: Consolas, Monaco, 'Courier New', monospace;
+ font-size: 12px;
+ line-height: 1.5;
+}
+
+.db-block-panel {
+ margin-bottom: 18px;
+}
+
+.db-block-title {
+ font-weight: 500;
+ margin-bottom: 8px;
+ color: #606266;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.expand-btn {
+ margin-left: auto;
+}
+
+.db-tabs {
+ margin-top: 8px;
+}
+
+.data-view-tabs {
+ margin-top: 8px;
+}
+
+.db-raw-toolbar {
+ display: flex;
+ justify-content: flex-end;
+ margin-bottom: 4px;
+}
+
+.field-tabs {
+ margin-top: 10px;
+}
+
+.field-title {
+ margin-top: 12px;
+ margin-bottom: 8px;
+}
</style>
--
Gitblit v1.9.3