4 Commits 57f0ccd3a7 ... d04d5849e8

Autor SHA1 Mensaje Fecha
  handy d04d5849e8 移除无用代码 hace 1 mes
  handy 1262e10296 底部留白去掉 hace 1 mes
  handy 01e940d792 优化滚动条 hace 1 mes
  handy 24086a160d 1 hace 1 mes
Se han modificado 5 ficheros con 344 adiciones y 127 borrados
  1. 32 16
      src/App.vue
  2. 47 0
      src/api/lock.ts
  3. 124 111
      src/components/WarehouseMap.vue
  4. 112 0
      src/components/WorkingHighlight.vue
  5. 29 0
      src/types/index.ts

+ 32 - 16
src/App.vue

@@ -346,22 +346,24 @@
         v-else
         class="map-container"
       >
-        <WarehouseMap
-          :locations="locations"
-          :current-level="currentLevel"
-          :selected-category="selectedCategory"
-          :selected-location-attribute="selectedLocationAttribute"
-          :selected-has-container="selectedHasContainer"
-          :selected-zone-id="selectedZoneId"
-          :loc-group-keyword="appliedLocGroupKeyword"
-          :location-id-keyword="appliedLocationIdKeyword"
-          :show-group-border="showGroupBorder"
-          :show-tooltip="showTooltip"
-          :category-color-visibility="categoryColorVisibility"
-          :theme-mode="isLightTheme ? 'light' : 'dark'"
-          @select-loc-group="handleSelectLocGroup"
-          @select-location-id="handleSelectLocationId"
-        />
+        <WorkingHighlight>
+          <WarehouseMap
+            :locations="locations"
+            :current-level="currentLevel"
+            :selected-category="selectedCategory"
+            :selected-location-attribute="selectedLocationAttribute"
+            :selected-has-container="selectedHasContainer"
+            :selected-zone-id="selectedZoneId"
+            :loc-group-keyword="appliedLocGroupKeyword"
+            :location-id-keyword="appliedLocationIdKeyword"
+            :show-group-border="showGroupBorder"
+            :show-tooltip="showTooltip"
+            :category-color-visibility="categoryColorVisibility"
+            :theme-mode="isLightTheme ? 'light' : 'dark'"
+            @select-loc-group="handleSelectLocGroup"
+            @select-location-id="handleSelectLocationId"
+          />
+        </WorkingHighlight>
       </div>
     </main>
   </div>
@@ -382,6 +384,7 @@ import { fetchLocationData } from './api/location'
 import type { LocationAttributeCode, LocationResourceDataVO } from './types'
 import WarehouseMap from './components/WarehouseMap.vue'
 import LoginModal from './components/LoginModal.vue'
+import WorkingHighlight from './components/WorkingHighlight.vue'
 import { config } from './config'
 import { AUTH_INVALID_EVENT, getApiEnvironment, isAuthenticated, removeToken } from './utils/auth'
 
@@ -457,6 +460,7 @@ const fillRateTooltipPosition = ref({
 })
 let refreshTimer: number | null = null
 let countdownTimer: number | null = null
+const WORKING_HIGHLIGHT_REFRESH_EVENT = 'working-highlight-refresh'
 
 const LOCATION_ATTRIBUTE_LABEL_MAP: Record<LocationAttributeCode, string> = {
   OK: '正常',
@@ -640,10 +644,12 @@ const copyText = async (text: string) => {
 
 const applyLocGroupFilter = () => {
   appliedLocGroupKeyword.value = locGroupKeywordInput.value.trim()
+  triggerWorkingHighlightRefresh()
 }
 
 const applyLocationIdFilter = () => {
   appliedLocationIdKeyword.value = locationIdKeywordInput.value.trim()
+  triggerWorkingHighlightRefresh()
 }
 
 const clearLocGroupFilter = () => {
@@ -683,6 +689,7 @@ const loadLocationData = async (options: { silent?: boolean } = {}) => {
     error.value = ''
   }
 
+  let loaded = false
   try {
     const data = await fetchLocationData({
       warehouse: config.warehouse,
@@ -690,6 +697,7 @@ const loadLocationData = async (options: { silent?: boolean } = {}) => {
     })
     locations.value = data
     hasLoadedOnce.value = true
+    loaded = true
   } catch (err: unknown) {
     if (!shouldUseSilentRefresh) {
       error.value = err instanceof Error ? err.message : '加载数据失败,请检查接口连接'
@@ -701,9 +709,17 @@ const loadLocationData = async (options: { silent?: boolean } = {}) => {
     } else {
       loading.value = false
     }
+    if (loaded) {
+      await nextTick()
+      triggerWorkingHighlightRefresh()
+    }
   }
 }
 
+const triggerWorkingHighlightRefresh = () => {
+  window.dispatchEvent(new Event(WORKING_HIGHLIGHT_REFRESH_EVENT))
+}
+
 const scheduleNextRefresh = () => {
   if (refreshTimer !== null) {
     window.clearTimeout(refreshTimer)

+ 47 - 0
src/api/lock.ts

@@ -0,0 +1,47 @@
+import axios from 'axios'
+import type { ApiResponse, LockPageData, LockPageRequest } from '../types'
+import { AUTH_INVALID_EVENT, getApiBaseUrl, getToken, removeToken } from '../utils/auth'
+
+const apiClient = axios.create({
+  timeout: 10000,
+  headers: {
+    'Content-Type': 'application/json'
+  }
+})
+
+apiClient.interceptors.request.use(
+  (requestConfig) => {
+    const token = getToken()
+    if (token) {
+      requestConfig.headers.Authorization = token
+    }
+    requestConfig.baseURL = getApiBaseUrl()
+    requestConfig.headers.Source = 'web'
+    return requestConfig
+  },
+  (error) => Promise.reject(error)
+)
+
+apiClient.interceptors.response.use(
+  (response) => {
+    const { code, message } = response.data || {}
+    if (code === 600) {
+      removeToken()
+      window.dispatchEvent(new CustomEvent(AUTH_INVALID_EVENT))
+      return Promise.reject(new Error(message || '登录失效,请重新登录'))
+    }
+    if (code !== 200) {
+      return Promise.reject(new Error(message || '请求失败'))
+    }
+    return response
+  },
+  (error) => Promise.reject(error)
+)
+
+export const fetchLockPage = async (params: LockPageRequest): Promise<LockPageData> => {
+  const response = await apiClient.post<ApiResponse<LockPageData>>(
+    '/api/basic/resource/lock/page',
+    params
+  )
+  return response.data.data
+}

+ 124 - 111
src/components/WarehouseMap.vue

@@ -31,12 +31,6 @@
             v-if="isLocationCell(cell) && isCellVisible(cell)"
             class="cell-content"
           >
-            <div
-              class="category-badge"
-              :style="getCategoryStyle(cell)"
-            >
-              {{ getHeatLabel(cell) }}
-            </div>
             <div class="loc-group">
               {{ cell.locGroup1 }}
             </div>
@@ -97,7 +91,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, onBeforeUnmount, onMounted, ref, watch, type CSSProperties } from 'vue'
+import { computed, inject, onBeforeUnmount, onMounted, ref, watch, type CSSProperties } from 'vue'
 import type { LocationResourceDataVO } from '../types'
 import { applyWarehouseLayoutEnhancement } from './warehouse-layout-enhancers'
 import {
@@ -105,6 +99,14 @@ import {
   type WarehouseLayoutSpecialCell
 } from './warehouse-layout-special-cells'
 
+const WORKING_HIGHLIGHT_KEY = 'working-highlight'
+
+interface WorkingHighlightState {
+  locGroupInbound: Set<string>
+  locGroupOutbound: Set<string>
+  containerOutbound: Set<string>
+}
+
 interface Props {
   locations: LocationResourceDataVO[]
   currentLevel: number
@@ -180,64 +182,8 @@ const DEPTH_CATEGORY_MAP: Record<number, string> = {
   3: 'C'
 }
 
-const CATEGORY_THEME_MAP_DARK: Record<string, { solid: string; soft: string; text: string }> = {
-  A: {
-    solid: '#1f7a3f',
-    soft: 'rgba(31, 122, 63, 0.18)',
-    text: '#f3fff7'
-  },
-  B: {
-    solid: '#2f5fd7',
-    soft: 'rgba(47, 95, 215, 0.18)',
-    text: '#f5f8ff'
-  },
-  C: {
-    solid: '#b8921f',
-    soft: 'rgba(184, 146, 31, 0.2)',
-    text: '#161200'
-  }
-}
-
-const CATEGORY_THEME_MAP_LIGHT: Record<string, { solid: string; soft: string; text: string }> = {
-  A: {
-    solid: '#1f7a3f',
-    soft: 'rgba(31, 122, 63, 0.12)',
-    text: '#ffffff'
-  },
-  B: {
-    solid: '#2f5fd7',
-    soft: 'rgba(47, 95, 215, 0.12)',
-    text: '#ffffff'
-  },
-  C: {
-    solid: '#b8921f',
-    soft: 'rgba(184, 146, 31, 0.18)',
-    text: '#1f1f1f'
-  }
-}
-
-const INACTIVE_CATEGORY_THEME_DARK = {
-  solid: '#5f6b7a',
-  soft: 'rgba(95, 107, 122, 0.16)',
-  text: '#eef4f8'
-}
-
-const INACTIVE_CATEGORY_THEME_LIGHT = {
-  solid: '#8a97a5',
-  soft: 'rgba(138, 151, 165, 0.18)',
-  text: '#ffffff'
-}
-
 const themeClass = computed(() => (props.themeMode === 'light' ? 'theme-light' : 'theme-dark'))
 
-const categoryThemeMap = computed(() =>
-  props.themeMode === 'light' ? CATEGORY_THEME_MAP_LIGHT : CATEGORY_THEME_MAP_DARK
-)
-
-const inactiveCategoryTheme = computed(() =>
-  props.themeMode === 'light' ? INACTIVE_CATEGORY_THEME_LIGHT : INACTIVE_CATEGORY_THEME_DARK
-)
-
 const groupBorderColor = computed(() => (props.themeMode === 'light' ? '#6f7a86' : '#ffffff'))
 
 const LOCATION_ATTRIBUTE_LABEL_MAP: Record<string, string> = {
@@ -379,40 +325,59 @@ const gridStyle = computed(() => {
   if (!gridMetrics.value) return {}
 
   const gap = 2
-  const horizontalPadding = 24
-  const verticalPadding = 12
+  const horizontalPadding = 12
+  const verticalPadding = 0
   const availableWidth = Math.max(wrapperSize.value.width - horizontalPadding, 0)
   const availableHeight = Math.max(wrapperSize.value.height - verticalPadding, 0)
 
-  const cellWidth =
+  const minX = gridBounds.value?.minX ?? 0
+  const minY = gridBounds.value?.minY ?? 0
+  const rowHasLocation = Array.from({ length: gridMetrics.value.rows }, () => false)
+  const colHasLocation = Array.from({ length: gridMetrics.value.cols }, () => false)
+  const isLocation = (cell: MapCell) => !('type' in cell)
+  for (const cell of gridData.value) {
+    if (!isLocation(cell)) continue
+    const rowIndex = cell.gridRow - minX
+    const colIndex = cell.gridCol - minY
+    if (rowIndex >= 0 && rowIndex < rowHasLocation.length) rowHasLocation[rowIndex] = true
+    if (colIndex >= 0 && colIndex < colHasLocation.length) colHasLocation[colIndex] = true
+  }
+
+  const compressedScale = 2 / 3
+  const rowScales = rowHasLocation.map((hasLocation) => (hasLocation ? 1 : compressedScale))
+  const colScales = colHasLocation.map((hasLocation) => (hasLocation ? 1 : compressedScale))
+  const rowScaleSum = rowScales.reduce((total, scale) => total + scale, 0)
+  const colScaleSum = colScales.reduce((total, scale) => total + scale, 0)
+
+  const baseCellWidth =
     availableWidth > 0
-      ? Math.max((availableWidth - (gridMetrics.value.cols - 1) * gap) / gridMetrics.value.cols, 28)
+      ? Math.max((availableWidth - (gridMetrics.value.cols - 1) * gap) / colScaleSum, 28)
       : 72
-  const cellHeight =
+  const baseCellHeight =
     availableHeight > 0
-      ? Math.max(
-          (availableHeight - (gridMetrics.value.rows - 1) * gap) / gridMetrics.value.rows,
-          24
-        )
+      ? Math.max((availableHeight - (gridMetrics.value.rows - 1) * gap) / rowScaleSum, 24)
       : 60
-  const compactSize = Math.min(cellWidth, cellHeight)
+  const compactSize = Math.min(baseCellWidth, baseCellHeight)
 
-  const showBadge = compactSize > 38 ? 'inline-flex' : 'none'
   const showGroup = compactSize > 58 ? 'block' : 'none'
-  const badgeFontSize = compactSize <= 42 ? 7 : compactSize <= 56 ? 8 : 9
-  const textFontSize = compactSize <= 56 ? 8 : 9
-  const idFontSize = compactSize <= 34 ? 8 : compactSize <= 42 ? 9 : compactSize <= 56 ? 9 : 10
+  const textFontSize = Math.max(compactSize <= 56 ? 8 : 9, 8)
+  const idFontSize = Math.max(
+    compactSize <= 34 ? 8 : compactSize <= 42 ? 9 : compactSize <= 56 ? 9 : 10,
+    9
+  )
   const contentGap = compactSize <= 42 ? 1 : 3
-  const contentPadding = compactSize <= 42 ? 1 : 3
+  const contentPadding = Math.max(compactSize <= 42 ? 1 : 3, 1)
   const groupBorderWidth = compactSize <= 34 ? 1 : 2
 
   return {
-    gridTemplateColumns: `repeat(${gridMetrics.value.cols}, ${cellWidth.toFixed(2)}px)`,
-    gridAutoRows: `${cellHeight.toFixed(2)}px`,
-    '--cell-badge-font-size': `${badgeFontSize}px`,
+    gridTemplateColumns: colScales
+      .map((scale) => `${(baseCellWidth * scale).toFixed(2)}px`)
+      .join(' '),
+    gridTemplateRows: rowScales
+      .map((scale) => `${(baseCellHeight * scale).toFixed(2)}px`)
+      .join(' '),
     '--cell-group-font-size': `${textFontSize}px`,
     '--cell-id-font-size': `${idFontSize}px`,
-    '--cell-badge-display': showBadge,
     '--cell-group-display': showGroup,
     '--cell-content-gap': `${contentGap}px`,
     '--cell-content-padding': `${contentPadding}px`,
@@ -509,6 +474,26 @@ const isLocationCell = (cell: MapCell): cell is GridCell => {
   return Boolean(cell && !('type' in cell))
 }
 
+const workingHighlightState = inject<WorkingHighlightState | null>(WORKING_HIGHLIGHT_KEY, null)
+
+const getWorkingHighlightClass = (cell: GridCell) => {
+  if (!workingHighlightState) {
+    return null
+  }
+  const containerCode = cell.containerCode || ''
+  if (containerCode && workingHighlightState.containerOutbound.has(containerCode)) {
+    return 'working-container-outbound'
+  }
+  const locGroupCode = cell.locGroup1 || ''
+  if (locGroupCode && workingHighlightState.locGroupInbound.has(locGroupCode)) {
+    return 'working-loc-group-inbound'
+  }
+  if (locGroupCode && workingHighlightState.locGroupOutbound.has(locGroupCode)) {
+    return 'working-loc-group-outbound'
+  }
+  return null
+}
+
 const getCellClass = (cell: MapCell) => {
   if (isSpecialCell(cell)) {
     return shouldHideNonLocationCells.value ? ['hidden-cell'] : [cell.type]
@@ -523,6 +508,10 @@ const getCellClass = (cell: MapCell) => {
   }
 
   const classNames = ['location-cell']
+  const workingClass = getWorkingHighlightClass(cell)
+  if (workingClass) {
+    classNames.push(workingClass)
+  }
   if (props.categoryColorVisibility[cell.category] !== false) {
     classNames.push(`category-${cell.category.toLowerCase()}`)
   } else {
@@ -534,10 +523,6 @@ const getCellClass = (cell: MapCell) => {
   return classNames
 }
 
-const getHeatLabel = (cell: GridCell) => {
-  return cell.category
-}
-
 const hasAbnormalLocationAttribute = (cell: GridCell) => {
   return Boolean(cell.locationAttribute && cell.locationAttribute !== 'OK')
 }
@@ -578,17 +563,6 @@ const getCellTitle = (cell: GridCell | null) => {
   ].join('\n')
 }
 
-const getCategoryStyle = (cell: GridCell) => {
-  const theme =
-    props.categoryColorVisibility[cell.category] === false
-      ? inactiveCategoryTheme.value
-      : categoryThemeMap.value[cell.category]
-  return {
-    background: theme?.solid || '#5f6b7a',
-    color: theme?.text || '#fff'
-  }
-}
-
 const tooltipLines = computed(() => {
   return getCellTitle(hoveredCell.value)
     .split('\n')
@@ -837,6 +811,9 @@ onBeforeUnmount(() => {
   --cell-group: #a0a0a0;
   --cell-container: #9a9a9a;
   --cell-alert: #ff4d4f;
+  --working-loc-group-inbound: rgba(96, 240, 160, 0.85);
+  --working-loc-group-outbound: rgba(255, 168, 74, 0.88);
+  --working-container-outbound: rgba(94, 200, 255, 0.9);
   --special-label: rgba(255, 255, 255, 0.76);
   --special-label-strong: rgba(255, 255, 255, 0.86);
   --tooltip-border: #1f1f1f;
@@ -860,6 +837,9 @@ onBeforeUnmount(() => {
   --cell-group: #5f6368;
   --cell-container: #6f7a86;
   --cell-alert: #c73939;
+  --working-loc-group-inbound: rgba(34, 174, 96, 0.9);
+  --working-loc-group-outbound: rgba(222, 120, 36, 0.92);
+  --working-container-outbound: rgba(26, 134, 207, 0.92);
   --special-label: rgba(31, 35, 40, 0.7);
   --special-label-strong: rgba(31, 35, 40, 0.82);
   --tooltip-border: #d7dce3;
@@ -888,11 +868,11 @@ onBeforeUnmount(() => {
 .map-grid {
   display: grid;
   gap: 2px;
-  padding: 0 12px 12px;
+  padding: 0 6px 0;
   box-sizing: border-box;
   width: 100%;
   height: 100%;
-  align-content: start;
+  align-content: stretch;
 }
 
 .grid-cell {
@@ -957,12 +937,56 @@ onBeforeUnmount(() => {
   background: var(--cell-category-muted);
 }
 
+.grid-cell.working-loc-group-inbound::before,
+.grid-cell.working-loc-group-outbound::before,
+.grid-cell.working-container-outbound::before {
+  content: '';
+  position: absolute;
+  inset: -1px;
+  border-radius: 6px;
+  pointer-events: none;
+  animation: workingPulse 1.6s ease-in-out infinite;
+}
+
+.grid-cell.working-loc-group-inbound::before {
+  box-shadow:
+    0 0 0 2px var(--working-loc-group-inbound),
+    0 0 18px var(--working-loc-group-inbound);
+}
+
+.grid-cell.working-loc-group-outbound::before {
+  box-shadow:
+    0 0 0 2px var(--working-loc-group-outbound),
+    0 0 18px var(--working-loc-group-outbound);
+}
+
+.grid-cell.working-container-outbound::before {
+  box-shadow:
+    0 0 0 2px var(--working-container-outbound),
+    0 0 18px var(--working-container-outbound);
+}
+
 .grid-cell:not(.aisle):not(.wall):not(.elevator):hover {
   transform: scale(1.03);
   box-shadow: var(--hover-shadow);
   z-index: 10;
 }
 
+@keyframes workingPulse {
+  0% {
+    opacity: 0.35;
+    transform: scale(0.97);
+  }
+  50% {
+    opacity: 1;
+    transform: scale(1.02);
+  }
+  100% {
+    opacity: 0.35;
+    transform: scale(0.97);
+  }
+}
+
 .cell-content {
   display: flex;
   flex-direction: column;
@@ -975,18 +999,6 @@ onBeforeUnmount(() => {
   overflow: hidden;
 }
 
-.category-badge {
-  display: var(--cell-badge-display, inline-flex);
-  align-items: center;
-  justify-content: center;
-  padding: 2px 8px;
-  border-radius: 10px;
-  font-size: var(--cell-badge-font-size, 10px);
-  font-weight: bold;
-  line-height: 1;
-  white-space: nowrap;
-}
-
 .loc-group {
   display: var(--cell-group-display, -webkit-box);
   font-size: var(--cell-group-font-size, 10px);
@@ -1047,6 +1059,7 @@ onBeforeUnmount(() => {
   color: var(--special-label);
   letter-spacing: 1px;
   user-select: none;
+  padding: 1px;
 }
 
 .special-cell-label-elevator {

+ 112 - 0
src/components/WorkingHighlight.vue

@@ -0,0 +1,112 @@
+<template>
+  <div class="working-highlight">
+    <slot />
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onBeforeUnmount, onMounted, provide, reactive, ref } from 'vue'
+import { fetchLockPage } from '../api/lock'
+import type { LockRecord } from '../types'
+import { AUTH_INVALID_EVENT, isAuthenticated } from '../utils/auth'
+
+interface WorkingHighlightState {
+  locGroupInbound: Set<string>
+  locGroupOutbound: Set<string>
+  containerOutbound: Set<string>
+}
+
+const WORKING_HIGHLIGHT_KEY = 'working-highlight'
+
+const highlightState = reactive<WorkingHighlightState>({
+  locGroupInbound: new Set<string>(),
+  locGroupOutbound: new Set<string>(),
+  containerOutbound: new Set<string>()
+})
+
+provide(WORKING_HIGHLIGHT_KEY, highlightState)
+
+const loading = ref(false)
+const WORKING_HIGHLIGHT_REFRESH_EVENT = 'working-highlight-refresh'
+
+const clearHighlights = () => {
+  highlightState.locGroupInbound = new Set<string>()
+  highlightState.locGroupOutbound = new Set<string>()
+  highlightState.containerOutbound = new Set<string>()
+}
+
+const normalizeCode = (value: string | null | undefined) => {
+  return (value || '').trim().toUpperCase()
+}
+
+const applyHighlights = (records: LockRecord[]) => {
+  const locGroupInbound = new Set<string>()
+  const locGroupOutbound = new Set<string>()
+  const containerOutbound = new Set<string>()
+
+  records.forEach((record) => {
+    const normalizedCode = normalizeCode(record.resourceCode)
+    if (!normalizedCode) {
+      return
+    }
+    if (record.resourceType === 'LOC_GROUP') {
+      if (record.lockSourceType === 'INBOUND') {
+        locGroupInbound.add(normalizedCode)
+      } else if (record.lockSourceType === 'OUTBOUND') {
+        locGroupOutbound.add(normalizedCode)
+      }
+      return
+    }
+    if (record.resourceType === 'CONTAINER' && record.lockSourceType === 'OUTBOUND') {
+      containerOutbound.add(normalizedCode)
+    }
+  })
+
+  highlightState.locGroupInbound = locGroupInbound
+  highlightState.locGroupOutbound = locGroupOutbound
+  highlightState.containerOutbound = containerOutbound
+}
+
+const loadHighlights = async () => {
+  if (loading.value || !isAuthenticated()) {
+    if (!isAuthenticated()) {
+      clearHighlights()
+    }
+    return
+  }
+
+  loading.value = true
+  try {
+    const data = await fetchLockPage({ page: 1, size: 50 })
+    applyHighlights(data.records || [])
+  } catch (error) {
+    console.error('Failed to load working highlights:', error)
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleAuthInvalid = () => {
+  clearHighlights()
+}
+
+const handleRefreshRequest = () => {
+  loadHighlights()
+}
+
+onMounted(() => {
+  window.addEventListener(AUTH_INVALID_EVENT, handleAuthInvalid)
+  window.addEventListener(WORKING_HIGHLIGHT_REFRESH_EVENT, handleRefreshRequest)
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener(AUTH_INVALID_EVENT, handleAuthInvalid)
+  window.removeEventListener(WORKING_HIGHLIGHT_REFRESH_EVENT, handleRefreshRequest)
+})
+</script>
+
+<style scoped>
+.working-highlight {
+  display: contents;
+}
+</style>

+ 29 - 0
src/types/index.ts

@@ -44,3 +44,32 @@ export interface LoginResponse {
   }
   version: string
 }
+
+export type LockResourceType = 'CONTAINER' | 'LOC_GROUP' | string
+export type LockSourceType = 'INBOUND' | 'OUTBOUND' | string
+
+export interface LockRecord {
+  id: number
+  resourceType: LockResourceType
+  resourceCode: string
+  lockSource: string
+  lockSourceType: LockSourceType
+  lockTime: string
+  createTime: string
+  creatorId: number
+  updateTime: string
+  updaterId: number | null
+}
+
+export interface LockPageData {
+  records: LockRecord[]
+  total: number
+  size: number
+  current: number
+  pages: number
+}
+
+export interface LockPageRequest {
+  page: number
+  size: number
+}