Forráskód Böngészése

UI部分控件逻辑与显示逻辑调整

avery 3 napja
szülő
commit
5c524b967d

+ 40 - 1
app.wxss

@@ -256,6 +256,10 @@ page {
   color: #94a3b8;
 }
 
+.theme-dark .generic-info-row--enum-list {
+  background: rgba(15, 23, 42, 0.16);
+}
+
 .theme-dark .generic-value-input,
 .theme-dark .crc-data-input,
 .theme-dark .generic-struct-input {
@@ -1207,6 +1211,41 @@ page {
   word-break: break-all;
 }
 
+.generic-info-row--toggle {
+  cursor: pointer;
+}
+
+.generic-info-value--toggle {
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  gap: 12rpx;
+}
+
+.generic-info-value--toggle .entry-chevron {
+  flex: none;
+  transform: rotate(45deg);
+}
+
+.generic-info-value--toggle .entry-chevron.is-expanded {
+  transform: rotate(135deg);
+}
+
+.generic-info-row--enum-list {
+  align-items: flex-start;
+  padding: 14rpx 0;
+  background: rgba(248, 250, 252, 0.72);
+}
+
+.generic-info-value--enum-list {
+  height: 360rpx;
+  max-height: 360rpx;
+  overflow-y: auto;
+  font-family: Menlo, Monaco, Consolas, monospace;
+  font-size: 23rpx;
+  line-height: 1.45;
+}
+
 .panel-action-button--icon {
   min-width: 58rpx;
   width: 58rpx;
@@ -1314,7 +1353,7 @@ page {
 .generic-scalar-value {
   flex: none;
   min-width: 96rpx;
-  max-width: 220rpx;
+  max-width: 190rpx;
   color: #0f8f87;
   font-family: Menlo, Monaco, Consolas, monospace;
   font-size: 25rpx;

+ 162 - 11
domain/parameter-groups/model.js

@@ -43,10 +43,13 @@ const {
   isBitRegisterType,
   isByteRegister,
   isTextRegister,
+  normalizeEnumOptions,
   normalizeBitOffset,
   normalizeBitWidth,
   normalizeTextByteLength,
   parseCoilValue,
+  parseEnumValueText,
+  parseNumberText,
   registerTypeIsBit,
   supportsRange,
   supportsUnit
@@ -429,6 +432,14 @@ function isStorageScalarGroup(group = {}) {
     && isStorageScalarEntryKind(group.sourceEntryKind)
 }
 
+function isStorageSourceLockedGroup(group = {}) {
+  return isStorageMemoryGroup(group)
+}
+
+function isStorageSourceLockedRegister(group = {}, register = {}) {
+  return isStorageSourceLockedGroup(group) || isStorageMemoryGroup(register)
+}
+
 function isParameterGroupPollEnabled(group = {}) {
   return group.pollEnabled !== false
 }
@@ -474,18 +485,98 @@ function formatStorageRegisterHexValue(register, rawBytes = []) {
   return formatStorageHexBytes(rawBytes, register.byteLength)
 }
 
+function getStorageRegisterTypeText(register = {}) {
+  const enumType = String(register.enumName || '').trim()
+  if (enumType) return enumType
+
+  const entryKind = String(register.sourceEntryKind || '').trim().toLowerCase()
+  const sourceValueType = String(register.sourceValueType || register.sourceElementType || '').trim().toLowerCase()
+  const isAggregateField = entryKind === 'struct' || sourceValueType === 'struct'
+  const valueTypeCandidates = [
+    register.dataTypeText,
+    register.dataType
+  ]
+  const sourceTypeCandidates = [
+    register.sourceDefinitionName,
+    register.sourceSymbolType,
+    register.sourceValueType,
+    register.sourceElementType
+  ]
+  const candidates = isAggregateField
+    ? valueTypeCandidates.concat(sourceTypeCandidates)
+    : sourceTypeCandidates.concat(valueTypeCandidates)
+
+  for (const value of candidates) {
+    const text = String(value || '').trim()
+    if (text && text !== '---') return text
+  }
+
+  return ''
+}
+
 function findEnumOptionByValue(register = {}) {
   const rawValue = Number(register.rawValue)
   if (!Number.isFinite(rawValue)) return null
 
-  return (Array.isArray(register.enumOptions) ? register.enumOptions : []).find((option) => (
+  return normalizeEnumOptions(register).find((option) => (
     Number(option && option.value) === rawValue
   )) || null
 }
 
+function findEnumOptionByInputValue(register = {}) {
+  const valueText = normalizeTextValue(register.inputValue).trim()
+  if (!valueText || valueText === '--') return null
+
+  let parsedValue = parseEnumValueText(register, valueText)
+  if (parsedValue === null) parsedValue = parseNumberText(valueText, register.dataType)
+  if (parsedValue === null) return null
+
+  return normalizeEnumOptions(register).find((option) => (
+    Number(option.value) === Number(parsedValue)
+  )) || null
+}
+
+function findScalarEnumOption(register = {}) {
+  if (register.isDirty) return findEnumOptionByInputValue(register)
+  if (!hasReadScalarValue(register)) return null
+
+  return findEnumOptionByValue(register)
+}
+
+function hasReadScalarValue(register = {}) {
+  return register.rawValue !== null
+    && register.rawValue !== undefined
+    && !Array.isArray(register.rawValue)
+}
+
+function isEnumSourceText(value) {
+  const text = String(value || '').trim().toLowerCase()
+
+  return text === 'enum' || /^enum\b/.test(text) || /\benum\b/.test(text)
+}
+
+function isEnumLikeRegister(group = {}, register = {}) {
+  if (String(register.enumName || '').trim()) return true
+  if (normalizeEnumOptions(register).length) return true
+
+  return [
+    group.sourceEntryKind,
+    group.sourceElementType,
+    group.sourceValueType,
+    group.sourceSymbolType,
+    register.sourceEntryKind,
+    register.sourceElementType,
+    register.sourceValueType,
+    register.sourceSymbolType
+  ].some(isEnumSourceText)
+}
+
 function formatScalarDecimalValue(register = {}) {
   const rawValue = register.rawValue
-  if (rawValue === null || rawValue === undefined || Array.isArray(rawValue)) return '--'
+  if (rawValue === null || rawValue === undefined || Array.isArray(rawValue)) return ''
+
+  const enumOption = findEnumOptionByValue(register)
+  if (enumOption) return `${enumOption.label || enumOption.name} (${Number(rawValue)})`
 
   const dataType = getDataType(register.dataType).key
   if (/^(?:u?int)(?:64|128|256)_t$/.test(dataType)) {
@@ -500,6 +591,22 @@ function formatScalarDecimalValue(register = {}) {
     : String(Math.trunc(numberValue))
 }
 
+function formatScalarInputValue(register = {}) {
+  const dirtyInputValue = normalizeTextValue(register.inputValue).trim()
+  if (register.isDirty) return dirtyInputValue
+  if (!hasReadScalarValue(register)) return ''
+
+  const rawValue = register.rawValue
+  if (typeof rawValue === 'bigint') return rawValue.toString()
+  if (typeof rawValue === 'number') {
+    if (!Number.isFinite(rawValue)) return ''
+
+    return Number.isInteger(rawValue) ? String(rawValue) : formatCompactNumber(rawValue, 6)
+  }
+
+  return normalizeTextValue(rawValue).trim()
+}
+
 function createScalarDefinitionText(register = {}, group = {}) {
   const enumOption = findEnumOptionByValue(register)
   const typeName = String(register.enumName || register.sourceSymbolType || group.sourceSymbolType || '').trim()
@@ -512,10 +619,20 @@ function createScalarDefinitionText(register = {}, group = {}) {
   return definitionText
 }
 
+function createScalarEnumDefinitionText(register = {}, group = {}) {
+  if (!normalizeEnumOptions(register).length) return ''
+
+  const enumOption = findScalarEnumOption(register)
+  if (!enumOption) return ''
+
+  return enumOption.label || enumOption.name
+}
+
 function normalizeRegister(register, group, index, address, byteOffset = 0) {
   const registerType = getRegisterType(group.registerType).key
   const layout = isStructLayout(group.layout) ? GROUP_LAYOUT_STRUCT : GROUP_LAYOUT_REGISTER
   const isStorageMemory = isStorageMemoryGroup(group)
+  const sourceMetadataLocked = isStorageSourceLockedRegister(group, register)
   const byteAddressed = isByteAddressedGroup(group)
   const dataType = normalizeRegisterDataType(register, registerType)
   const bitOffset = normalizeBitOffset(register.bitOffset)
@@ -558,12 +675,13 @@ function normalizeRegister(register, group, index, address, byteOffset = 0) {
     : (isBitRegisterType(registerType)
       ? formatCoilDisplayValue(rawValue)
       : (byteAddressed ? formatRawByteText(rawBytes) : formatRawWordText(rawWords)))
+  const hasRawValue = rawValue !== null && rawValue !== undefined
   const rawValueText = isStorageMemory
-    ? formatStorageRegisterHexValue({
+    ? (hasRawValue ? formatStorageRegisterHexValue({
       ...register,
       byteLength,
       dataType
-    }, rawBytes)
+    }, rawBytes) : '')
     : defaultRawValueText
   const displayValue = rawValue === null
     ? (inputValue.trim() ? inputValue : '--')
@@ -599,9 +717,18 @@ function normalizeRegister(register, group, index, address, byteOffset = 0) {
   const registerStartAddressText = isStorageMemory
     ? formatStorageStartAddressText(address, storageAddressWidth)
     : formatRegisterStartAddressText(address)
+  const registerTypeText = getStorageRegisterTypeText({
+    ...register,
+    dataType,
+    dataTypeText: getRegisterValueTypeLabel(dataType)
+  })
   const metaText = isStorageMemory
-    ? `${registerStartAddressText} ${rawValueText}`
+    ? [registerStartAddressText, registerTypeText, rawValueText].filter(Boolean).join(' ')
     : `${addressText} ${rawValueText}`.trim()
+  const cardMetaText = isStorageMemory
+    ? metaText
+    : (sourceMetadataLocked ? '' : metaText)
+  const name = register.name || `寄存器 ${index + 1}`
 
   return {
     address,
@@ -631,10 +758,11 @@ function normalizeRegister(register, group, index, address, byteOffset = 0) {
     isPlaceholderByteField,
     isDirty: !!register.isDirty,
     maxValue: normalizeTextValue(register.maxValue),
-    metaText,
+    displayName: name,
+    metaText: cardMetaText,
     minValue: normalizeTextValue(register.minValue),
     memoryEndian,
-    name: register.name || `寄存器 ${index + 1}`,
+    name,
     conversionFormula,
     conversionFormulaErrorText: conversionResult && conversionResult.errorText ? conversionResult.errorText : '',
     rawValue,
@@ -642,6 +770,8 @@ function normalizeRegister(register, group, index, address, byteOffset = 0) {
     rawBytes,
     rawWords,
     registerCount,
+    registerStartAddressText,
+    registerTypeText,
     byteOffset,
     registerType,
     showDataType: !isBitRegisterType(registerType),
@@ -653,7 +783,8 @@ function normalizeRegister(register, group, index, address, byteOffset = 0) {
     structByteLength: register.structByteLength,
     remark: register.remark || '',
     ...pickFields(register, SOURCE_REGISTER_FIELDS),
-    sourceMetaText: createRegisterSourceMetaText(register)
+    sourceMetaText: createRegisterSourceMetaText(register),
+    sourceMetadataLocked
   }
 }
 
@@ -746,6 +877,7 @@ function normalizeGroup(group) {
   const addressSpan = byteAddressed ? Math.max(1, byteLength) : wordQuantity
   const storageMemory = isStorageMemoryGroup(baseGroup)
   const storageScalarGroup = isStorageScalarGroup(baseGroup)
+  const sourceMetadataLocked = isStorageSourceLockedGroup(baseGroup)
   const storageAddressWidth = storageMemory ? resolveStorageAddressWidth(baseGroup, startAddress) : 0
   const storageAddressMax = getStorageAddressMax(storageAddressWidth, startAddress)
   const addressOverflow = storageMemory
@@ -761,13 +893,25 @@ function normalizeGroup(group) {
     : (addressOverflow ? `0x${padWordHex(addressMax)}+` : `0x${padWordHex(endAddress)}`)
   const scalarRegister = storageScalarGroup ? (registers[0] || null) : null
   const scalarDefinitionText = scalarRegister ? createScalarDefinitionText(scalarRegister, baseGroup) : ''
+  const scalarEnumDefinitionText = scalarRegister ? createScalarEnumDefinitionText(scalarRegister, baseGroup) : ''
+  const scalarInputValueText = scalarRegister ? formatScalarInputValue(scalarRegister) : ''
   const scalarRawHexText = scalarRegister ? scalarRegister.rawValueText : ''
   const scalarValueText = scalarRegister ? formatScalarDecimalValue(scalarRegister) : ''
+  const isStorageEnumScalar = !!(storageScalarGroup && scalarRegister && isEnumLikeRegister(baseGroup, scalarRegister))
   const displayName = storageMemory
     ? stripStorageAreaPrefix(baseGroup.name)
     : baseGroup.name
+  const storageCardMetaParts = [startAddressText]
+  if (storageScalarGroup && scalarEnumDefinitionText) {
+    storageCardMetaParts.push(scalarEnumDefinitionText)
+  }
+  const storageCardMetaText = storageMemory
+    ? storageCardMetaParts.filter(Boolean).join(' ')
+    : ''
   const listMetaText = storageMemory
-    ? (storageScalarGroup
+    ? (sourceMetadataLocked
+      ? ''
+      : (storageScalarGroup
       ? [
         getStorageAreaText(baseGroup),
         startAddressText,
@@ -775,10 +919,10 @@ function normalizeGroup(group) {
         scalarRawHexText,
         scalarDefinitionText
       ].filter(Boolean).join(' ')
-      : `${getStorageAreaText(baseGroup)} ${startAddressText}-${endAddressText} ${byteLength}B`)
+      : `${getStorageAreaText(baseGroup)} ${startAddressText}-${endAddressText} ${byteLength}B`))
     : `${formatAddressRange(startAddress, addressSpan)} · ${quantity}/${wordQuantity}${createGroupSourceMetaText(baseGroup) ? ` · ${createGroupSourceMetaText(baseGroup)}` : ''}${addressOverflow ? ' · 地址超出 0xFFFF' : ''}`
   const detailMetaText = storageMemory
-    ? `${getStorageAreaText(baseGroup, true)} ${startAddressText} ${quantity}/${byteLength}B`
+    ? (sourceMetadataLocked ? '' : `${getStorageAreaText(baseGroup, true)} ${startAddressText} ${quantity}/${byteLength}B`)
     : `${formatAddressRange(startAddress, addressSpan)} · ${quantity}/${wordQuantity}${createGroupSourceMetaText(baseGroup) ? ` · ${createGroupSourceMetaText(baseGroup)}` : ''}${addressOverflow ? ' · 地址超出 0xFFFF' : ''}`
   const sourceMetaText = createGroupSourceMetaText(baseGroup)
 
@@ -797,6 +941,7 @@ function normalizeGroup(group) {
     endAddressText,
     functionCode: registerType.functionCode,
     isReadOnly: !registerType.writable,
+    isStorageEnumScalar,
     isStorageScalarGroup: storageScalarGroup,
     byteLength,
     listMetaText,
@@ -806,10 +951,14 @@ function normalizeGroup(group) {
     registers,
     paddedByteLength,
     scalarDefinitionText,
+    scalarEnumDefinitionText,
+    scalarInputValueText,
     scalarRawHexText,
     scalarValueText,
     sourceMetaText,
+    sourceMetadataLocked,
     startAddressText,
+    storageCardMetaText,
     wordQuantity,
     writable: registerType.writable
   }
@@ -927,6 +1076,8 @@ module.exports = {
   isParameterGroupPollEnabled,
   isStorageMemoryGroup,
   isStorageScalarGroup,
+  isStorageSourceLockedGroup,
+  isStorageSourceLockedRegister,
   isStorageStructGroup,
   isTextRegister,
   normalizeGroup,

+ 18 - 3
features/parameter-groups/dialog-handlers.js

@@ -66,6 +66,15 @@ function createDialogHandlers(parameterGroupService) {
       })
     },
 
+    toggleParameterEnumOptions() {
+      const dialog = this.data.parameterDialog || {}
+      if (!Array.isArray(dialog.enumOptions) || !dialog.enumOptions.length) return
+
+      this.updateParameterDialog({
+        enumExpanded: !dialog.enumExpanded
+      })
+    },
+
     parseParameterStructDefinition() {
       const dialog = this.data.parameterDialog || createParameterDialogState()
       const sourceText = dialog.structDefinition || ''
@@ -150,6 +159,7 @@ function createDialogHandlers(parameterGroupService) {
         register
       } = findParameterRegister(getParameterGroupsFromState(this.data), groupId, registerIndex)
       if (!register) return
+      if (register.sourceMetadataLocked || (group && group.sourceMetadataLocked)) return
 
       this.updateParameterDialog(createParameterRegisterDialogState('viewRegister', group, register, registerIndex))
     },
@@ -197,9 +207,11 @@ function createDialogHandlers(parameterGroupService) {
       }
 
       if (mode === 'editGroup') {
-        const group = parameterGroupService.updateGroupConfig(dialog.groupId, createParameterGroupConfig(dialog))
+        const group = dialog.metadataLocked
+          ? parameterGroupService.updateGroupPollEnabled(dialog.groupId, dialog.pollEnabled)
+          : parameterGroupService.updateGroupConfig(dialog.groupId, createParameterGroupConfig(dialog))
         if (group) {
-          if (this.pageToast) this.pageToast.show(`${group.name}已更新`)
+          if (this.pageToast) this.pageToast.show(`${group.displayName || group.name}已更新`)
           this.closeParameterDraft()
           if (this.data.activeParameterGroupId === group.id) {
             this.setData({
@@ -220,7 +232,10 @@ function createDialogHandlers(parameterGroupService) {
           return
         }
         parameterGroupService.updateRegister(dialog.groupId, dialog.registerIndex, changedData)
-        if (this.pageToast) this.pageToast.show(`${dialog.name || '寄存器'}已更新`)
+        if (dialog.showPollEnabled) {
+          parameterGroupService.updateGroupPollEnabled(dialog.groupId, dialog.pollEnabled)
+        }
+        if (this.pageToast) this.pageToast.show(`${dialog.displayName || dialog.name || '寄存器'}已更新`)
         this.closeParameterDraft()
       }
     }

+ 18 - 10
features/parameter-groups/io.js

@@ -66,6 +66,14 @@ function getMemoryByteLength(group = {}) {
   return Math.max(1, Math.ceil((Number(group.wordQuantity) || 1) * 2))
 }
 
+function getGroupCommandName(group = {}, fallback = '参数组') {
+  return group.displayName || group.name || fallback
+}
+
+function getRegisterCommandName(register = {}, fallback = '变量') {
+  return register.displayName || register.name || fallback
+}
+
 function shouldUseStorageAccess(options = {}, group = {}) {
   return !!options.useStorageAccess && isMemoryGroup(group)
 }
@@ -246,7 +254,7 @@ function getRegisterWordsForModbusWrite(group) {
     const registerWords = getRegisterEncodedWords(register)
 
     if (!Array.isArray(registerWords) || !registerWords.length) {
-      throw new Error(`${register.name || `寄存器 ${index + 1}`} 没有有效写入值`)
+      throw new Error(`${getRegisterCommandName(register, `寄存器 ${index + 1}`)} 没有有效写入值`)
     }
 
     const dataType = getDataType(register.dataType).key
@@ -296,7 +304,7 @@ async function writeModbusCoilGroup(group, options = {}) {
     const coilValue = parseCoilValue(getRegisterWriteValueText(register))
 
     if (coilValue === null) {
-      transport.showCommandAlert('参数组写入', `${register.name || `寄存器 ${index + 1}`} 没有有效写入值`)
+      transport.showCommandAlert('参数组写入', `${getRegisterCommandName(register, `寄存器 ${index + 1}`)} 没有有效写入值`)
       return null
     }
 
@@ -304,7 +312,7 @@ async function writeModbusCoilGroup(group, options = {}) {
       slaveAddress,
       group.startAddress + index,
       !!coilValue,
-      register.name || group.name || '参数组写入',
+      getRegisterCommandName(register, getGroupCommandName(group, '参数组写入')),
       'parameter-group-coil-write',
       {
         maxFrameBytes: options.maxPacketLength
@@ -348,7 +356,7 @@ async function writeModbusRegisterGroup(group, options = {}) {
       slaveAddress,
       span.address,
       spanWords,
-      group.name || '参数组写入',
+      getGroupCommandName(group, '参数组写入'),
       'parameter-group-write',
       {
         maxFrameBytes: options.maxPacketLength
@@ -440,7 +448,7 @@ async function writeMemoryRegister(group, register, options = {}) {
         memoryType,
         address,
         byteLength,
-        register.name ? `${register.name} 读改写` : '变量读改写',
+        `${getRegisterCommandName(register)} 读改写`,
         'parameter-group-memory-register-rmw-read',
         {
           maxFrameBytes: maxPacketLength
@@ -466,7 +474,7 @@ async function writeMemoryRegister(group, register, options = {}) {
   }
 
   if (!Array.isArray(bytes) || !bytes.length) {
-    transport.showCommandAlert('内存写入', `${register.name || '变量'} 没有有效写入值`)
+    transport.showCommandAlert('内存写入', `${getRegisterCommandName(register)} 没有有效写入值`)
     return null
   }
 
@@ -477,7 +485,7 @@ async function writeMemoryRegister(group, register, options = {}) {
     memoryType,
     address,
     bytes,
-    register.name || group.name || '变量写入',
+    getRegisterCommandName(register, getGroupCommandName(group, '变量写入')),
     'parameter-group-memory-register-write',
     {
       maxFrameBytes: maxPacketLength
@@ -506,7 +514,7 @@ async function readMemoryGroup(group, options = {}) {
     memoryType,
     address,
     byteLength,
-    group.name || '内存读取',
+    getGroupCommandName(group, '内存读取'),
     'parameter-group-memory-read',
     {
       maxFrameBytes: maxPacketLength,
@@ -548,7 +556,7 @@ async function writeMemoryGroup(group, options = {}) {
         memoryType,
         address,
         byteLength,
-        group.name ? `${group.name} 读改写` : '内存读改写',
+        `${getGroupCommandName(group, '内存')} 读改写`,
         'parameter-group-memory-rmw-read',
         {
           maxFrameBytes: maxPacketLength
@@ -568,7 +576,7 @@ async function writeMemoryGroup(group, options = {}) {
     memoryType,
     address,
     bytes,
-    group.name || '内存写入',
+    getGroupCommandName(group, '内存写入'),
     'parameter-group-memory-write',
     {
       maxFrameBytes: maxPacketLength

+ 113 - 5
features/parameter-groups/service.js

@@ -23,11 +23,19 @@ const {
   REGISTER_TYPE_OPTIONS,
   cloneImportedGroup,
   getDataType,
+  getRegisterWriteValueText,
   isAddressRangeOverflow,
+  isStorageSourceLockedGroup,
+  isStorageSourceLockedRegister,
   normalizeGroup,
   normalizeGroupConfig,
   validateRegisterValue
 } = require('../../domain/parameter-groups/model.js')
+const {
+  normalizeEnumOptions,
+  parseEnumValueText,
+  parseNumberText
+} = require('../../domain/parameter-groups/value-codec.js')
 const {
   findGroup,
   getGroups,
@@ -109,7 +117,7 @@ function isUnconfiguredRegister(register = {}) {
 }
 
 function getUnconfiguredRegisterName(register = {}, index = 0) {
-  return register.name || `变量 ${index + 1}`
+  return register.displayName || register.name || `变量 ${index + 1}`
 }
 
 function findUnconfiguredRegister(group = {}) {
@@ -124,6 +132,74 @@ function findUnconfiguredRegister(group = {}) {
     : null
 }
 
+function isEnumSourceText(value) {
+  const text = String(value || '').trim().toLowerCase()
+
+  return text === 'enum' || /^enum\b/.test(text) || /\benum\b/.test(text)
+}
+
+function isEnumLikeRegister(group = {}, register = {}) {
+  if (String(register.enumName || '').trim()) return true
+  if (normalizeEnumOptions(register).length) return true
+
+  return [
+    group.sourceEntryKind,
+    group.sourceElementType,
+    group.sourceValueType,
+    register.sourceEntryKind,
+    register.sourceElementType,
+    register.sourceValueType
+  ].some(isEnumSourceText)
+}
+
+function enumOptionMatchesValue(option, value) {
+  if (value === null || value === undefined) return false
+  if (typeof value === 'bigint') {
+    const optionValue = Number(option && option.value)
+    if (!Number.isFinite(optionValue)) return false
+
+    return BigInt(Math.trunc(optionValue)) === value
+  }
+
+  return Number(option && option.value) === Number(value)
+}
+
+function validateEnumWriteRegister(group = {}, register = {}, context = {}) {
+  if (!isEnumLikeRegister(group, register)) return true
+
+  const enumOptions = normalizeEnumOptions(register)
+  if (!enumOptions.length) {
+    if (!context.missingEnumWarningShown) {
+      transport.showCommandAlert('枚举写入', '未加载结构体/枚举定义,无法校验枚举值')
+      context.missingEnumWarningShown = true
+    }
+    return true
+  }
+
+  const valueText = getRegisterWriteValueText(register).trim()
+  if (!valueText || valueText === '--') return true
+
+  const enumValue = parseEnumValueText(register, valueText)
+  const parsedValue = enumValue === null
+    ? parseNumberText(valueText, register.dataType)
+    : enumValue
+  const exists = enumOptions.some((option) => enumOptionMatchesValue(option, parsedValue))
+
+  if (exists) return true
+
+  transport.showCommandAlert('枚举写入', '写入值不在枚举定义中,已取消写入')
+  return false
+}
+
+function validateEnumWriteGroup(group = {}) {
+  const context = {
+    missingEnumWarningShown: false
+  }
+  const registers = Array.isArray(group.registers) ? group.registers : []
+
+  return registers.every((register) => validateEnumWriteRegister(group, register, context))
+}
+
 function initParameterGroups() {
   settingsService.init()
   init(getActiveProtocolMode())
@@ -295,6 +371,9 @@ function updateGroupConfig(groupId, config = {}) {
   const protocolMode = getActiveProtocolMode()
   const group = findGroup(groupId)
   if (!group) return null
+  if (isStorageSourceLockedGroup(group)) {
+    return updateGroupPollEnabled(groupId, config.pollEnabled)
+  }
 
   let nextConfig
   try {
@@ -331,6 +410,23 @@ function updateGroupConfig(groupId, config = {}) {
   return updatedGroup
 }
 
+function updateGroupPollEnabled(groupId, pollEnabled) {
+  let updatedGroup = null
+
+  updateGroups((group) => {
+    if (group.id !== groupId) return group
+
+    updatedGroup = normalizeGroup({
+      ...group,
+      pollEnabled: pollEnabled === false ? false : true
+    })
+
+    return updatedGroup
+  })
+
+  return updatedGroup
+}
+
 function setGroupExpanded(groupId, expanded) {
   updateGroups((group) => group.id === groupId
     ? {
@@ -357,7 +453,7 @@ function removeGroup(groupId) {
 function reorderRegister(groupId, fromIndex, toIndex) {
   const group = findGroup(groupId)
   if (!group) return null
-  if (group.isStructLayout) return group
+  if (group.isStructLayout || group.sourceMetadataLocked) return group
 
   const registers = group.registers.slice()
   const sourceIndex = Number(fromIndex)
@@ -387,8 +483,17 @@ function reorderRegister(groupId, fromIndex, toIndex) {
 function updateRegister(groupId, registerIndex, changedData) {
   updateGroups((group) => {
     if (group.id !== groupId) return group
-    const shouldResetReadState = Object.prototype.hasOwnProperty.call(changedData, 'dataType')
-      || Object.prototype.hasOwnProperty.call(changedData, 'textByteLength')
+    const register = group.registers[registerIndex]
+    const safeChangedData = isStorageSourceLockedRegister(group, register)
+      ? Object.keys(changedData || {}).reduce((data, key) => {
+        if (key !== 'name' && key !== 'dataType' && key !== 'textByteLength') {
+          data[key] = changedData[key]
+        }
+        return data
+      }, {})
+      : changedData
+    const shouldResetReadState = Object.prototype.hasOwnProperty.call(safeChangedData, 'dataType')
+      || Object.prototype.hasOwnProperty.call(safeChangedData, 'textByteLength')
 
     return {
       ...group,
@@ -397,7 +502,7 @@ function updateRegister(groupId, registerIndex, changedData) {
           ? {
             ...register,
             ...(shouldResetReadState ? { rawBytes: [], rawValue: null, rawWords: [] } : {}),
-            ...changedData
+            ...safeChangedData
           }
           : register
       ))
@@ -465,6 +570,7 @@ async function writeRegister(groupId, registerIndex) {
     transport.showCommandAlert('参数组写入', `${getUnconfiguredRegisterName(register, registerIndex)} 需要先配置数据类型`)
     return false
   }
+  if (!validateEnumWriteRegister(group, register)) return false
 
   const writeResult = await parameterGroupIo.writeRegister(group, registerIndex, {
     useStorageAccess: isStorageAccessProtocolMode(protocolMode)
@@ -499,6 +605,7 @@ async function writeGroup(groupId, options = {}) {
     transport.showCommandAlert('参数组写入', `${getUnconfiguredRegisterName(unconfigured.register, unconfigured.index)} 需要先配置数据类型`)
     return false
   }
+  if (!validateEnumWriteGroup(group)) return false
 
   const writeResult = await parameterGroupIo.writeGroup(group, {
     ...options,
@@ -539,6 +646,7 @@ module.exports = {
   subscribe,
   syncFromStorageAccessCodeInfo,
   updateGroupConfig,
+  updateGroupPollEnabled,
   updateRegister,
   updateRegisterValue,
   validateRegisterInputValue,

+ 157 - 19
features/parameter-groups/view-model.js

@@ -2,6 +2,9 @@ const parameterGroupService = require('./service.js')
 const {
   validateValueFormula
 } = require('../../domain/parameter-groups/value-formula.js')
+const {
+  normalizeEnumOptions
+} = require('../../domain/parameter-groups/value-codec.js')
 const settingsService = require('../../store/settings-store.js')
 const themeService = require('../../store/theme-store.js')
 const transport = require('../../transport/ble-core.js')
@@ -36,10 +39,85 @@ function validateCodeInfoVariableDataType(dialog = {}, dataType = {}) {
 
   const dataTypeByteLength = getDataTypeConfigByteLength(dataType, dialog)
   if (dataTypeByteLength !== sourceByteLength) {
-    throw new Error(`变量 TLV 长度为 ${sourceByteLength}B,不能选择 ${dataType.label || dataType.key || '该类型'}`)
+    throw new Error(`变量长度为 ${sourceByteLength}B,不能选择 ${dataType.label || dataType.key || '该类型'}`)
   }
 }
 
+function getStorageSourceDisplayText(sourceMemoryArea) {
+  const text = String(sourceMemoryArea || '').trim().toUpperCase()
+  if (text === 'XDATA' || text === 'IDATA' || text === 'DATA' || text === 'CODE') {
+    return text.toLowerCase()
+  }
+
+  return ''
+}
+
+function getStorageSourceTypeText(source = {}) {
+  return String(
+    source.sourceDefinitionName
+      || source.sourceSymbolType
+      || source.sourceValueType
+      || source.sourceElementType
+      || source.dataTypeText
+      || source.dataType
+      || ''
+  ).trim()
+}
+
+function getStorageRegisterTypeText(register = {}) {
+  const entryKind = String(register.sourceEntryKind || '').trim().toLowerCase()
+  const sourceValueType = String(register.sourceValueType || '').trim()
+  const sourceSymbolType = String(register.sourceSymbolType || '').trim()
+  const dataTypeText = String(register.dataTypeText || register.dataType || '').trim()
+  const isStructFieldSource = entryKind === 'struct'
+    || (entryKind === 'array' && sourceValueType === 'struct')
+    || sourceValueType === 'struct'
+
+  return String(
+    register.enumName
+      || (sourceValueType && sourceValueType !== 'struct' ? sourceValueType : '')
+      || (!isStructFieldSource && sourceSymbolType && sourceSymbolType !== 'struct' ? sourceSymbolType : '')
+      || dataTypeText
+  ).trim()
+}
+
+function isEnumTypeSource(source = {}) {
+  const entryKind = String(source.sourceEntryKind || '').trim().toLowerCase()
+  const valueTypes = [
+    source.sourceValueType,
+    source.sourceElementType,
+    source.sourceSymbolType,
+    source.sourceDefinitionName
+  ]
+
+  if (entryKind === 'enum') return true
+  if (String(source.enumName || '').trim()) return true
+  if (normalizeEnumOptions(source).length) return true
+
+  return valueTypes.some((value) => {
+    const text = String(value || '').trim().toLowerCase()
+
+    return text === 'enum' || /^enum\b/.test(text) || /\benum\b/.test(text)
+  })
+}
+
+function getStorageDetailTitle(source = {}) {
+  const entryKind = String(source.sourceEntryKind || '').trim().toLowerCase()
+  const valueType = String(source.sourceValueType || source.sourceElementType || source.sourceSymbolType || '').trim().toLowerCase()
+  if (isEnumTypeSource(source)) return '枚举详情'
+  if (entryKind === 'array') return valueType === 'struct' ? '结构体数组详情' : '数组详情'
+  if (entryKind === 'struct' || valueType === 'struct') return '结构体详情'
+  if (entryKind === 'variable') return '变量详情'
+
+  return '存储详情'
+}
+
+function createEnumOptionRows(register = {}) {
+  return normalizeEnumOptions(register).map((option) => ({
+    text: `${option.label || option.name} = ${Number(option.value)}`
+  }))
+}
+
 function getPageState() {
   const settingsState = settingsService.getState()
   const transportState = transport.getState()
@@ -135,17 +213,31 @@ function createParameterDialogState(overrides = {}) {
     conversionFormula: '',
     conversionFormulaErrorText: '',
     addressText: '',
+    displayName: '',
     displayValue: '',
+    enumExpanded: false,
+    enumOptions: [],
+    isEnumType: false,
     rawValueText: '',
     showDataType: false,
     showRange: false,
     showUnit: false,
     readOnly: false,
+    metadataLocked: false,
     parsedStructRegisters: [],
     pollEnabled: true,
     showPollEnabled: false,
+    sourceAddressText: '',
     sourceByteLength: '',
+    sourceDefinitionName: '',
+    sourceDisplayText: '',
     sourceEntryKind: '',
+    sourceElementType: '',
+    sourceMemoryArea: '',
+    sourceSymbolName: '',
+    sourceSymbolType: '',
+    sourceTypeText: '',
+    sourceValueType: '',
     structDefinition: '',
     structParsedSummary: '',
     ...overrides
@@ -155,64 +247,102 @@ function createParameterDialogState(overrides = {}) {
 function createParameterGroupDialogState(group) {
   const isEdit = !!group
   const isStorageGroup = !!(group && group.sourceMemoryArea)
+  const metadataLocked = !!(group && group.sourceMetadataLocked)
+  const firstRegister = isEdit && Array.isArray(group.registers) ? (group.registers[0] || {}) : {}
   const registerTypeIndex = isEdit ? (group.registerTypeIndex || 0) : 0
   const registerType = getOption(parameterGroupService.REGISTER_TYPE_OPTIONS, registerTypeIndex)
   return createParameterDialogState({
     confirmText: isEdit ? '保存' : '确认',
+    displayName: isEdit ? (group.displayName || group.name) : '',
+    enumOptions: createEnumOptionRows(firstRegister),
     groupId: isEdit ? group.id : '',
     groupName: isEdit ? group.name : (isStorageGroup ? '结构体组' : '寄存器组'),
     layout: isEdit ? (group.layout || 'register') : 'register',
+    metadataLocked,
     mode: isEdit ? 'editGroup' : 'createGroup',
     quantity: isEdit ? String(group.quantity || 1) : '1',
     pollEnabled: isEdit ? group.pollEnabled !== false : true,
     registerTypeIndex,
     registerTypeText: registerType.label || '',
     showPollEnabled: true,
+    sourceAddressText: isEdit ? (group.sourceAddressText || group.startAddressText || '') : '',
+    sourceByteLength: isEdit ? (group.sourceByteLength || group.byteLength || '') : '',
+    sourceDefinitionName: isEdit ? (group.sourceDefinitionName || '') : '',
+    sourceDisplayText: isEdit ? getStorageSourceDisplayText(group.sourceMemoryArea) : '',
+    sourceEntryKind: isEdit ? (group.sourceEntryKind || '') : '',
+    sourceElementType: isEdit ? (group.sourceElementType || '') : '',
+    sourceMemoryArea: isEdit ? (group.sourceMemoryArea || '') : '',
+    sourceMetaText: isEdit ? (group.sourceMetaText || '') : '',
+    sourceSymbolName: isEdit ? (group.sourceSymbolName || '') : '',
+    sourceSymbolType: isEdit ? (group.sourceSymbolType || '') : '',
+    sourceTypeText: isEdit ? getStorageSourceTypeText(group) : '',
+    sourceValueType: isEdit ? (group.sourceValueType || '') : '',
     startAddress: isEdit && group.startAddressText ? group.startAddressText.replace(/^0x/i, '') : '0000',
-    title: isStorageGroup
+    title: metadataLocked
+      ? getStorageDetailTitle(group)
+      : (isStorageGroup
       ? (isEdit ? '编辑结构体组' : '添加结构体组')
-      : (isEdit ? '编辑寄存器组' : '添加寄存器组'),
+      : (isEdit ? '编辑寄存器组' : '添加寄存器组')),
     visible: true
   })
 }
 
 function createParameterRegisterDialogState(mode, group, register, registerIndex) {
   const isView = mode === 'viewRegister'
+  const metadataLocked = !!(register && register.sourceMetadataLocked) || !!(group && group.sourceMetadataLocked)
   const dataTypeIndex = register.dataTypeIndex || 0
   const dataType = getOption(parameterGroupService.DATA_TYPE_OPTIONS, dataTypeIndex)
+  const isEnumType = isEnumTypeSource(register)
+    || isEnumTypeSource(group)
+    || !!(group && group.isStorageEnumScalar)
 
   return createParameterDialogState({
     cancelText: isView ? '关闭' : '取消',
     confirmText: isView ? '' : '保存',
     dataTypeIndex,
     dataTypeText: register.dataTypeText || dataType.label || '',
+    displayName: register.displayName || register.name,
     groupId: group.id,
     groupName: group.name,
+    metadataLocked,
     mode,
     name: register.name,
     registerIndex,
     registerTypeIndex: group.registerTypeIndex || 0,
     remark: register.remark || '',
     startAddress: group.startAddressText ? group.startAddressText.replace(/^0x/i, '') : '0000',
-    title: isView ? '寄存器信息' : '寄存器配置',
     textByteLength: String(register.textByteLength || '32'),
     showTextLength: !!register.showTextLength,
     unit: register.unit || '',
     visible: true,
-    conversionFormula: register.conversionFormula || '',
-    conversionFormulaErrorText: register.conversionFormulaErrorText || '',
-    maxValue: register.maxValue || '',
-    minValue: register.minValue || '',
+    conversionFormula: isEnumType ? '' : (register.conversionFormula || ''),
+    conversionFormulaErrorText: isEnumType ? '' : (register.conversionFormulaErrorText || ''),
+    maxValue: isEnumType ? '' : (register.maxValue || ''),
+    minValue: isEnumType ? '' : (register.minValue || ''),
+    enumOptions: createEnumOptionRows(register),
+    isEnumType,
     addressText: register.addressRangeText || register.addressText || '',
     displayValue: register.displayValue || '',
     rawValueText: register.rawValueText || '--',
+    sourceAddressText: register.sourceAddressText || register.addressText || '',
     sourceByteLength: register.sourceByteLength || '',
+    sourceDefinitionName: register.sourceDefinitionName || '',
+    sourceDisplayText: getStorageSourceDisplayText(register.sourceMemoryArea || group.sourceMemoryArea),
     sourceEntryKind: register.sourceEntryKind || '',
+    sourceElementType: register.sourceElementType || '',
+    sourceMemoryArea: register.sourceMemoryArea || group.sourceMemoryArea || '',
     sourceMetaText: register.sourceMetaText || '',
+    sourceSymbolName: register.sourceSymbolName || '',
+    sourceSymbolType: register.sourceSymbolType || '',
+    sourceTypeText: getStorageRegisterTypeText(register),
+    sourceValueType: register.sourceValueType || '',
     showDataType: !!register.showDataType,
-    showRange: !!register.showRange,
-    showUnit: !!register.showUnit,
-    readOnly: isView
+    showRange: !isEnumType && !!register.showRange,
+    showPollEnabled: metadataLocked && isEnumType,
+    showUnit: !isEnumType && !!register.showUnit,
+    pollEnabled: group.pollEnabled !== false,
+    readOnly: isView,
+    title: metadataLocked ? getStorageDetailTitle(register) : (isView ? '寄存器信息' : '寄存器配置')
   })
 }
 
@@ -265,21 +395,29 @@ function createParameterGroupConfig(dialog) {
 function createParameterRegisterChangedData(dialog, dataTypeOptions) {
   const dataType = getOption(dataTypeOptions, dialog.dataTypeIndex)
   const isTextType = dataType.kind === 'text'
-  const showUnit = dataType.kind === 'number' && dataType.key !== 'hex'
-  const conversionFormula = String(dialog.conversionFormula || '').trim()
+  const isEnumType = !!dialog.isEnumType
+  const showUnit = !isEnumType && dataType.kind === 'number' && dataType.key !== 'hex'
+  const conversionFormula = isEnumType ? '' : String(dialog.conversionFormula || '').trim()
 
   validateValueFormula(conversionFormula)
+
+  const changedData = {
+    conversionFormula,
+    maxValue: isTextType || isEnumType ? '' : dialog.maxValue,
+    minValue: isTextType || isEnumType ? '' : dialog.minValue,
+    remark: dialog.remark,
+    unit: showUnit ? dialog.unit : ''
+  }
+
+  if (dialog.metadataLocked) return changedData
+
   validateCodeInfoVariableDataType(dialog, dataType)
 
   return {
+    ...changedData,
     name: dialog.name,
-    conversionFormula,
     dataType: dataType.key,
-    maxValue: isTextType ? '' : dialog.maxValue,
-    minValue: isTextType ? '' : dialog.minValue,
-    remark: dialog.remark,
-    textByteLength: isTextType ? dialog.textByteLength : '',
-    unit: showUnit ? dialog.unit : ''
+    textByteLength: isTextType ? dialog.textByteLength : ''
   }
 }
 

+ 37 - 2
pages/params/params.js

@@ -1,5 +1,6 @@
 const {
   createParameterDialogState,
+  createParameterRegisterDialogState,
   findParameterGroup,
   findParameterRegister,
   getActiveParameterGroup,
@@ -40,6 +41,22 @@ function isReadableParameterGroup(group = {}, data = {}) {
   return !!group.functionCode
 }
 
+function isStorageScalarEntryKind(group = {}) {
+  const entryKind = String(group.sourceEntryKind || '').trim().toLowerCase()
+
+  return entryKind === 'enum' || entryKind === 'variable'
+}
+
+function isStorageNavigableGroup(group = {}) {
+  if (!group || !group.sourceMemoryArea) return false
+  if (isStorageScalarEntryKind(group)) return false
+
+  return group.sourceEntryKind === 'array'
+    || group.sourceEntryKind === 'struct'
+    || group.isStructLayout
+    || (Array.isArray(group.registers) && group.registers.length > 1)
+}
+
 Page({
   data: {
     ...getPageState(),
@@ -190,13 +207,15 @@ Page({
     const groupId = event.currentTarget.dataset.groupId
     const group = findParameterGroup(getParameterGroupsFromState(this.data), groupId)
     if (!group) return
-    if (group.isStorageScalarGroup) return
 
     if (this.parameterGroupLongPressGuard === groupId) {
       this.parameterGroupLongPressGuard = ''
       return
     }
 
+    if (this.data.isStorageAccessProtocol && !isStorageNavigableGroup(group)) return
+    if (!this.data.isStorageAccessProtocol && group.sourceMetadataLocked) return
+
     if (this.pageToast) this.pageToast.clear()
     this.closeParameterDraft()
 
@@ -226,6 +245,22 @@ Page({
     })
   },
 
+  openStorageParameterGroupDetail(event) {
+    if (!this.data.isStorageAccessProtocol) return
+
+    const groupId = event.currentTarget.dataset.groupId
+    const group = findParameterGroup(getParameterGroupsFromState(this.data), groupId)
+    if (!group || !group.sourceMemoryArea) return
+
+    if (group.isStorageScalarGroup) {
+      this.openStorageScalarRegisterEdit(event)
+      return
+    }
+
+    this.parameterGroupLongPressGuard = groupId
+    this.openParameterGroupEdit(event)
+  },
+
   backToParamsHome() {
     if (this.pageToast) this.pageToast.clear()
     this.closeParameterDraft()
@@ -313,7 +348,7 @@ Page({
       this.scheduleParameterAutoPoll(this.data.parameterPollInterval || 100)
     }
     if (ok && this.pageToast) {
-      this.pageToast.show(`${register.name || '变量'}已写入`)
+      this.pageToast.show(`${register.displayName || register.name || '变量'}已写入`)
     }
   },
 

+ 189 - 103
pages/params/params.wxml

@@ -50,23 +50,38 @@
           data-group-id="{{group.id}}"
           bindtouchstart="onParameterGroupTouchStart"
           bindtouchend="onParameterGroupTouchEnd"
+          bindlongpress="openStorageParameterGroupDetail"
         >
           <view class="panel-header panel-header--with-actions">
             <block wx:if="{{group.isStorageScalarGroup}}">
-              <view class="panel-heading-toggle generic-scalar-heading">
+              <view
+                class="panel-heading-toggle generic-scalar-heading"
+                data-group-id="{{group.id}}"
+                bindtap="openParameterGroup"
+              >
                 <view class="panel-icon icon-terminal">
                   <image class="panel-icon-image" src="/assets/icons/terminal-white.png" mode="aspectFit" />
                 </view>
                 <view class="generic-group-title-wrap">
-                  <view
-                    class="panel-title"
-                    data-group-id="{{group.id}}"
-                    catchlongpress="openStorageScalarRegisterEdit"
-                  >{{group.displayName || group.name}}</view>
-                  <view class="param-meta generic-group-meta">{{group.listMetaText || group.addressRangeText}}</view>
+                  <view class="panel-title" data-group-id="{{group.id}}">{{group.displayName || group.name}}</view>
+                  <view class="param-meta generic-group-meta">{{group.storageCardMetaText || group.addressRangeText}}</view>
                 </view>
               </view>
-              <view class="generic-scalar-value">{{group.scalarValueText || '--'}}</view>
+              <block wx:if="{{group.isStorageEnumScalar}}">
+                <view class="generic-register-input-wrap generic-scalar-input-wrap">
+                  <input
+                    class="value-input generic-register-value {{group.registers[0].isDirty ? 'value-input--dirty' : ''}}"
+                    data-group-id="{{group.id}}"
+                    data-index="0"
+                    value="{{group.scalarInputValueText}}"
+                    catchtap="noop"
+                    bindinput="onParameterRegisterValueInput"
+                    bindblur="onParameterRegisterValueBlur"
+                    catchlongpress="noop"
+                  />
+                </view>
+              </block>
+              <view wx:else class="generic-scalar-value">{{group.scalarValueText || ''}}</view>
             </block>
             <block wx:else>
               <view
@@ -79,7 +94,12 @@
                 </view>
                 <view class="generic-group-title-wrap">
                   <view class="panel-title" data-group-id="{{group.id}}" catchlongpress="openParameterGroupEdit">{{group.displayName || group.name}}</view>
-                  <view class="param-meta generic-group-meta">{{group.listMetaText || group.addressRangeText}}</view>
+                  <block wx:if="{{isStorageAccessProtocol}}">
+                    <view class="param-meta generic-group-meta">{{group.storageCardMetaText || group.addressRangeText}}</view>
+                  </block>
+                  <block wx:else>
+                    <view wx:if="{{group.listMetaText || !group.sourceMetadataLocked}}" class="param-meta generic-group-meta">{{group.listMetaText || group.addressRangeText}}</view>
+                  </block>
                 </view>
               </view>
               <view wx:if="{{isModbusProtocol}}" class="panel-actions generic-group-actions">
@@ -119,10 +139,10 @@
                   bindtap="openParameterRegisterInfo"
                   catchlongpress="openParameterRegisterEdit"
                 >
-                  {{register.name}}
+                  {{register.displayName || register.name}}
                 </view>
-                <view class="generic-register-meta">
-                  <text>{{register.metaText || (register.addressText + ' ' + register.rawValueText)}}</text>
+                <view wx:if="{{register.metaText || !register.sourceMetadataLocked}}" class="generic-register-meta">
+                  <text>{{register.metaText || (register.addressText + (register.rawValueText ? ' ' + register.rawValueText : ''))}}</text>
                 </view>
               </view>
               <view class="generic-register-input-wrap {{register.showUnit && register.unit ? 'generic-register-input-wrap--unit' : ''}}">
@@ -157,7 +177,7 @@
         <view class="generic-group-detail-header">
           <view class="panel-title">{{activeParameterGroup.detailTitleText || activeParameterGroup.displayName || activeParameterGroup.name}}</view>
         </view>
-        <view class="generic-group-detail-meta">
+        <view wx:if="{{activeParameterGroup.detailMetaText || !activeParameterGroup.sourceMetadataLocked}}" class="generic-group-detail-meta">
           {{activeParameterGroup.detailMetaText || activeParameterGroup.addressRangeText}}
         </view>
         <view
@@ -169,7 +189,7 @@
           style="{{register.dragStyle}}"
         >
           <view
-            wx:if="{{!activeParameterGroup.isStructLayout}}"
+            wx:if="{{!activeParameterGroup.isStructLayout && !activeParameterGroup.sourceMetadataLocked}}"
             class="generic-register-drag-handle {{register.dragHandleClass}}"
             data-group-id="{{activeParameterGroup.id}}"
             data-index="{{register.sourceIndex !== undefined ? register.sourceIndex : registerIndex}}"
@@ -191,10 +211,10 @@
               bindtap="openParameterRegisterInfo"
               catchlongpress="openParameterRegisterEdit"
             >
-              {{register.name}}
+              {{register.displayName || register.name}}
             </view>
-            <view class="generic-register-meta">
-              <text>{{register.metaText || (register.addressText + ' ' + register.rawValueText)}}</text>
+            <view wx:if="{{register.metaText || !register.sourceMetadataLocked}}" class="generic-register-meta">
+              <text>{{register.metaText || (register.addressText + (register.rawValueText ? ' ' + register.rawValueText : ''))}}</text>
             </view>
           </view>
           <view class="generic-register-input-wrap {{register.showUnit && register.unit ? 'generic-register-input-wrap--unit' : ''}}">
@@ -232,87 +252,135 @@
 
     <block wx:if="{{parameterDialog.mode == 'createGroup' || parameterDialog.mode == 'editGroup'}}">
       <view class="generic-dialog-body">
-        <view class="generic-config-row">
-          <view class="param-main">
-            <view class="param-name">{{isStorageAccessProtocol ? '结构体组名' : '寄存器组名'}}</view>
-            <view class="param-meta">{{isStorageAccessProtocol ? '内存地址连续' : '每组寄存器地址连续'}}</view>
+        <block wx:if="{{parameterDialog.metadataLocked}}">
+          <view class="generic-info-stack">
+            <view class="generic-info-row">
+              <view class="generic-info-label">名称</view>
+              <view class="generic-info-value">{{parameterDialog.groupName}}</view>
+            </view>
+            <view wx:if="{{parameterDialog.sourceDisplayText}}" class="generic-info-row">
+              <view class="generic-info-label">来源</view>
+              <view class="generic-info-value">{{parameterDialog.sourceDisplayText}}</view>
+            </view>
+            <view wx:if="{{parameterDialog.sourceAddressText}}" class="generic-info-row">
+              <view class="generic-info-label">起始地址</view>
+              <view class="generic-info-value">{{parameterDialog.sourceAddressText}}</view>
+            </view>
+            <view wx:if="{{parameterDialog.sourceByteLength}}" class="generic-info-row">
+              <view class="generic-info-label">长度</view>
+              <view class="generic-info-value">{{parameterDialog.sourceByteLength}}B</view>
+            </view>
+            <view wx:if="{{parameterDialog.sourceTypeText}}" class="generic-info-row">
+              <view class="generic-info-label">类型</view>
+              <view class="generic-info-value">{{parameterDialog.sourceTypeText}}</view>
+            </view>
+            <view wx:if="{{parameterDialog.enumOptions.length}}" class="generic-info-row generic-info-row--toggle" bindtap="toggleParameterEnumOptions">
+              <view class="generic-info-label">枚举</view>
+              <view class="generic-info-value generic-info-value--toggle">
+                <text>{{parameterDialog.enumExpanded ? '收起' : '展开'}}</text>
+                <view class="entry-chevron {{parameterDialog.enumExpanded ? 'is-expanded' : ''}}"></view>
+              </view>
+            </view>
+            <view wx:if="{{parameterDialog.enumOptions.length && parameterDialog.enumExpanded}}" class="generic-info-row generic-info-row--enum-list">
+              <view class="generic-info-label"></view>
+              <scroll-view class="generic-info-value generic-info-value--enum-list" scroll-y>
+                <view wx:for="{{parameterDialog.enumOptions}}" wx:for-item="option" wx:key="text">{{option.text}}</view>
+              </scroll-view>
+            </view>
+            <view wx:if="{{parameterDialog.showPollEnabled}}" class="generic-info-row">
+              <view class="generic-info-label">参与轮询</view>
+              <switch
+                checked="{{parameterDialog.pollEnabled}}"
+                color="#0f766e"
+                data-field="pollEnabled"
+                bindchange="onParameterDraftSwitchChange"
+              />
+            </view>
           </view>
-          <input
-            class="value-input generic-value-input"
-            data-field="groupName"
-            value="{{parameterDialog.groupName}}"
-            bindinput="onParameterDraftInput"
-          />
-        </view>
-        <view wx:if="{{isModbusProtocol}}" class="generic-config-row">
-          <view class="param-main">
-            <view class="param-name">寄存器类型</view>
-            <view class="param-meta">决定读取功能码与是否可写</view>
+        </block>
+        <block wx:else>
+          <view class="generic-config-row">
+            <view class="param-main">
+              <view class="param-name">{{isStorageAccessProtocol ? '结构体组名' : '寄存器组名'}}</view>
+              <view class="param-meta">{{isStorageAccessProtocol ? '内存地址连续' : '每组寄存器地址连续'}}</view>
+            </view>
+            <input
+              class="value-input generic-value-input"
+              data-field="groupName"
+              value="{{parameterDialog.groupName}}"
+              bindinput="onParameterDraftInput"
+            />
           </view>
-          <picker
-            mode="selector"
-            range="{{parameterRegisterTypeOptions}}"
-            range-key="label"
-            value="{{parameterDialog.registerTypeIndex}}"
-            bindchange="onParameterDraftTypeChange"
-          >
-            <view class="generic-picker-value">{{parameterDialog.registerTypeText}}</view>
-          </picker>
-        </view>
-        <view class="generic-config-row">
-          <view class="param-main">
-            <view class="param-name">{{isStorageAccessProtocol ? '内存起始地址' : '寄存器起始地址'}}</view>
-            <view class="param-meta">{{isStorageAccessProtocol ? '16进制,最多 8 位,例如 000000A0' : '16进制,例如 00A0'}}</view>
+          <view wx:if="{{isModbusProtocol}}" class="generic-config-row">
+            <view class="param-main">
+              <view class="param-name">寄存器类型</view>
+              <view class="param-meta">决定读取功能码与是否可写</view>
+            </view>
+            <picker
+              mode="selector"
+              range="{{parameterRegisterTypeOptions}}"
+              range-key="label"
+              value="{{parameterDialog.registerTypeIndex}}"
+              bindchange="onParameterDraftTypeChange"
+            >
+              <view class="generic-picker-value">{{parameterDialog.registerTypeText}}</view>
+            </picker>
           </view>
-          <input
-            class="value-input generic-value-input"
-            data-field="startAddress"
-            value="{{parameterDialog.startAddress}}"
-            bindinput="onParameterDraftInput"
-          />
-        </view>
-        <view class="generic-config-row">
-          <view class="param-main">
-            <view class="param-name">{{isStorageAccessProtocol ? '字节长度' : '寄存器数量'}}</view>
-            <view class="param-meta">{{parameterDialog.structParsedSummary || '1 - 256'}}</view>
+          <view class="generic-config-row">
+            <view class="param-main">
+              <view class="param-name">{{isStorageAccessProtocol ? '内存起始地址' : '寄存器起始地址'}}</view>
+              <view class="param-meta">{{isStorageAccessProtocol ? '16进制,最多 8 位,例如 000000A0' : '16进制,例如 00A0'}}</view>
+            </view>
+            <input
+              class="value-input generic-value-input"
+              data-field="startAddress"
+              value="{{parameterDialog.startAddress}}"
+              bindinput="onParameterDraftInput"
+            />
           </view>
-          <input
-            class="value-input generic-value-input"
-            type="number"
-            data-field="quantity"
-            value="{{parameterDialog.quantity}}"
-            bindinput="onParameterDraftInput"
-          />
-        </view>
-        <view wx:if="{{parameterDialog.showPollEnabled}}" class="generic-config-row">
-          <view class="param-main">
-            <view class="param-name">参与轮询</view>
-            <view class="param-meta">自动轮询</view>
+          <view class="generic-config-row">
+            <view class="param-main">
+              <view class="param-name">{{isStorageAccessProtocol ? '字节长度' : '寄存器数量'}}</view>
+              <view class="param-meta">{{parameterDialog.structParsedSummary || '1 - 256'}}</view>
+            </view>
+            <input
+              class="value-input generic-value-input"
+              type="number"
+              data-field="quantity"
+              value="{{parameterDialog.quantity}}"
+              bindinput="onParameterDraftInput"
+            />
           </view>
-          <switch
-            checked="{{parameterDialog.pollEnabled}}"
-            color="#0f766e"
-            data-field="pollEnabled"
-            bindchange="onParameterDraftSwitchChange"
-          />
-        </view>
-        <view wx:if="{{parameterDialog.mode == 'createGroup'}}" class="generic-struct-section">
-          <view class="generic-struct-header">
+          <view wx:if="{{parameterDialog.showPollEnabled}}" class="generic-config-row">
             <view class="param-main">
-              <view class="param-name">结构体/枚举定义</view>
-              <view class="param-meta">支持 typedef struct、typedef enum、typedef 别名与数组</view>
+              <view class="param-name">参与轮询</view>
+              <view class="param-meta">自动轮询</view>
             </view>
-            <view class="panel-action-button" bindtap="parseParameterStructDefinition">解析</view>
+            <switch
+              checked="{{parameterDialog.pollEnabled}}"
+              color="#0f766e"
+              data-field="pollEnabled"
+              bindchange="onParameterDraftSwitchChange"
+            />
           </view>
-          <textarea
-            class="generic-struct-input"
-            maxlength="-1"
-            placeholder="粘贴 C 结构体/枚举定义"
-            data-field="structDefinition"
-            value="{{parameterDialog.structDefinition}}"
-            bindinput="onParameterDraftInput"
-          />
-        </view>
+          <view wx:if="{{parameterDialog.mode == 'createGroup'}}" class="generic-struct-section">
+            <view class="generic-struct-header">
+              <view class="param-main">
+                <view class="param-name">结构体/枚举定义</view>
+                <view class="param-meta">支持 typedef struct、typedef enum、typedef 别名与数组</view>
+              </view>
+              <view class="panel-action-button" bindtap="parseParameterStructDefinition">解析</view>
+            </view>
+            <textarea
+              class="generic-struct-input"
+              maxlength="-1"
+              placeholder="粘贴 C 结构体/枚举定义"
+              data-field="structDefinition"
+              value="{{parameterDialog.structDefinition}}"
+              bindinput="onParameterDraftInput"
+            />
+          </view>
+        </block>
       </view>
     </block>
 
@@ -321,31 +389,40 @@
         <view class="generic-info-stack">
           <view class="generic-info-row">
             <view class="generic-info-label">名称</view>
-            <view wx:if="{{parameterDialog.mode == 'viewRegister'}}" class="generic-info-value">{{parameterDialog.name}}</view>
+            <view wx:if="{{parameterDialog.mode == 'viewRegister' || parameterDialog.metadataLocked}}" class="generic-info-value">{{parameterDialog.name}}</view>
             <input wx:else class="value-input generic-value-input" data-field="name" value="{{parameterDialog.name}}" bindinput="onParameterDraftInput" />
           </view>
           <view class="generic-info-row">
             <view class="generic-info-label">地址</view>
             <view class="generic-info-value">{{parameterDialog.addressText}}</view>
           </view>
-          <view wx:if="{{parameterDialog.sourceMetaText}}" class="generic-info-row">
-            <view class="generic-info-label">来源</view>
-            <view class="generic-info-value">{{parameterDialog.sourceMetaText}}</view>
-          </view>
           <view wx:if="{{parameterDialog.sourceByteLength}}" class="generic-info-row">
-            <view class="generic-info-label">TLV长度</view>
+            <view class="generic-info-label">长度</view>
             <view class="generic-info-value">{{parameterDialog.sourceByteLength}}B</view>
           </view>
           <view wx:if="{{parameterDialog.showDataType}}" class="generic-info-row">
             <view class="generic-info-label">类型</view>
-            <view wx:if="{{parameterDialog.mode == 'viewRegister'}}" class="generic-info-value">{{parameterDialog.dataTypeText}}</view>
+            <view wx:if="{{parameterDialog.mode == 'viewRegister' || parameterDialog.metadataLocked}}" class="generic-info-value">{{parameterDialog.sourceTypeText || parameterDialog.dataTypeText}}</view>
             <picker wx:else mode="selector" range="{{parameterDataTypeOptions}}" range-key="label" value="{{parameterDialog.dataTypeIndex}}" bindchange="onParameterDialogDataTypeChange">
               <view class="generic-picker-value">{{parameterDialog.dataTypeText}}</view>
             </picker>
           </view>
+          <view wx:if="{{parameterDialog.enumOptions.length}}" class="generic-info-row generic-info-row--toggle" bindtap="toggleParameterEnumOptions">
+            <view class="generic-info-label">枚举</view>
+            <view class="generic-info-value generic-info-value--toggle">
+              <text>{{parameterDialog.enumExpanded ? '收起' : '展开'}}</text>
+              <view class="entry-chevron {{parameterDialog.enumExpanded ? 'is-expanded' : ''}}"></view>
+            </view>
+          </view>
+          <view wx:if="{{parameterDialog.enumOptions.length && parameterDialog.enumExpanded}}" class="generic-info-row generic-info-row--enum-list">
+            <view class="generic-info-label"></view>
+            <scroll-view class="generic-info-value generic-info-value--enum-list" scroll-y>
+              <view wx:for="{{parameterDialog.enumOptions}}" wx:for-item="option" wx:key="text">{{option.text}}</view>
+            </scroll-view>
+          </view>
           <view wx:if="{{parameterDialog.showTextLength}}" class="generic-info-row">
             <view class="generic-info-label">长度</view>
-            <view wx:if="{{parameterDialog.mode == 'viewRegister'}}" class="generic-info-value">{{parameterDialog.textByteLength || '--'}}B</view>
+            <view wx:if="{{parameterDialog.mode == 'viewRegister' || parameterDialog.metadataLocked}}" class="generic-info-value">{{parameterDialog.textByteLength || '--'}}B</view>
             <input wx:else class="value-input generic-value-input" type="number" data-field="textByteLength" value="{{parameterDialog.textByteLength}}" bindinput="onParameterDraftInput" />
           </view>
           <view class="generic-info-row">
@@ -353,26 +430,35 @@
             <view wx:if="{{parameterDialog.mode == 'viewRegister'}}" class="generic-info-value">{{parameterDialog.remark || '--'}}</view>
             <input wx:else class="value-input generic-value-input" data-field="remark" value="{{parameterDialog.remark}}" bindinput="onParameterDraftInput" />
           </view>
-          <view wx:if="{{parameterDialog.showUnit}}" class="generic-info-row">
+          <view wx:if="{{parameterDialog.showPollEnabled}}" class="generic-info-row">
+            <view class="generic-info-label">参与轮询</view>
+            <switch
+              checked="{{parameterDialog.pollEnabled}}"
+              color="#0f766e"
+              data-field="pollEnabled"
+              bindchange="onParameterDraftSwitchChange"
+            />
+          </view>
+          <view wx:if="{{!parameterDialog.isEnumType && parameterDialog.showUnit}}" class="generic-info-row">
             <view class="generic-info-label">单位</view>
             <view wx:if="{{parameterDialog.mode == 'viewRegister'}}" class="generic-info-value">{{parameterDialog.unit || '--'}}</view>
             <input wx:else class="value-input generic-value-input" data-field="unit" value="{{parameterDialog.unit}}" bindinput="onParameterDraftInput" />
           </view>
-          <view wx:if="{{parameterDialog.showDataType}}" class="generic-info-row">
+          <view wx:if="{{!parameterDialog.isEnumType && parameterDialog.showDataType}}" class="generic-info-row">
             <view class="generic-info-label">公式</view>
             <view wx:if="{{parameterDialog.mode == 'viewRegister'}}" class="generic-info-value">{{parameterDialog.conversionFormula || '--'}}</view>
             <input wx:else class="value-input generic-value-input" data-field="conversionFormula" value="{{parameterDialog.conversionFormula}}" placeholder="x" bindinput="onParameterDraftInput" />
           </view>
-          <view wx:if="{{parameterDialog.conversionFormulaErrorText}}" class="generic-info-row">
+          <view wx:if="{{!parameterDialog.isEnumType && parameterDialog.conversionFormulaErrorText}}" class="generic-info-row">
             <view class="generic-info-label">公式</view>
             <view class="generic-info-value">{{parameterDialog.conversionFormulaErrorText}}</view>
           </view>
-          <view wx:if="{{parameterDialog.mode == 'viewRegister' || parameterDialog.showRange}}" class="generic-info-row">
+          <view wx:if="{{!parameterDialog.isEnumType && (parameterDialog.mode == 'viewRegister' || parameterDialog.showRange)}}" class="generic-info-row">
             <view class="generic-info-label">最小值</view>
             <view wx:if="{{parameterDialog.mode == 'viewRegister'}}" class="generic-info-value">{{parameterDialog.minValue || '--'}}</view>
             <input wx:else class="value-input generic-value-input" data-field="minValue" value="{{parameterDialog.minValue}}" bindinput="onParameterDraftInput" />
           </view>
-          <view wx:if="{{parameterDialog.mode == 'viewRegister' || parameterDialog.showRange}}" class="generic-info-row">
+          <view wx:if="{{!parameterDialog.isEnumType && (parameterDialog.mode == 'viewRegister' || parameterDialog.showRange)}}" class="generic-info-row">
             <view class="generic-info-label">最大值</view>
             <view wx:if="{{parameterDialog.mode == 'viewRegister'}}" class="generic-info-value">{{parameterDialog.maxValue || '--'}}</view>
             <input wx:else class="value-input generic-value-input" data-field="maxValue" value="{{parameterDialog.maxValue}}" bindinput="onParameterDraftInput" />