7 天以前 e522a99b9144bb7b299dc2a90844817e4dcad8d6
chore(config): 更新测试环境API端口配置

- 将VITE_BASE_API从9003端口更新为9048端口
- 将VITE_JAVA_API从9002端口更新为9049端口
- 保持其他测试环境配置不变
已修改1个文件
662 ■■■■■ 文件已修改
src/components/AIChatSidebar/index.vue 662 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AIChatSidebar/index.vue
@@ -254,14 +254,141 @@
                    </ul>
                  </div>
                  <div class="purchase-section-title">补充或确认数据</div>
                  <el-input
                      v-model="message.payloadText"
                      type="textarea"
                      :rows="8"
                      resize="vertical"
                      spellcheck="false"
                      class="payload-editor"
                  />
                  <div class="payload-toolbar">
                    <el-button
                        size="small"
                        plain
                        :disabled="message.confirming || message.confirmed"
                        @click="addPurchaseRootField(message)"
                    >
                      <el-icon><Plus /></el-icon>
                      新增顶层字段
                    </el-button>
                  </div>
                  <div class="payload-tree-table-wrapper">
                    <el-table
                        :data="message.payloadTreeData || []"
                        row-key="id"
                        border
                        stripe
                        size="small"
                        default-expand-all
                        :tree-props="{ children: 'children' }"
                        empty-text="暂无待确认数据"
                    >
                      <el-table-column label="字段" min-width="240">
                        <template #default="{ row }">
                          <div class="payload-key-cell">
                            <template v-if="row.parentType === 'object'">
                              <el-input
                                  v-if="row.keyEditable"
                                  v-model="row.key"
                                  size="small"
                                  :disabled="message.confirming || message.confirmed"
                                  placeholder="字段名"
                              />
                              <div v-else class="payload-fixed-key" :title="row.key">
                                <span>{{ getPurchaseFieldLabel(row.key) }}</span>
                                <small v-if="getPurchaseFieldLabel(row.key) !== row.key">{{ row.key }}</small>
                              </div>
                            </template>
                            <span v-else class="payload-array-index">{{ getPurchaseArrayItemLabel(row, message) }}</span>
                          </div>
                        </template>
                      </el-table-column>
                      <el-table-column label="类型" width="130" align="center">
                        <template #default="{ row }">
                          <el-select
                              v-model="row.valueType"
                              size="small"
                              :disabled="message.confirming || message.confirmed"
                              @change="handlePurchaseNodeTypeChange(message, row)"
                          >
                            <el-option
                                v-for="option in purchaseValueTypeOptions"
                                :key="option.value"
                                :label="option.label"
                                :value="option.value"
                            />
                          </el-select>
                        </template>
                      </el-table-column>
                      <el-table-column label="值" min-width="250">
                        <template #default="{ row }">
                          <div v-if="row.valueType === 'object'" class="payload-container-cell">
                            对象({{ row.children?.length || 0 }})
                          </div>
                          <div v-else-if="row.valueType === 'array'" class="payload-container-cell">
                            数组({{ row.children?.length || 0 }})
                          </div>
                          <el-switch
                              v-else-if="row.valueType === 'boolean'"
                              v-model="row.value"
                              size="small"
                              :disabled="message.confirming || message.confirmed"
                          />
                          <span v-else-if="row.valueType === 'null'" class="payload-null-value">null</span>
                          <el-input
                              v-else
                              v-model="row.value"
                              size="small"
                              :placeholder="row.valueType === 'number' ? '请输入数字' : '请输入内容'"
                              :disabled="message.confirming || message.confirmed"
                          />
                        </template>
                      </el-table-column>
                      <el-table-column label="操作" width="180" align="center">
                        <template #default="{ row }">
                          <div class="payload-row-actions">
                            <el-tooltip v-if="row.valueType === 'object'" content="新增字段" placement="top">
                              <el-button
                                  :icon="Plus"
                                  circle
                                  size="small"
                                  text
                                  type="primary"
                                  :disabled="message.confirming || message.confirmed"
                                  @click="addPurchaseChildNode(message, row)"
                              />
                            </el-tooltip>
                            <el-tooltip v-else-if="row.valueType === 'array'" content="新增数组项" placement="top">
                              <el-button
                                  :icon="Plus"
                                  circle
                                  size="small"
                                  text
                                  type="primary"
                                  :disabled="message.confirming || message.confirmed"
                                  @click="addPurchaseChildNode(message, row)"
                              />
                            </el-tooltip>
                            <el-tooltip v-if="row.parentType === 'array'" content="新增同级项" placement="top">
                              <el-button
                                  :icon="Plus"
                                  circle
                                  size="small"
                                  text
                                  type="primary"
                                  :disabled="message.confirming || message.confirmed"
                                  @click="addPurchaseSiblingNode(message, row)"
                              />
                            </el-tooltip>
                            <el-tooltip content="删除当前项" placement="top">
                              <el-button
                                  :icon="Delete"
                                  circle
                                  size="small"
                                  text
                                  type="danger"
                                  :disabled="message.confirming || message.confirmed"
                                  @click="removePurchaseNode(message, row)"
                              />
                            </el-tooltip>
                          </div>
                        </template>
                      </el-table-column>
                    </el-table>
                  </div>
                  <div class="payload-editor-tip">
                    日期请填写 yyyy-MM-dd,例如 2026-04-30。产品明细建议放在每条采购台账的 productData 中,确认时会自动兼容旧格式并清理审批字段。
                  </div>
@@ -274,7 +401,7 @@
                        size="small"
                        :loading="message.confirming"
                        :disabled="message.confirmed || isSending"
                        @click="confirmPurchaseAnalysis(message)"
                        @click="confirmPurchaseAnalysisFromTable(message)"
                    >
                      确认并执行
                    </el-button>
@@ -453,8 +580,8 @@
const windowWidth = ref(window.innerWidth)
const drawerSize = computed(() => {
  if (windowWidth.value < 768) return '100%'
  if (windowWidth.value < 1200) return '500px'
  return '600px'
  if (windowWidth.value < 1200) return '50%'
  return '50%'
})
const messageListRef = ref(null)
const isSending = ref(false)
@@ -535,7 +662,8 @@
  totalPriceWithTax: '含税总价',
  invoiceType: '发票类型',
  inventoryWarningQuantity: '库存预警数量',
  isInspected: '是否质检'
  isInspected: '是否质检',
  isChecked: '是否质检'
}
const purchasePayloadFieldKeyMap = {
  采购台账: 'purchaseLedgers',
@@ -621,10 +749,45 @@
  taxInclusiveTotalPrice: 'taxInclusiveTotalPrice',
  invoiceType: 'invoiceType',
  inventoryWarningQuantity: 'inventoryWarningQuantity',
  isInspected: 'isInspected'
  isInspected: 'isInspected',
  isChecked: 'isInspected'
}
// 历史会话相关
const purchaseValueTypeOptions = [
  { label: '文本', value: 'string' },
  { label: '数字', value: 'number' },
  { label: '布尔', value: 'boolean' },
  { label: '空值', value: 'null' },
  { label: '对象', value: 'object' },
  { label: '数组', value: 'array' }
]
const purchaseContainerValueTypes = new Set(['object', 'array'])
const purchaseHiddenFieldKeySet = new Set(['templatename', 'approvalstatus', 'phonenumber', 'type'])
const purchaseHiddenKeyWordList = [
  'attachment',
  'file',
  'invoice',
  'ticketregistration',
  'receiptpayment',
  'payment'
]
const purchaseHiddenChineseKeywordList = ['附件', '开票', '来票', '回款', '付款']
let purchasePayloadTreeNodeSeed = 0
const shouldHidePurchaseField = (fieldKey = '') => {
  const rawKey = String(fieldKey || '')
  if (!rawKey) return false
  const normalizedFieldKey = purchasePayloadFieldKeyMap[rawKey] || rawKey
  const lowerKey = String(normalizedFieldKey).toLowerCase()
  if (lowerKey.endsWith('id') || lowerKey.endsWith('ids')) return true
  if (purchaseHiddenFieldKeySet.has(lowerKey)) return true
  if (purchaseHiddenKeyWordList.some(keyword => lowerKey.includes(keyword))) return true
  if (purchaseHiddenChineseKeywordList.some(keyword => rawKey.includes(keyword))) return true
  return false
}
const showHistory = ref(false)
const sessions = ref([])
const loadingSessions = ref(false)
@@ -696,7 +859,9 @@
          chartOptions: null,
          chartRenderReady: false,
          type: '',
          tableData: null
          tableData: null,
          payloadTreeData: null,
          payloadHiddenData: null
        }
        messages.value.push(messageObj)
@@ -931,8 +1096,13 @@
  if (parsedData.action === 'confirm_required' && parsedData.businessType) {
    messageObj.type = 'purchase_analysis_confirm'
    messageObj.purchaseAnalysisData = parsedData
    if (!Array.isArray(messageObj.payloadTreeData) || !messageObj.payloadTreeData.length) {
      initializePurchasePayloadTree(messageObj, parsedData.payload || {})
    }
    if (!messageObj.payloadText) {
      messageObj.payloadText = JSON.stringify(localizePurchasePayload(parsedData.payload || {}), null, 2)
      const payloadFromTree = buildPurchasePayloadFromNodes(messageObj.payloadTreeData, 'object')
      const payloadWithHidden = mergePurchasePayloadWithHidden(payloadFromTree, messageObj.payloadHiddenData)
      messageObj.payloadText = JSON.stringify(localizePurchasePayload(payloadWithHidden), null, 2)
    }
    messageObj.confirmResult = ''
    messageObj.confirmed = false
@@ -1079,22 +1249,361 @@
  }
}
const hasMeaningfulPayloadValue = (value) => {
  if (value === null || value === undefined) return false
  if (typeof value === 'string') return value.trim() !== ''
  if (Array.isArray(value)) return value.some(item => hasMeaningfulPayloadValue(item))
  if (typeof value === 'object') return Object.values(value).some(item => hasMeaningfulPayloadValue(item))
  return true
}
const mergeMappedPayloadValue = (existingValue, incomingValue) => {
  if (existingValue === undefined) return incomingValue
  const existingHasValue = hasMeaningfulPayloadValue(existingValue)
  const incomingHasValue = hasMeaningfulPayloadValue(incomingValue)
  if (existingHasValue && !incomingHasValue) return existingValue
  if (!existingHasValue && incomingHasValue) return incomingValue
  if (
    existingValue &&
    incomingValue &&
    typeof existingValue === 'object' &&
    typeof incomingValue === 'object' &&
    !Array.isArray(existingValue) &&
    !Array.isArray(incomingValue)
  ) {
    return { ...existingValue, ...incomingValue }
  }
  return incomingValue
}
const mapPayloadKeys = (value, keyMap) => {
  if (Array.isArray(value)) {
    return value.map(item => mapPayloadKeys(item, keyMap))
  }
  if (value && typeof value === 'object') {
    return Object.entries(value).reduce((result, [key, item]) => {
      result[keyMap[key] || key] = mapPayloadKeys(item, keyMap)
      const mappedKey = keyMap[key] || key
      const mappedValue = mapPayloadKeys(item, keyMap)
      result[mappedKey] = mergeMappedPayloadValue(result[mappedKey], mappedValue)
      return result
    }, {})
  }
  return value
}
const clonePurchasePayloadValue = (value) => {
  if (Array.isArray(value)) {
    return value.map(item => clonePurchasePayloadValue(item))
  }
  if (value && typeof value === 'object') {
    return Object.entries(value).reduce((result, [key, item]) => {
      result[key] = clonePurchasePayloadValue(item)
      return result
    }, {})
  }
  return value
}
const splitPurchasePayloadByVisibility = (value) => {
  if (Array.isArray(value)) {
    const splitItems = value.map(item => splitPurchasePayloadByVisibility(item))
    const visible = splitItems.map(item => item.visible)
    const hidden = splitItems.map(item => item.hidden)
    return { visible, hidden }
  }
  if (value && typeof value === 'object') {
    const visible = {}
    const hidden = {}
    Object.entries(value).forEach(([key, item]) => {
      if (shouldHidePurchaseField(key)) {
        hidden[key] = clonePurchasePayloadValue(item)
        return
      }
      const child = splitPurchasePayloadByVisibility(item)
      visible[key] = child.visible
      if (hasMeaningfulPayloadValue(child.hidden)) {
        hidden[key] = child.hidden
      }
    })
    return { visible, hidden }
  }
  return { visible: value, hidden: undefined }
}
const mergePurchasePayloadWithHidden = (visibleValue, hiddenValue) => {
  if (hiddenValue === undefined || hiddenValue === null) return visibleValue
  if (visibleValue === undefined || visibleValue === null) return clonePurchasePayloadValue(hiddenValue)
  if (Array.isArray(visibleValue) && Array.isArray(hiddenValue)) {
    const maxLength = Math.max(visibleValue.length, hiddenValue.length)
    const merged = []
    for (let i = 0; i < maxLength; i++) {
      merged[i] = mergePurchasePayloadWithHidden(visibleValue[i], hiddenValue[i])
    }
    return merged
  }
  if (
    visibleValue &&
    hiddenValue &&
    typeof visibleValue === 'object' &&
    typeof hiddenValue === 'object' &&
    !Array.isArray(visibleValue) &&
    !Array.isArray(hiddenValue)
  ) {
    const merged = { ...clonePurchasePayloadValue(hiddenValue) }
    Object.entries(visibleValue).forEach(([key, item]) => {
      merged[key] = mergePurchasePayloadWithHidden(item, merged[key])
    })
    return merged
  }
  return visibleValue
}
const localizePurchasePayload = (payload) => mapPayloadKeys(payload, purchasePayloadFieldLabelMap)
const normalizePurchasePayload = (payload) => mapPayloadKeys(payload, purchasePayloadFieldKeyMap)
const createPurchasePayloadNodeId = () => `purchase-node-${Date.now()}-${purchasePayloadTreeNodeSeed++}`
const detectPurchaseValueType = (value) => {
  if (Array.isArray(value)) return 'array'
  if (value === null) return 'null'
  const valueType = typeof value
  if (valueType === 'number') return 'number'
  if (valueType === 'boolean') return 'boolean'
  if (valueType === 'object') return 'object'
  return 'string'
}
const normalizePurchaseNodeValueForEdit = (value, valueType) => {
  if (valueType === 'number') return value === null || value === undefined ? '' : String(value)
  if (valueType === 'boolean') return Boolean(value)
  if (valueType === 'null') return ''
  return value === null || value === undefined ? '' : String(value)
}
const createPurchaseTreeNode = ({
  key = '',
  parentType = 'object',
  keyEditable = false,
  valueType = 'string',
  value = '',
  children = []
} = {}) => ({
  id: createPurchasePayloadNodeId(),
  key,
  parentType,
  keyEditable,
  valueType,
  value,
  children
})
const reorderPurchaseObjectEntries = (value) => {
  const entries = Object.entries(value || {})
  const productDataIndex = entries.findIndex(([key]) => key === 'productData')
  if (productDataIndex <= -1 || productDataIndex === entries.length - 1) {
    return entries
  }
  const [productDataEntry] = entries.splice(productDataIndex, 1)
  entries.push(productDataEntry)
  return entries
}
const buildPurchasePayloadTreeNodes = (value, parentType = 'object') => {
  if (Array.isArray(value)) {
    return value.map(item => {
      const itemType = detectPurchaseValueType(item)
      const node = createPurchaseTreeNode({
        key: '',
        parentType: 'array',
        keyEditable: false,
        valueType: itemType,
        value: normalizePurchaseNodeValueForEdit(item, itemType)
      })
      if (purchaseContainerValueTypes.has(itemType)) {
        node.children = buildPurchasePayloadTreeNodes(item, itemType)
      }
      return node
    })
  }
  if (value && typeof value === 'object') {
    return reorderPurchaseObjectEntries(value).map(([key, item]) => {
      const itemType = detectPurchaseValueType(item)
      const node = createPurchaseTreeNode({
        key,
        parentType,
        keyEditable: false,
        valueType: itemType,
        value: normalizePurchaseNodeValueForEdit(item, itemType)
      })
      if (purchaseContainerValueTypes.has(itemType)) {
        node.children = buildPurchasePayloadTreeNodes(item, itemType)
      }
      return node
    })
  }
  return []
}
const initializePurchasePayloadTree = (messageObj, payload = {}) => {
  const sourcePayload = payload && typeof payload === 'object' && !Array.isArray(payload)
    ? payload
    : {}
  const { visible, hidden } = splitPurchasePayloadByVisibility(sourcePayload)
  const visiblePayload = visible && typeof visible === 'object' && !Array.isArray(visible) ? visible : {}
  messageObj.payloadTreeData = buildPurchasePayloadTreeNodes(visiblePayload, 'object')
  messageObj.payloadHiddenData = hidden && typeof hidden === 'object' ? hidden : {}
}
const getPurchaseFieldLabel = (fieldKey) => purchasePayloadFieldLabelMap[fieldKey] || fieldKey || '字段'
const createPurchaseDefaultNode = (parentType = 'object') => createPurchaseTreeNode({
  key: parentType === 'object' ? 'newField' : '',
  parentType,
  keyEditable: parentType === 'object',
  valueType: 'string',
  value: ''
})
const getPurchaseScalarNodeValue = (node) => {
  if (node.valueType === 'null') return null
  if (node.valueType === 'boolean') return Boolean(node.value)
  if (node.valueType === 'number') {
    const text = String(node.value ?? '').trim()
    if (!text) return null
    const numberValue = Number(text)
    return Number.isFinite(numberValue) ? numberValue : text
  }
  return node.value === null || node.value === undefined ? '' : String(node.value)
}
const buildPurchasePayloadFromNodes = (nodes, parentType = 'object') => {
  if (!Array.isArray(nodes)) {
    return parentType === 'array' ? [] : {}
  }
  if (parentType === 'array') {
    return nodes.map(node => {
      if (purchaseContainerValueTypes.has(node.valueType)) {
        return buildPurchasePayloadFromNodes(node.children, node.valueType)
      }
      return getPurchaseScalarNodeValue(node)
    })
  }
  return nodes.reduce((result, node, index) => {
    const rawKey = String(node.key ?? '').trim()
    const key = rawKey || `field_${index + 1}`
    if (purchaseContainerValueTypes.has(node.valueType)) {
      result[key] = buildPurchasePayloadFromNodes(node.children, node.valueType)
    } else {
      result[key] = getPurchaseScalarNodeValue(node)
    }
    return result
  }, {})
}
const findPurchaseNodeLocation = (nodes, targetId, parentNode = null) => {
  if (!Array.isArray(nodes)) return null
  for (let index = 0; index < nodes.length; index++) {
    const node = nodes[index]
    if (node.id === targetId) {
      return {
        siblings: nodes,
        index,
        node,
        parentNode
      }
    }
    const next = findPurchaseNodeLocation(node.children, targetId, node)
    if (next) return next
  }
  return null
}
const getPurchaseArrayItemLabel = (row, message) => {
  const location = findPurchaseNodeLocation(message?.payloadTreeData, row.id)
  return `[${(location?.index ?? 0) + 1}]`
}
const handlePurchaseNodeTypeChange = (message, row) => {
  if (!message || !row) return
  if (purchaseContainerValueTypes.has(row.valueType)) {
    row.children = []
    row.value = ''
    return
  }
  row.children = []
  if (row.valueType === 'boolean') {
    row.value = false
  } else if (row.valueType === 'null') {
    row.value = ''
  } else {
    row.value = ''
  }
}
const addPurchaseRootField = (message) => {
  if (!message) return
  if (!Array.isArray(message.payloadTreeData)) {
    message.payloadTreeData = []
  }
  message.payloadTreeData.push(createPurchaseDefaultNode('object'))
}
const addPurchaseChildNode = (message, row) => {
  if (!message || !row || !purchaseContainerValueTypes.has(row.valueType)) return
  if (!Array.isArray(row.children)) {
    row.children = []
  }
  row.children.push(createPurchaseDefaultNode(row.valueType))
}
const addPurchaseSiblingNode = (message, row) => {
  if (!message || !row) return
  const location = findPurchaseNodeLocation(message.payloadTreeData, row.id)
  if (!location || location.node.parentType !== 'array') return
  location.siblings.splice(location.index + 1, 0, createPurchaseDefaultNode('array'))
}
const removePurchaseNode = (message, row) => {
  if (!message || !row) return
  const location = findPurchaseNodeLocation(message.payloadTreeData, row.id)
  if (!location) return
  location.siblings.splice(location.index, 1)
}
const hasPurchaseNodeValidationError = (nodes, parentType = 'object') => {
  if (!Array.isArray(nodes)) return false
  return nodes.some((node) => {
    if (parentType === 'object' && !String(node.key ?? '').trim()) {
      return true
    }
    if (node.valueType === 'number') {
      const text = String(node.value ?? '').trim()
      if (text && !Number.isFinite(Number(text))) {
        return true
      }
    }
    if (purchaseContainerValueTypes.has(node.valueType)) {
      return hasPurchaseNodeValidationError(node.children, node.valueType)
    }
    return false
  })
}
const purchaseDateFieldKeys = new Set([
  'entryDateStart',
@@ -1257,7 +1766,11 @@
const getVisiblePurchaseMissingFields = (analysisData) => {
  const fields = Array.isArray(analysisData?.missingFields) ? analysisData.missingFields : []
  const visibleFields = analysisData?.businessType === 'purchase_ledger'
    ? fields.filter(field => !purchaseApprovalFieldKeys.has(field))
    ? fields.filter(field => {
      if (purchaseApprovalFieldKeys.has(field)) return false
      const normalizedField = purchasePayloadFieldKeyMap[field] || field
      return !shouldHidePurchaseField(normalizedField) && !shouldHidePurchaseField(field)
    })
    : fields
  return visibleFields.map(field => purchasePayloadFieldLabelMap[field] || field)
}
@@ -1372,6 +1885,53 @@
  }
}
const confirmPurchaseAnalysisFromTable = async (message) => {
  if (!message?.purchaseAnalysisData || message.confirming || message.confirmed) return
  if (!Array.isArray(message.payloadTreeData)) {
    initializePurchasePayloadTree(message, message.purchaseAnalysisData.payload || {})
  }
  if (hasPurchaseNodeValidationError(message.payloadTreeData, 'object')) {
    message.confirmResult = '请先补全字段名,并确保数字字段填写合法数字'
    message.confirmed = false
    return
  }
  let payload
  try {
    const draftPayload = buildPurchasePayloadFromNodes(message.payloadTreeData, 'object')
    const mergedPayload = mergePurchasePayloadWithHidden(draftPayload, message.payloadHiddenData)
    const normalizedPayload = normalizePurchasePayload(mergedPayload)
    payload = sanitizePurchasePayloadForSubmit(
      normalizePurchasePayloadDates(normalizedPayload),
      message.purchaseAnalysisData.businessType
    )
    message.payloadText = JSON.stringify(localizePurchasePayload(normalizedPayload), null, 2)
  } catch (err) {
    message.confirmResult = '待提交数据格式有误,请检查后再确认'
    message.confirmed = false
    return
  }
  message.confirming = true
  message.confirmResult = ''
  try {
    const res = await request.post(`${currentAssistant.value.apiBase}/analyze-files/confirm`, {
      businessType: message.purchaseAnalysisData.businessType,
      payload
    })
    message.confirmed = true
    message.confirmResult = res?.msg || '确认成功,业务处理已提交'
    ElMessage.success(message.confirmResult)
  } catch (err) {
    message.confirmed = false
    message.confirmResult = err?.message || '确认失败,请检查数据后重试'
  } finally {
    message.confirming = false
  }
}
const scrollToBottom = () => {
  nextTick(() => {
    if (messageListRef.value) {
@@ -1428,7 +1988,9 @@
    chartOptions: null,
    chartRenderReady: false,
    type: '',
    tableData: null
    tableData: null,
    payloadTreeData: null,
    payloadHiddenData: null
  })
  outputState.value[botMsgIndex] = {
@@ -1549,7 +2111,9 @@
    chartOptions: null,
    chartRenderReady: false,
    type: '',
    tableData: null
    tableData: null,
    payloadTreeData: null,
    payloadHiddenData: null
  }
  messages.value.push(botMsg)
@@ -2774,14 +3338,64 @@
  color: $deep-blue;
}
.payload-editor {
  :deep(.el-textarea__inner) {
    font-family: Consolas, Monaco, monospace;
    font-size: 12px;
    line-height: 1.55;
.payload-toolbar {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 8px;
}
.payload-tree-table-wrapper {
  border: 1px solid rgba(0, 85, 212, 0.1);
  border-radius: 10px;
  overflow: auto;
  :deep(.el-table) {
    --el-table-header-bg-color: #f5f8ff;
    --el-table-border-color: rgba(0, 85, 212, 0.08);
  }
}
.payload-key-cell {
  display: flex;
  align-items: center;
  min-height: 28px;
}
.payload-fixed-key {
  display: flex;
  flex-direction: column;
  gap: 2px;
  line-height: 1.3;
  color: #1f2937;
  small {
    font-size: 11px;
    color: #6b7280;
  }
}
.payload-array-index {
  font-size: 12px;
  color: #475467;
}
.payload-container-cell {
  color: #344054;
  font-size: 12px;
}
.payload-null-value {
  color: #6b7280;
  font-size: 12px;
}
.payload-row-actions {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
}
.payload-editor-tip {
  margin-top: 6px;
  font-size: 12px;