Ver código fonte

feat: 库存列表重构为设备维度,详情弹窗基于层模版展示商品并支持增减库存

- 库存列表从商品维度切换为设备维度展示(门店设备、设备ID、门店名称、剩余库存、标准库存、库存状态)
- 新增设备库存详情弹窗,基于层模版 JSON 数据按门/层纵向展示商品
- 详情弹窗每行集成增减库存操作(− 输入数量 +),替换原列表页上货入口
- 修复 device-stats 和 low-stock 查询中 device_id JOIN 的 collation 冲突

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 1 semana atrás
pai
commit
41b9e4f444

+ 2 - 1
haha-admin-web/src/api/inventory.ts

@@ -21,7 +21,8 @@ export const getDeviceInventoryStats = (params: {
   page?: number;
   pageSize?: number;
   deviceId?: string;
-  shopId?: number;
+  shopName?: string;
+  deviceName?: string;
 }) => {
   return http.request<ResultTable>("get", "/inventory/device-stats", {
     params

+ 424 - 0
haha-admin-web/src/views/inventory/components/DeviceInventoryDetail.vue

@@ -0,0 +1,424 @@
+<script setup lang="ts">
+import { ref } from "vue";
+import { message } from "@/utils/message";
+import { getDeviceInventory, increaseStock, adjustStock } from "@/api/inventory";
+import { getLayerTemplateByDeviceId } from "@/api/layer-template";
+import type { FloorConfig, GoodsItem } from "@/api/layer-template";
+import type { DeviceInventoryRecord } from "../utils/types";
+
+/** 合并后的商品展示数据 */
+interface GoodsDisplay {
+  productCode: string;
+  productName: string;
+  productImage: string;
+  templateStock: number;
+  realStock: number;
+  warningThreshold: number;
+  inventoryRecord: DeviceInventoryRecord | null;
+  quantity: number;
+}
+
+/** 按门+层分组后的展示结构 */
+interface FloorDisplay {
+  door: string;
+  floor: number;
+  floorLabel: string;
+  goods: GoodsDisplay[];
+}
+
+const visible = ref(false);
+const deviceId = ref("");
+const deviceName = ref("");
+const loading = ref(false);
+const floorDisplays = ref<FloorDisplay[]>([]);
+const orphanGoods = ref<GoodsDisplay[]>([]);
+
+function buildInventoryMap(records: DeviceInventoryRecord[]): Map<string, DeviceInventoryRecord> {
+  const map = new Map<string, DeviceInventoryRecord>();
+  for (const r of records) {
+    if (r.product_code) {
+      map.set(r.product_code, r);
+    }
+  }
+  return map;
+}
+
+function parseFloors(raw: any): FloorConfig[] {
+  if (!raw) return [];
+  try {
+    const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
+    return Array.isArray(parsed) ? parsed : [];
+  } catch {
+    return [];
+  }
+}
+
+function mergeGoods(
+  goods: GoodsItem[],
+  inventoryMap: Map<string, DeviceInventoryRecord>,
+  usedCodes: Set<string>
+): GoodsDisplay[] {
+  return (goods || []).map(g => {
+    const code = g.key || "";
+    if (code) usedCodes.add(code);
+    const inv = inventoryMap.get(code);
+    return {
+      productCode: code,
+      productName: g.label || inv?.product_name || code,
+      productImage: g.pic || inv?.product_image || "",
+      templateStock: g.stock || 0,
+      realStock: inv?.stock ?? 0,
+      warningThreshold: inv?.warning_threshold ?? 0,
+      inventoryRecord: inv || null,
+      quantity: 1
+    };
+  });
+}
+
+async function loadData() {
+  loading.value = true;
+  try {
+    const [invRes, tmplRes] = await Promise.all([
+      getDeviceInventory(deviceId.value).catch(() => ({ data: [] })),
+      getLayerTemplateByDeviceId(deviceId.value as any).catch(() => ({ data: null }))
+    ]);
+
+    const records = (invRes.data || []) as DeviceInventoryRecord[];
+    const template = tmplRes.data;
+    const inventoryMap = buildInventoryMap(records);
+    const usedCodes = new Set<string>();
+    const displays: FloorDisplay[] = [];
+
+    if (template) {
+      const leftFloors = parseFloors(template.leftFloors);
+      const rightFloors = parseFloors(template.rightFloors);
+      const isDoubleDoor = template.deviceType === 2 && rightFloors.length > 0;
+
+      for (const floor of leftFloors) {
+        displays.push({
+          door: isDoubleDoor ? "左门" : "",
+          floor: floor.floor || 0,
+          floorLabel: `第${floor.floor}层`,
+          goods: mergeGoods(floor.goods || [], inventoryMap, usedCodes)
+        });
+      }
+
+      if (isDoubleDoor) {
+        for (const floor of rightFloors) {
+          displays.push({
+            door: "右门",
+            floor: floor.floor || 0,
+            floorLabel: `第${floor.floor}层`,
+            goods: mergeGoods(floor.goods || [], inventoryMap, usedCodes)
+          });
+        }
+      }
+    }
+
+    const orphans: GoodsDisplay[] = [];
+    for (const r of records) {
+      if (r.product_code && !usedCodes.has(r.product_code)) {
+        orphans.push({
+          productCode: r.product_code,
+          productName: r.product_name,
+          productImage: r.product_image || "",
+          templateStock: 0,
+          realStock: r.stock,
+          warningThreshold: r.warning_threshold,
+          inventoryRecord: r,
+          quantity: 1
+        });
+      }
+    }
+
+    floorDisplays.value = displays;
+    orphanGoods.value = orphans;
+  } catch (e) {
+    console.error("加载设备库存详情失败:", e);
+  } finally {
+    loading.value = false;
+  }
+}
+
+function open(dId: string, dName?: string) {
+  deviceId.value = dId;
+  deviceName.value = dName || "";
+  visible.value = true;
+  loadData();
+}
+
+function handleClose() {
+  visible.value = false;
+  floorDisplays.value = [];
+  orphanGoods.value = [];
+}
+
+async function handleStockIn(row: GoodsDisplay) {
+  const quantity = row.quantity;
+  if (!quantity || quantity <= 0) {
+    message("请输入大于 0 的数量", { type: "warning" });
+    return;
+  }
+  try {
+    await increaseStock({
+      deviceId: deviceId.value,
+      productId: row.inventoryRecord?.product_id || 0,
+      productCode: row.productCode,
+      productName: row.productName,
+      quantity
+    });
+    message("上货成功", { type: "success" });
+    row.quantity = 1;
+    loadData();
+  } catch (e) {
+    console.error("上货失败:", e);
+  }
+}
+
+async function handleStockOut(row: GoodsDisplay) {
+  if (!row.inventoryRecord) {
+    message("该商品无库存记录,无法扣减", { type: "warning" });
+    return;
+  }
+  const quantity = row.quantity;
+  if (!quantity || quantity <= 0) {
+    message("请输入大于 0 的数量", { type: "warning" });
+    return;
+  }
+  if (quantity > row.realStock) {
+    message(`扣减数量不能超过当前库存(${row.realStock})`, { type: "warning" });
+    return;
+  }
+  try {
+    await adjustStock({
+      deviceId: deviceId.value,
+      productId: row.inventoryRecord.product_id,
+      newStock: row.realStock - quantity,
+      remark: "手动扣减"
+    });
+    message("扣减成功", { type: "success" });
+    row.quantity = 1;
+    loadData();
+  } catch (e) {
+    console.error("扣减失败:", e);
+  }
+}
+
+defineExpose({ open });
+</script>
+
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="`设备库存详情 - ${deviceId}`"
+    width="1100px"
+    destroy-on-close
+    :close-on-click-modal="false"
+    @close="handleClose"
+  >
+    <div v-loading="loading">
+      <el-empty
+        v-if="!loading && floorDisplays.length === 0 && orphanGoods.length === 0"
+        description="该设备暂无库存记录"
+      />
+
+      <template v-for="(display, di) in floorDisplays" :key="di">
+        <el-divider content-position="left">
+          {{ display.door ? `${display.door} · ` : "" }}{{ display.floorLabel }}
+        </el-divider>
+
+        <el-table :data="display.goods" border size="small" style="width: 100%">
+          <el-table-column label="商品图片" width="80" align="center">
+            <template #default="{ row }">
+              <el-image
+                v-if="row.productImage"
+                :src="row.productImage"
+                :preview-src-list="[row.productImage]"
+                fit="cover"
+                style="width: 50px; height: 50px; border-radius: 4px"
+                preview-teleported
+              />
+              <span v-else class="text-gray-400 text-xs">无图片</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="商品名称" prop="productName" min-width="130" />
+          <el-table-column label="商品编码" prop="productCode" min-width="100" />
+          <el-table-column label="实时库存" width="90" align="center">
+            <template #default="{ row }">
+              <el-tag
+                :type="row.realStock === 0 ? 'danger' : row.realStock <= row.warningThreshold ? 'warning' : 'success'"
+                size="small"
+              >
+                {{ row.realStock }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="标准库存" width="90" align="center">
+            <template #default="{ row }">
+              {{ row.templateStock || row.warningThreshold || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="增减库存" width="200" align="center" fixed="right">
+            <template #default="{ row }">
+              <div class="stock-ops">
+                <el-button
+                  class="stock-btn stock-btn-minus"
+                  size="small"
+                  circle
+                  :disabled="row.realStock <= 0"
+                  @click="handleStockOut(row)"
+                >
+                  −
+                </el-button>
+                <el-input-number
+                  v-model="row.quantity"
+                  :min="1"
+                  :max="9999"
+                  controls-position="right"
+                  size="small"
+                  class="stock-input"
+                />
+                <el-button
+                  class="stock-btn stock-btn-plus"
+                  size="small"
+                  circle
+                  @click="handleStockIn(row)"
+                >
+                  +
+                </el-button>
+              </div>
+            </template>
+          </el-table-column>
+        </el-table>
+      </template>
+
+      <!-- 未分配商品 -->
+      <template v-if="orphanGoods.length > 0">
+        <el-divider content-position="left">未在模版中的商品</el-divider>
+        <el-table :data="orphanGoods" border size="small" style="width: 100%">
+          <el-table-column label="商品图片" width="80" align="center">
+            <template #default="{ row }">
+              <el-image
+                v-if="row.productImage"
+                :src="row.productImage"
+                :preview-src-list="[row.productImage]"
+                fit="cover"
+                style="width: 50px; height: 50px; border-radius: 4px"
+                preview-teleported
+              />
+              <span v-else class="text-gray-400 text-xs">无图片</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="商品名称" prop="productName" min-width="130" />
+          <el-table-column label="商品编码" prop="productCode" min-width="100" />
+          <el-table-column label="实时库存" width="90" align="center">
+            <template #default="{ row }">
+              <el-tag
+                :type="row.realStock === 0 ? 'danger' : row.realStock <= row.warningThreshold ? 'warning' : 'success'"
+                size="small"
+              >
+                {{ row.realStock }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column label="标准库存" width="90" align="center">
+            <template #default="{ row }">
+              {{ row.warningThreshold || '-' }}
+            </template>
+          </el-table-column>
+          <el-table-column label="增减库存" width="200" align="center" fixed="right">
+            <template #default="{ row }">
+              <div class="stock-ops">
+                <el-button
+                  class="stock-btn stock-btn-minus"
+                  size="small"
+                  circle
+                  :disabled="row.realStock <= 0"
+                  @click="handleStockOut(row)"
+                >
+                  −
+                </el-button>
+                <el-input-number
+                  v-model="row.quantity"
+                  :min="1"
+                  :max="9999"
+                  controls-position="right"
+                  size="small"
+                  class="stock-input"
+                />
+                <el-button
+                  class="stock-btn stock-btn-plus"
+                  size="small"
+                  circle
+                  @click="handleStockIn(row)"
+                >
+                  +
+                </el-button>
+              </div>
+            </template>
+          </el-table-column>
+        </el-table>
+      </template>
+    </div>
+
+    <template #footer>
+      <el-button @click="handleClose">关闭</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<style lang="scss" scoped>
+:deep(.el-divider__text) {
+  font-size: 15px;
+  font-weight: 600;
+  color: var(--el-color-primary);
+}
+
+.stock-ops {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 4px;
+}
+
+.stock-input {
+  width: 100px;
+}
+
+.stock-btn {
+  width: 26px;
+  height: 26px;
+  padding: 0;
+  font-size: 15px;
+  font-weight: 600;
+  line-height: 1;
+  flex-shrink: 0;
+}
+
+.stock-btn-plus {
+  color: #fff;
+  background-color: #67c23a;
+  border-color: #67c23a;
+
+  &:hover {
+    background-color: #85ce61;
+    border-color: #85ce61;
+  }
+}
+
+.stock-btn-minus {
+  color: #fff;
+  background-color: #f56c6c;
+  border-color: #f56c6c;
+
+  &:hover {
+    background-color: #f78989;
+    border-color: #f78989;
+  }
+
+  &.is-disabled {
+    background-color: #fab6b6;
+    border-color: #fab6b6;
+  }
+}
+</style>

+ 21 - 33
haha-admin-web/src/views/inventory/index.vue

@@ -4,8 +4,8 @@ import { useInventory } from "./utils/hook";
 import { PureTableBar } from "@/components/RePureTableBar";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
 import Refresh from "~icons/ep/refresh";
-import AddFill from "~icons/ri/add-circle-line";
-import EditPen from "~icons/ep/edit-pen";
+import View from "~icons/ep/view";
+import DeviceInventoryDetail from "./components/DeviceInventoryDetail.vue";
 
 defineOptions({
   name: "InventoryManage"
@@ -13,6 +13,7 @@ defineOptions({
 
 const formRef = ref();
 const tableRef = ref();
+const detailDialogRef = ref();
 
 const {
   form,
@@ -21,11 +22,8 @@ const {
   dataList,
   pagination,
   deviceOptions,
-  productOptions,
   onSearch,
   resetForm,
-  handleStockIn,
-  handleAdjust,
   handleSizeChange,
   handleCurrentChange
 } = useInventory(tableRef);
@@ -55,24 +53,21 @@ const {
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="商品:" prop="productId">
-        <el-select
-          v-model="form.productId"
-          placeholder="请选择商品"
+      <el-form-item label="设备名称:" prop="deviceName">
+        <el-input
+          v-model="form.deviceName"
+          placeholder="请输入设备名称"
           clearable
-          filterable
           class="w-[180px]!"
-        >
-          <el-option
-            v-for="item in productOptions"
-            :key="item.id"
-            :label="item.name"
-            :value="item.id"
-          />
-        </el-select>
+        />
       </el-form-item>
-      <el-form-item label="低库存:" prop="lowStock">
-        <el-checkbox v-model="form.lowStock">仅显示低库存</el-checkbox>
+      <el-form-item label="门店名称:" prop="shopName">
+        <el-input
+          v-model="form.shopName"
+          placeholder="请输入门店名称"
+          clearable
+          class="w-[180px]!"
+        />
       </el-form-item>
       <el-form-item>
         <el-button
@@ -94,19 +89,10 @@ const {
       :columns="columns"
       @refresh="onSearch"
     >
-      <template #buttons>
-        <el-button
-          type="primary"
-          :icon="useRenderIcon(AddFill)"
-          @click="handleStockIn"
-        >
-          上货
-        </el-button>
-      </template>
       <template v-slot="{ size, dynamicColumns }">
         <pure-table
           ref="tableRef"
-          row-key="id"
+          row-key="device_id"
           adaptive
           :adaptiveConfig="{ offsetBottom: 108 }"
           align-whole="center"
@@ -129,15 +115,17 @@ const {
               link
               type="primary"
               :size="size"
-              :icon="useRenderIcon(EditPen)"
-              @click="handleAdjust(row)"
+              :icon="useRenderIcon(View)"
+              @click="detailDialogRef.open(row.device_id, row.device_name)"
             >
-              调整
+              查看详情
             </el-button>
           </template>
         </pure-table>
       </template>
     </PureTableBar>
+
+    <DeviceInventoryDetail ref="detailDialogRef" />
   </div>
 </template>
 

+ 53 - 197
haha-admin-web/src/views/inventory/utils/hook.tsx

@@ -1,38 +1,19 @@
-import dayjs from "dayjs";
-import { message } from "@/utils/message";
-import { addDialog } from "@/components/ReDialog";
 import type { PaginationProps } from "@pureadmin/table";
-import { deviceDetection } from "@pureadmin/utils";
-import {
-  getInventoryList,
-  increaseStock,
-  adjustStock
-} from "@/api/inventory";
-import { getProductList } from "@/api/product";
+import { getDeviceInventoryStats } from "@/api/inventory";
 import { getDeviceList } from "@/api/device";
-import { type Ref, ref, toRaw, reactive, onMounted } from "vue";
-import {
-  ElForm,
-  ElFormItem,
-  ElInput,
-  ElInputNumber,
-  ElSelect,
-  ElOption
-} from "element-plus";
-import type { InventoryItem, InventorySearchForm, StockInForm, StockAdjustForm } from "./types";
+import { type Ref, ref, reactive, onMounted } from "vue";
+import type { InventorySearchForm, DeviceInventoryStats } from "./types";
 
 export function useInventory(tableRef: Ref) {
   const form = reactive<InventorySearchForm>({
     deviceId: "",
-    productId: "",
-    lowStock: false
+    shopName: "",
+    deviceName: ""
   });
   const formRef = ref();
-  const ruleFormRef = ref();
-  const dataList = ref([]);
+  const dataList = ref<DeviceInventoryStats[]>([]);
   const loading = ref(true);
   const deviceOptions = ref([]);
-  const productOptions = ref([]);
   const pagination = reactive<PaginationProps>({
     total: 0,
     pageSize: 10,
@@ -42,61 +23,69 @@ export function useInventory(tableRef: Ref) {
 
   const columns: TableColumnList = [
     {
-      label: "设备ID",
-      prop: "deviceId",
-      minWidth: 120
-    },
-    {
-      label: "设备名称",
-      prop: "deviceName",
-      minWidth: 120
+      label: "门店设备",
+      prop: "device_name",
+      minWidth: 150
     },
     {
-      label: "商品编码",
-      prop: "productCode",
-      minWidth: 120
+      label: "设备ID",
+      prop: "device_id",
+      minWidth: 130
     },
     {
-      label: "商品名称",
-      prop: "productName",
+      label: "门店名称",
+      prop: "shop_name",
       minWidth: 150
     },
     {
-      label: "库存数量",
-      prop: "stock",
+      label: "剩余库存",
+      prop: "remaining_stock",
       minWidth: 100,
       cellRenderer: ({ row }) => (
-        <el-tag type={row.stock <= 5 ? "danger" : row.stock <= 10 ? "warning" : "success"}>
-          {row.stock}
+        <el-tag
+          type={
+            row.stock_status === "缺货"
+              ? "danger"
+              : row.stock_status === "低库存"
+                ? "warning"
+                : "success"
+          }
+        >
+          {row.remaining_stock}
         </el-tag>
       )
     },
     {
-      label: "货架号",
-      prop: "shelfNum",
-      minWidth: 80
-    },
-    {
-      label: "位置",
-      prop: "position",
-      minWidth: 80
+      label: "标准库存",
+      prop: "standard_stock",
+      minWidth: 100
     },
     {
-      label: "更新时间",
-      prop: "lastUpdateTime",
-      minWidth: 160,
-      formatter: ({ lastUpdateTime }) =>
-        lastUpdateTime ? dayjs(lastUpdateTime).format("YYYY-MM-DD HH:mm:ss") : "-"
+      label: "库存状态",
+      prop: "stock_status",
+      minWidth: 100,
+      cellRenderer: ({ row }) => (
+        <el-tag
+          type={
+            row.stock_status === "缺货"
+              ? "danger"
+              : row.stock_status === "低库存"
+                ? "warning"
+                : "success"
+          }
+        >
+          {row.stock_status}
+        </el-tag>
+      )
     },
     {
       label: "操作",
       fixed: "right",
-      width: 150,
+      width: 100,
       slot: "operation"
     }
   ];
 
-  // 搜索
   async function onSearch() {
     loading.value = true;
     try {
@@ -104,15 +93,14 @@ export function useInventory(tableRef: Ref) {
         page: pagination.currentPage,
         pageSize: pagination.pageSize
       };
-      
-      // 只添加非空的搜索条件
+
       if (form.deviceId) searchParams.deviceId = form.deviceId;
-      if (form.productId) searchParams.productId = form.productId;
-      if (form.lowStock) searchParams.lowStock = form.lowStock;
-      
-      const { data } = await getInventoryList(searchParams);
+      if (form.shopName) searchParams.shopName = form.shopName;
+      if (form.deviceName) searchParams.deviceName = form.deviceName;
+
+      const { data } = await getDeviceInventoryStats(searchParams);
       if (data) {
-        dataList.value = data.list || [];
+        dataList.value = (data.list || []) as DeviceInventoryStats[];
         pagination.total = Number(data.total) || 0;
       }
     } catch (error) {
@@ -124,7 +112,6 @@ export function useInventory(tableRef: Ref) {
     }
   }
 
-  // 重置表单
   const resetForm = formEl => {
     if (!formEl) return;
     formEl.resetFields();
@@ -132,7 +119,6 @@ export function useInventory(tableRef: Ref) {
     onSearch();
   };
 
-  // 分页
   function handleSizeChange(val: number) {
     pagination.pageSize = val;
     onSearch();
@@ -143,123 +129,6 @@ export function useInventory(tableRef: Ref) {
     onSearch();
   }
 
-  // 上货
-  const stockInForm = reactive<StockInForm>({
-    deviceId: "",
-    productId: 0,
-    productCode: "",
-    productName: "",
-    quantity: 1,
-    shelfNum: 1,
-    position: ""
-  });
-
-  function handleStockIn() {
-    addDialog({
-      title: "上货",
-      width: "40%",
-      draggable: true,
-      fullscreen: deviceDetection(),
-      closeOnClickModal: false,
-      contentRenderer: () => (
-        <ElForm ref={ruleFormRef} model={stockInForm} label-width="100px">
-          <ElFormItem label="设备" prop="deviceId" rules={[{ required: true, message: "请选择设备", trigger: "change" }]}>
-            <ElSelect v-model={stockInForm.deviceId} placeholder="请选择设备" class="w-full!">
-              {deviceOptions.value.map(item => (
-                <ElOption key={item.deviceId} label={item.shopName ? `${item.deviceId} — ${item.shopName}` : item.deviceId} value={item.deviceId} />
-              ))}
-            </ElSelect>
-          </ElFormItem>
-          <ElFormItem label="商品" prop="productId" rules={[{ required: true, message: "请选择商品", trigger: "change" }]}>
-            <ElSelect v-model={stockInForm.productId} placeholder="请选择商品" class="w-full!">
-              {productOptions.value.map(item => (
-                <ElOption key={item.id} label={item.name} value={item.id} />
-              ))}
-            </ElSelect>
-          </ElFormItem>
-          <ElFormItem label="数量" prop="quantity" rules={[{ required: true, message: "请输入数量", trigger: "blur" }]}>
-            <ElInputNumber v-model={stockInForm.quantity} min={1} class="w-full!" />
-          </ElFormItem>
-          <ElFormItem label="货架号" prop="shelfNum">
-            <ElInputNumber v-model={stockInForm.shelfNum} min={1} class="w-full!" />
-          </ElFormItem>
-          <ElFormItem label="位置" prop="position">
-            <ElInput v-model={stockInForm.position} placeholder="请输入位置" clearable />
-          </ElFormItem>
-        </ElForm>
-      ),
-      beforeSure: async (done) => {
-        const valid = await ruleFormRef.value.validate().catch(() => false);
-        if (!valid) return;
-
-        try {
-          const res = await increaseStock(toRaw(stockInForm));
-          if (res.code === 200) {
-            message("上货成功", { type: "success" });
-            done();
-            onSearch();
-          } else {
-            message(res.message || "上货失败", { type: "error" });
-          }
-        } catch (error) {
-          message("上货失败", { type: "error" });
-        }
-      }
-    });
-  }
-
-  // 库存调整
-  const adjustForm = reactive<StockAdjustForm>({
-    deviceId: "",
-    productId: 0,
-    newStock: 0,
-    remark: ""
-  });
-
-  function handleAdjust(row: InventoryItem) {
-    adjustForm.deviceId = row.deviceId;
-    adjustForm.productId = row.productId;
-    adjustForm.newStock = row.stock;
-    adjustForm.remark = "";
-
-    addDialog({
-      title: "库存调整",
-      width: "30%",
-      draggable: true,
-      fullscreen: deviceDetection(),
-      closeOnClickModal: false,
-      contentRenderer: () => (
-        <ElForm ref={ruleFormRef} model={adjustForm} label-width="100px">
-          <ElFormItem label="当前库存">{row.stock}</ElFormItem>
-          <ElFormItem label="调整后库存" prop="newStock" rules={[{ required: true, message: "请输入调整后库存", trigger: "blur" }]}>
-            <ElInputNumber v-model={adjustForm.newStock} min={0} class="w-full!" />
-          </ElFormItem>
-          <ElFormItem label="备注" prop="remark">
-            <ElInput v-model={adjustForm.remark} type="textarea" placeholder="请输入备注" rows={3} />
-          </ElFormItem>
-        </ElForm>
-      ),
-      beforeSure: async (done) => {
-        const valid = await ruleFormRef.value.validate().catch(() => false);
-        if (!valid) return;
-
-        try {
-          const res = await adjustStock(toRaw(adjustForm));
-          if (res.code === 200) {
-            message("库存调整成功", { type: "success" });
-            done();
-            onSearch();
-          } else {
-            message(res.message || "调整失败", { type: "error" });
-          }
-        } catch (error) {
-          message("调整失败", { type: "error" });
-        }
-      }
-    });
-  }
-
-  // 获取设备列表
   async function fetchDeviceOptions() {
     try {
       const { data } = await getDeviceList({ page: 1, pageSize: 1000 });
@@ -269,18 +138,8 @@ export function useInventory(tableRef: Ref) {
     }
   }
 
-  // 获取商品列表
-  async function fetchProductOptions() {
-    try {
-      const { data } = await getProductList({ page: 1, pageSize: 1000 });
-      productOptions.value = data.list || [];
-    } catch (error) {
-      console.error("获取商品列表失败:", error);
-    }
-  }
-
   onMounted(async () => {
-    await Promise.all([fetchDeviceOptions(), fetchProductOptions()]);
+    await fetchDeviceOptions();
     onSearch();
   });
 
@@ -291,11 +150,8 @@ export function useInventory(tableRef: Ref) {
     dataList,
     pagination,
     deviceOptions,
-    productOptions,
     onSearch,
     resetForm,
-    handleStockIn,
-    handleAdjust,
     handleSizeChange,
     handleCurrentChange
   };

+ 48 - 3
haha-admin-web/src/views/inventory/utils/types.ts

@@ -1,4 +1,49 @@
-// 库存信息类型
+// 设备维度库存统计类型(对应 /inventory/device-stats 返回的 snake_case 字段)
+export interface DeviceInventoryStats {
+  device_id_primary: number;
+  device_id: string;
+  device_name: string;
+  shop_id: number;
+  shop_name: string;
+  shop_address: string;
+  remaining_stock: number;
+  standard_stock: number;
+  product_count: number;
+  zero_stock_count: number;
+  low_stock_count: number;
+  stock_status: string;
+  stock_ratio: number;
+  has_unknown_product: boolean;
+  last_restock_time: string;
+}
+
+// 设备库存记录(含商品详情,对应 /inventory/device/{deviceId} 返回的 snake_case 字段)
+export interface DeviceInventoryRecord {
+  id: number;
+  device_id: string;
+  product_id: number;
+  product_name: string;
+  product_code: string;
+  stock: number;
+  warning_threshold: number;
+  shelf_num: number;
+  position: string;
+  last_restock_time: string;
+  update_time: string;
+  barcode: string;
+  product_image: string;
+  cost_price: number;
+  retail_price: number;
+}
+
+// 按层分组的库存
+export interface ShelfGroup {
+  shelfNum: number;
+  label: string;
+  products: DeviceInventoryRecord[];
+}
+
+// 库存信息类型(保留,供上货等弹窗使用)
 export interface InventoryItem {
   id: number;
   deviceId: string;
@@ -15,8 +60,8 @@ export interface InventoryItem {
 // 搜索表单类型
 export interface InventorySearchForm {
   deviceId: string;
-  productId: number | string;
-  lowStock: boolean;
+  shopName: string;
+  deviceName: string;
 }
 
 // 上货表单类型

+ 2 - 2
haha-mapper/src/main/java/com/haha/mapper/DeviceInventoryMapper.java

@@ -38,7 +38,7 @@ public interface DeviceInventoryMapper extends BaseMapper<DeviceInventory> {
      */
     @Select("SELECT di.*, d.name as device_name, p.barcode " +
             "FROM t_device_inventory di " +
-            "LEFT JOIN t_device d ON di.device_id = d.device_id " +
+            "LEFT JOIN t_device d ON di.device_id COLLATE utf8mb4_unicode_ci = d.device_id COLLATE utf8mb4_unicode_ci " +
             "LEFT JOIN t_product p ON di.product_id = p.id " +
             "WHERE di.stock <= di.warning_threshold " +
             "ORDER BY di.stock ASC")
@@ -65,7 +65,7 @@ public interface DeviceInventoryMapper extends BaseMapper<DeviceInventory> {
             "  SUM(CASE WHEN di.product_id IS NULL OR di.product_id = 0 THEN 1 ELSE 0 END) as unknown_product_count " +
             "FROM t_device d " +
             "LEFT JOIN t_shop s ON d.shop_id = s.id " +
-            "LEFT JOIN t_device_inventory di ON d.device_id = di.device_id " +
+            "LEFT JOIN t_device_inventory di ON d.device_id COLLATE utf8mb4_unicode_ci = di.device_id COLLATE utf8mb4_unicode_ci " +
             "WHERE 1=1 " +
             "<if test='shopName != null and shopName != \"\"'> " +
             "  AND s.name LIKE CONCAT('%', #{shopName}, '%') " +