zh 3 месяцев назад
Родитель
Сommit
c64496bed2
2 измененных файлов с 616 добавлено и 76 удалено
  1. 58 0
      src/api/location/merge.ts
  2. 558 76
      src/views/robot/merge/index.vue

+ 58 - 0
src/api/location/merge.ts

@@ -0,0 +1,58 @@
+// @ts-ignore
+import request from '@/utils/request'
+
+// 库位合并详情
+export interface LocationMergeDetails {
+  id: number
+  code: string
+  warehouse: string
+  sourceLocation: string
+  targetLocation: string
+  sourceBox: string
+  targetBox: string
+  status: string
+  quantity: number
+  sku: string
+  barcode: string
+  productName: string
+  lotNumber: string
+  owner: string
+  qualityStatus: string
+  warehouseType: string
+  productionDate: string
+  expiryDate: string
+}
+
+// 料箱相关的并库详情VO
+export interface BoxRelatedMergeDetailsVO {
+  boxCode: string
+  station: string
+  mergeDetails: LocationMergeDetails[]
+  boxStatus: number // 0,10-等待调箱, 20-到达, 30-取消, 40-异常
+  inventoryLocations: string[]
+}
+
+/**
+ * 通过料箱号查看相关作业中任务详情
+ * @param boxCode 料箱号
+ */
+export function getWorkingDetailsByBox(boxCode: string, warehouse: string) {
+  return request<BoxRelatedMergeDetailsVO[]>({
+    url: '/api/wms/location/merge/workingDetailsByBox',
+    method: 'get',
+    params: { boxCode, warehouse }
+  })
+}
+
+/**
+ * 获取料箱库位的隔口种类
+ * @param warehouse 仓库
+ * @param locations 库位列表
+ */
+export function getBoxSplitCode(warehouse: string, locations: string[]) {
+  return request<Record<string, number>>({
+    url: `/api/basic/location/getBoxSplitCode/${warehouse}`,
+    method: 'post',
+    data: locations
+  })
+}

+ 558 - 76
src/views/robot/merge/index.vue

@@ -12,7 +12,7 @@
     <!-- 信息展示表格 -->
     <div class="info-table">
       <div class="table-row">
-        <div class="cell label">目标库位</div>
+        <div class="cell label">库位</div>
         <div class="cell value">{{ productInfo.targetLocation }}</div>
         <div class="cell label">库存数量</div>
         <div class="cell value">{{ productInfo.stockQty }}</div>
@@ -28,9 +28,9 @@
       <div class="table-row row-small">
         <div class="cell label">质量状态</div>
         <div class="cell value">{{ productInfo.qualityStatus }}</div>
-        <div class="cell label">属性仓</div>
+        <div class="cell label label-small">属性仓</div>
         <div class="cell value">{{ productInfo.warehouseType }}</div>
-        <div class="cell label">批号</div>
+        <div class="cell label label-small">批号</div>
         <div class="cell value"></div>
       </div>
       <div class="table-row">
@@ -65,41 +65,43 @@
     <div class="grid-section">
       <div class="grid-container">
         <div
-          v-for="box in boxList"
-          :key="box.id"
+          v-for="station in stationList"
+          :key="station.id"
           class="box-wrapper"
         >
-          <!-- 序号在料箱上方 -->
-          <div class="box-number">{{ box.id }}</div>
+          <!-- 站台序号在上方 -->
+          <div class="box-number">{{ station.displayNumber }}</div>
           <div
             class="box-item"
             :class="{
-              'box-filled': box.status === 'filled' && !box.splitCount,
-              'box-empty-box': box.status === 'emptyBox',
-              'box-waiting': box.status === 'waiting',
-              'box-selected': selectedBox === box.id,
-              'box-split': box.splitCount
+              'box-filled': station.status === 'filled' && !station.splitCount,
+              'box-empty-box': station.status === 'emptyBox',
+              'box-waiting': station.status === 'waiting',
+              'box-error': station.status === 'error',
+              'box-selected': selectedBox === station.stationCode,
+              'box-split': station.splitCount
             }"
-            @click="!box.splitCount && selectBox(box)"
+            @click="!station.splitCount && station.status !== 'offline' && selectStation(station)"
           >
             <!-- 分割的料箱 -->
-            <template v-if="box.splitCount && box.subLocations">
-              <div class="sub-grid" :style="getSubGridStyle(box.splitCount)">
+            <template v-if="station.splitCount && station.subLocations">
+              <div class="sub-grid" :style="getSubGridStyle(station.splitCount)">
                 <div
-                  v-for="sub in box.subLocations"
+                  v-for="sub in station.subLocations"
                   :key="sub.id"
                   class="sub-location"
                   :class="{
                     'sub-filled': sub.status === 'filled',
-                    'sub-selected': selectedBox === sub.id
+                    'sub-selected': selectedBox === sub.locationCode,
+                    'sub-disabled': !isLocationClickable(sub.locationCode)
                   }"
-                  @click.stop="selectSubLocation(box, sub)"
+                  @click.stop="isLocationClickable(sub.locationCode) && selectSubLocation(station, sub)"
                 ></div>
               </div>
             </template>
-            <!-- 普通料箱 -->
+            <!-- 普通站台 -->
             <template v-else>
-              <span v-if="box.label" class="box-label">{{ box.label }}</span>
+              <span v-if="station.label" class="box-label">{{ station.label }}</span>
             </template>
           </div>
         </div>
@@ -114,24 +116,236 @@
         <van-button class="btn-submit" size="small" @click="submitMove">提交移库</van-button>
       </div>
     </div>
+
+    <!-- 库位信息弹窗 -->
+    <van-popup
+      v-model:show="showLocationPopup"
+      position="bottom"
+      round
+      :style="{ maxHeight: '70%' }"
+    >
+      <div class="location-popup">
+        <div class="popup-header">
+          <span class="popup-title">库位信息</span>
+          <van-icon name="cross" @click="showLocationPopup = false" />
+        </div>
+        <div class="popup-content">
+          <div class="info-row">
+            <span class="info-label">库位编号</span>
+            <span class="info-value">{{ currentLocation.id }}</span>
+          </div>
+          <div class="info-row">
+            <span class="info-label">商品名称</span>
+            <span class="info-value">{{ currentLocation.productName || '-' }}</span>
+          </div>
+          <div class="info-row">
+            <span class="info-label">商品条码</span>
+            <span class="info-value">{{ currentLocation.barcode || '-' }}</span>
+          </div>
+          <div class="info-row">
+            <span class="info-label">库存数量</span>
+            <span class="info-value">{{ currentLocation.qty || 0 }}</span>
+          </div>
+          <!-- 推荐库位列表 -->
+          <div class="recommend-section">
+            <div class="recommend-title">
+              {{ currentLocation.recommendType === 'clear' ? '推荐清空' : '推荐保留' }}
+            </div>
+            <div v-if="currentLocation.relatedLocations.length > 0" class="recommend-list">
+              <div
+                v-for="(item, index) in currentLocation.relatedLocations"
+                :key="index"
+                class="recommend-item"
+              >
+                <span class="recommend-location">{{ currentLocation.recommendType === 'clear' ? '保留库位' : '清空库位' }}: {{ item.location }}</span>
+                <span class="recommend-qty">推荐移库数量: {{ item.quantity }}</span>
+              </div>
+            </div>
+            <div v-else class="recommend-empty">暂无推荐库位</div>
+          </div>
+        </div>
+        <div class="popup-footer">
+          <van-button type="primary" block @click="confirmSelectLocation">选择此库位</van-button>
+        </div>
+      </div>
+    </van-popup>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref, reactive } from 'vue'
-import { showToast } from 'vant'
+import { closeListener, openListener, scanInit } from '@/utils/keydownListener'
+import { onMounted, onUnmounted, ref, reactive, watch, nextTick } from 'vue'
+import { showToast, showLoadingToast, closeToast } from 'vant'
+import { useStore } from '@/store/modules/user'
+import { getWorkingDetailsByBox, getBoxSplitCode, type BoxRelatedMergeDetailsVO, type LocationMergeDetails } from '@/api/location/merge'
+
+const store = useStore()
+const warehouse = store.warehouse
+
+
+// 页面初始化
+onMounted(() => {
+  openListener()
+  scanInit(_handlerScan)
+  // 获取焦点
+  nextTick(() => {
+    focusScanInput()
+  })
+})
+
+onUnmounted(() => {
+  closeListener()
+})
+
+// 设置扫描输入框焦点
+const focusScanInput = () => {
+  const input = document.querySelector('.scan-section input') as HTMLInputElement
+  if (input) {
+    input.focus()
+  }
+}
+
+// 扫描条码监听
+const _handlerScan = (code: string) => {
+  if (code) {
+    boxCode.value = code
+    loadBoxData(code)
+  }
+}
 
 // 扫描料箱号
 const boxCode = ref('')
 
+// 监听 boxCode 变化,当有值时调用接口
+watch(boxCode, (newVal) => {
+  if (newVal && newVal.length > 5) {
+    // 防抖处理,避免输入过程中频繁调用
+    loadBoxData(newVal)
+  }
+})
+
+// 加载料箱数据
+const loadBoxData = async (code: string) => {
+  if (!code) return
+
+  try {
+    showLoadingToast({ message: '加载中...', forbidClick: true })
+
+    // 1. 调用 getWorkingDetailsByBox 获取任务详情
+    const res = await getWorkingDetailsByBox(code, warehouse)
+    const boxDetailsList: BoxRelatedMergeDetailsVO[] = res.data || []
+
+    if (boxDetailsList.length === 0) {
+      closeToast()
+      showToast('未找到相关任务')
+      return
+    }
+
+    // 保存返回的数据
+    mergeDataList.value = boxDetailsList
+
+    // 2. 遍历 mergeDetails 取源库位和目标库位,去重
+    const locationSet = new Set<string>()
+    boxDetailsList.forEach(boxDetail => {
+      boxDetail.mergeDetails?.forEach((detail: LocationMergeDetails) => {
+        if (detail.sourceLocation) locationSet.add(detail.sourceLocation)
+        if (detail.targetLocation) locationSet.add(detail.targetLocation)
+      })
+    })
+
+    const locations = Array.from(locationSet)
+
+    if (locations.length > 0) {
+      // 3. 调用 getBoxSplitCode 获取库位分割信息
+      const splitRes = await getBoxSplitCode(warehouse, locations)
+      const splitMap: Record<string, number> = splitRes.data || {}
+
+      // 4. 构建可点击库位信息
+      buildClickableLocationsMap(boxDetailsList)
+
+      // 5. 初始化站台区域
+      updateStationList(boxDetailsList, splitMap)
+    }
+
+    closeToast()
+  } catch (error: any) {
+    closeToast()
+    showToast(error.message || '加载失败')
+  }
+}
+
+// 合并数据列表
+const mergeDataList = ref<BoxRelatedMergeDetailsVO[]>([])
+
+// 构建可点击库位信息Map
+const buildClickableLocationsMap = (boxDetailsList: BoxRelatedMergeDetailsVO[]) => {
+  const map = new Map<string, ClickableLocationInfo>()
+
+  boxDetailsList.forEach(boxDetail => {
+    boxDetail.mergeDetails?.forEach((detail: LocationMergeDetails) => {
+      const sourceLocation = detail.sourceLocation
+      const targetLocation = detail.targetLocation
+      const quantity = detail.moveQty || 0
+
+      // 处理源库位(推荐清空库位)
+      if (sourceLocation) {
+        if (!map.has(sourceLocation)) {
+          map.set(sourceLocation, {
+            recommendType: 'clear',
+            relatedLocations: [],
+            productName: detail.productName || '',
+            barcode: detail.barcode || '',
+            qty: quantity
+          })
+        }
+        const sourceInfo = map.get(sourceLocation)!
+        // 添加对应的保留库位
+        if (targetLocation) {
+          const existingIdx = sourceInfo.relatedLocations.findIndex(r => r.location === targetLocation)
+          if (existingIdx === -1) {
+            sourceInfo.relatedLocations.push({ location: targetLocation, quantity })
+          } else {
+            sourceInfo.relatedLocations[existingIdx].quantity += quantity
+          }
+        }
+      }
+
+      // 处理目标库位(推荐保留库位)
+      if (targetLocation) {
+        if (!map.has(targetLocation)) {
+          map.set(targetLocation, {
+            recommendType: 'keep',
+            relatedLocations: [],
+            productName: detail.productName || '',
+            barcode: detail.barcode || '',
+            qty: 0
+          })
+        }
+        const targetInfo = map.get(targetLocation)!
+        // 添加对应的清空库位
+        if (sourceLocation) {
+          const existingIdx = targetInfo.relatedLocations.findIndex(r => r.location === sourceLocation)
+          if (existingIdx === -1) {
+            targetInfo.relatedLocations.push({ location: sourceLocation, quantity })
+          } else {
+            targetInfo.relatedLocations[existingIdx].quantity += quantity
+          }
+        }
+      }
+    })
+  })
+
+  clickableLocationsMap.value = map
+}
+
 // 商品信息
 const productInfo = reactive({
-  targetLocation: 'HK000001',
+  targetLocation: '',
   stockQty: '',
   productName: '',
   barcode: '',
-  qualityStatus: 'ZP',
-  warehouseType: 'CHZP',
+  qualityStatus: '',
+  warehouseType: '',
   batchNo: '',
   productionDate: '',
   expiryDate: '',
@@ -148,62 +362,126 @@ const confirmMoveQty = () => {
   isEditingMoveQty.value = false
 }
 
-// 料箱数据 - 支持分割成多个库位
+// 站台数据结构
 interface SubLocation {
   id: string
   status: 'empty' | 'filled' | 'selected'
+  locationCode: string // 实际库位编码
 }
 
-interface BoxItem {
+interface StationItem {
   id: number
-  status: 'empty' | 'filled' | 'emptyBox' | 'waiting'
+  stationCode: string // 站台编码 RLOCHK13A01011
+  displayNumber: string // 显示序号 13-24
+  status: 'offline' | 'waiting' | 'filled' | 'emptyBox' | 'error'
   label?: string
-  splitCount?: number // 分割数量: 2, 4, 6, 8
-  subLocations?: SubLocation[] // 子库位
+  splitCount?: number
+  subLocations?: SubLocation[]
+  boxCode?: string
+  inventoryLocations?: string[]
+}
+
+// 站台列表
+const stationList = ref<StationItem[]>([])
+
+// 初始化站台列表(12个站台,编号13-24)
+const initStations = () => {
+  stationList.value = Array.from({ length: 12 }, (_, i) => {
+    const num = String(i + 13).padStart(2, '0')
+    return {
+      id: i + 1,
+      stationCode: `RLOCHK${num}A01011`,
+      displayNumber: num,
+      status: 'offline' as const
+    }
+  })
+}
+
+// 判断库位是否可点击
+const isLocationClickable = (locationCode: string): boolean => {
+  return clickableLocationsMap.value.has(locationCode)
 }
 
 // 生成子库位
-const generateSubLocations = (boxId: number, count: number, filledIndexes: number[] = []): SubLocation[] => {
-  return Array.from({ length: count }, (_, i) => ({
-    id: `${boxId}-${i + 1}`,
-    status: filledIndexes.includes(i) ? 'filled' : 'empty'
-  }))
+const generateSubLocations = (boxCode: string, splitCount: number, inventoryLocations: string[]): SubLocation[] => {
+  return Array.from({ length: splitCount }, (_, i) => {
+    // 库位编码: 分割数为1时库位=料箱编码;否则为 料箱编码-序号
+    const locationCode = splitCount === 1 ? boxCode : `${boxCode}-${i + 1}`
+    return {
+      id: `${boxCode}-${i + 1}`,
+      locationCode: locationCode,
+      status: inventoryLocations.includes(locationCode) ? 'filled' : 'empty'
+    }
+  })
 }
 
-const boxList = ref<BoxItem[]>([
-  { id: 1, status: 'filled' },
-  {
-    id: 2,
-    status: 'empty',
-    splitCount: 2,
-    subLocations: generateSubLocations(2, 2, [0])
-  },
-  {
-    id: 3,
-    status: 'empty',
-    splitCount: 4,
-    subLocations: generateSubLocations(3, 4, [0, 1])
-  },
-  {
-    id: 4,
-    status: 'empty',
-    splitCount: 6,
-    subLocations: generateSubLocations(4, 6, [0, 2])
-  },
-  {
-    id: 5,
-    status: 'empty',
-    splitCount: 8,
-    subLocations: generateSubLocations(5, 8, [0, 3, 5])
-  },
-  { id: 6, status: 'empty' },
-  { id: 7, status: 'filled' },
-  { id: 8, status: 'empty' },
-  { id: 9, status: 'emptyBox', label: '空箱' },
-  { id: 10, status: 'waiting', label: '等待调箱' },
-  { id: 11, status: 'empty' },
-  { id: 12, status: 'empty' }
-])
+// 根据接口数据更新站台列表
+const updateStationList = (boxDetailsList: BoxRelatedMergeDetailsVO[], splitMap: Record<string, number>) => {
+  const stationToBoxMap = new Map<string, BoxRelatedMergeDetailsVO>()
+  boxDetailsList.forEach(boxDetail => {
+    if (boxDetail.station) {
+      stationToBoxMap.set(boxDetail.station, boxDetail)
+    }
+  })
+
+  stationList.value = stationList.value.map(station => {
+    const boxDetail = stationToBoxMap.get(station.stationCode)
+
+    if (!boxDetail) {
+      return { ...station, status: 'offline' as const, label: undefined, splitCount: undefined, subLocations: undefined, boxCode: undefined }
+    }
+
+    const boxCode = boxDetail.boxCode
+    const boxStatus = boxDetail.boxStatus
+    const inventoryLocations = boxDetail.inventoryLocations || []
+
+    let status: StationItem['status'] = 'offline'
+    let label: string | undefined = undefined
+
+    if (boxStatus === 0 || boxStatus === 10) {
+      status = 'waiting'
+      label = '等待调箱'
+    } else if (boxStatus === 20) {
+      if (inventoryLocations.length === 0) {
+        status = 'emptyBox'
+        label = '空箱'
+      } else {
+        status = 'filled'
+      }
+    } else if (boxStatus === 30) {
+      status = 'offline'
+    } else if (boxStatus === 40) {
+      status = 'error'
+      label = '异'
+    }
+
+    // 获取分割数量
+    let splitCount = 1
+    if (splitMap[boxCode]) {
+      splitCount = splitMap[boxCode]
+    } else {
+      boxDetail.mergeDetails?.forEach(detail => {
+        if (detail.sourceLocation && splitMap[detail.sourceLocation]) {
+          splitCount = Math.max(splitCount, splitMap[detail.sourceLocation])
+        }
+        if (detail.targetLocation && splitMap[detail.targetLocation]) {
+          splitCount = Math.max(splitCount, splitMap[detail.targetLocation])
+        }
+      })
+    }
+
+    const updatedStation: StationItem = { ...station, status, label, boxCode, inventoryLocations }
+
+    if (splitCount > 1) {
+      updatedStation.splitCount = splitCount
+      updatedStation.subLocations = generateSubLocations(boxCode, splitCount, inventoryLocations)
+    }
+
+    return updatedStation
+  })
+}
+
+initStations()
 
 // 获取子库位的grid样式
 const getSubGridStyle = (splitCount: number) => {
@@ -225,15 +503,94 @@ const getSubGridStyle = (splitCount: number) => {
 // 选中的料箱/库位
 const selectedBox = ref<string | number | null>(null)
 
-// 选择料箱或子库位
-const selectBox = (box: BoxItem, subId?: string) => {
-  if (box.status === 'waiting' || box.status === 'emptyBox') return
-  selectedBox.value = subId || box.id
+// 可点击库位信息(来自mergeDetails的sourceLocation和targetLocation)
+interface LocationRecommendInfo {
+  location: string
+  quantity: number
+}
+
+interface ClickableLocationInfo {
+  recommendType: 'clear' | 'keep' // clear=推荐清空库位(源), keep=推荐保留库位(目标)
+  relatedLocations: LocationRecommendInfo[] // 对应的推荐库位列表
+  productName: string
+  barcode: string
+  qty: number
+}
+
+// 可点击库位集合 Map<库位编码, 库位推荐信息>
+const clickableLocationsMap = ref<Map<string, ClickableLocationInfo>>(new Map())
+
+// 库位信息弹窗
+const showLocationPopup = ref(false)
+const currentLocation = reactive({
+  id: '',
+  boxId: '',
+  status: '' as 'empty' | 'filled',
+  recommendType: '' as 'clear' | 'keep',
+  productName: '',
+  barcode: '',
+  qty: 0,
+  relatedLocations: [] as LocationRecommendInfo[]
+})
+
+// 选择站台(对于未分割的料箱,库位编码等于料箱编码)
+const selectStation = (station: StationItem) => {
+  if (station.status === 'waiting' || station.status === 'emptyBox' || station.status === 'offline') return
+
+  const locationCode = station.boxCode || ''
+
+  // 检查是否可点击
+  if (!isLocationClickable(locationCode)) {
+    showToast('该库位不在合并任务中')
+    return
+  }
+
+  selectedBox.value = station.stationCode
+
+  // 获取库位推荐信息
+  const locationInfo = clickableLocationsMap.value.get(locationCode)
+
+  // 显示库位信息弹窗
+  currentLocation.id = locationCode
+  currentLocation.boxId = locationCode
+  currentLocation.status = station.status === 'filled' ? 'filled' : 'empty'
+  currentLocation.recommendType = locationInfo?.recommendType || 'keep'
+  currentLocation.productName = locationInfo?.productName || ''
+  currentLocation.barcode = locationInfo?.barcode || ''
+  currentLocation.qty = locationInfo?.qty || 0
+  currentLocation.relatedLocations = locationInfo?.relatedLocations || []
+  showLocationPopup.value = true
 }
 
 // 选择子库位
-const selectSubLocation = (box: BoxItem, sub: SubLocation) => {
-  selectedBox.value = sub.id
+const selectSubLocation = (station: StationItem, sub: SubLocation) => {
+  // 检查是否可点击
+  if (!isLocationClickable(sub.locationCode)) {
+    return
+  }
+
+  selectedBox.value = sub.locationCode
+
+  // 获取库位推荐信息
+  const locationInfo = clickableLocationsMap.value.get(sub.locationCode)
+
+  // 显示库位信息弹窗
+  currentLocation.id = sub.locationCode
+  currentLocation.boxId = station.boxCode || ''
+  currentLocation.status = sub.status === 'filled' ? 'filled' : 'empty'
+  currentLocation.recommendType = locationInfo?.recommendType || 'keep'
+  currentLocation.productName = locationInfo?.productName || ''
+  currentLocation.barcode = locationInfo?.barcode || ''
+  currentLocation.qty = locationInfo?.qty || 0
+  currentLocation.relatedLocations = locationInfo?.relatedLocations || []
+  showLocationPopup.value = true
+}
+
+// 确认选择库位
+const confirmSelectLocation = () => {
+  productInfo.targetLocationNew = currentLocation.id
+  showLocationPopup.value = false
+  showToast(`已选择库位: ${currentLocation.id}`)
 }
 
 // 呼唤机器人
@@ -245,6 +602,12 @@ const callRobot = () => {
 const resetInput = () => {
   boxCode.value = ''
   selectedBox.value = null
+  mergeDataList.value = []
+  clickableLocationsMap.value = new Map()
+  initStations()
+  nextTick(() => {
+    focusScanInput()
+  })
 }
 
 // 提交移库
@@ -288,7 +651,7 @@ const submitMove = () => {
 
     &.row-small .cell {
       font-size: 11px;
-      padding: 6px 4px;
+      padding: 6px 6px;
     }
   }
 
@@ -310,6 +673,10 @@ const submitMove = () => {
       flex: 0 0 55px;
     }
 
+    &.label-small {
+      flex: 0 0 40px;
+    }
+
     &.value {
       color: #333;
 
@@ -403,6 +770,16 @@ const submitMove = () => {
       border-color: #e0b0b0;
     }
 
+    &.box-error {
+      background: #ffcccc;
+      border-color: #ff6666;
+
+      .box-label {
+        color: #ff0000;
+        font-weight: bold;
+      }
+    }
+
     &.box-selected {
       border: 2px solid #1989fa;
     }
@@ -432,6 +809,13 @@ const submitMove = () => {
         &.sub-selected {
           border: 2px solid #1989fa;
         }
+
+        &.sub-disabled {
+          background: #f0f0f0;
+          border-color: #ddd;
+          cursor: not-allowed;
+          opacity: 0.6;
+        }
       }
     }
   }
@@ -477,4 +861,102 @@ const submitMove = () => {
     color: #fff;
   }
 }
+
+.location-popup {
+  padding: 16px;
+
+  .popup-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding-bottom: 12px;
+    border-bottom: 1px solid #eee;
+
+    .popup-title {
+      font-size: 16px;
+      font-weight: 500;
+      color: #333;
+    }
+  }
+
+  .popup-content {
+    padding: 12px 0;
+
+    .info-row {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 10px 0;
+      border-bottom: 1px solid #f5f5f5;
+
+      &:last-child {
+        border-bottom: none;
+      }
+
+      .info-label {
+        font-size: 14px;
+        color: #666;
+      }
+
+      .info-value {
+        font-size: 14px;
+        color: #333;
+      }
+    }
+  }
+
+  .popup-footer {
+    padding-top: 12px;
+  }
+
+  .recommend-section {
+    margin-top: 12px;
+    padding-top: 12px;
+    border-top: 1px solid #eee;
+
+    .recommend-title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+      margin-bottom: 8px;
+    }
+
+    .recommend-list {
+      max-height: 150px;
+      overflow-y: auto;
+    }
+
+    .recommend-item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 8px 12px;
+      background: #f9f9f9;
+      border-radius: 4px;
+      margin-bottom: 6px;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+
+      .recommend-location {
+        font-size: 13px;
+        color: #333;
+        font-weight: 500;
+      }
+
+      .recommend-qty {
+        font-size: 12px;
+        color: #666;
+      }
+    }
+
+    .recommend-empty {
+      font-size: 13px;
+      color: #999;
+      text-align: center;
+      padding: 12px 0;
+    }
+  }
+}
 </style>