Sfoglia il codice sorgente

设备商品层模版

skyline 1 mese fa
parent
commit
fcc42ba89c

+ 85 - 66
haha-admin-web/src/api/layer-template.ts

@@ -24,101 +24,120 @@ type ResultTable = {
 };
 
 /**
- * 楼层配置类型定义
- * FloorConfig type definition for layer template floor configuration
+ * 商品项类型定义(对应哈哈平台层模板中的商品数据结构)
+ * GoodsItem type definition matching Haha platform layer template product structure
+ */
+export interface GoodsItem {
+  /** 商品code(哈哈平台商品编码) / Product code from Haha platform */
+  key?: string;
+
+  /** 商品名称 / Product name */
+  label?: string;
+
+  /** 库存/标准库存 / Stock quantity */
+  stock?: number;
+
+  /** 商品分类 / Product category */
+  type?: string;
+
+  /** 商品图片URL / Product image URL */
+  pic?: string;
+
+  /** 商品售价 / Product selling price */
+  c_price?: string;
+
+  /** 是否折扣 / Whether on discount */
+  discount?: string;
+
+  /** 优惠价/活动价 / Discount price */
+  dis_price?: string;
+
+  /** 算法识别图 / Algorithm recognition image */
+  rec_pic?: string;
+}
+
+/**
+ * 楼层配置类型定义(对应哈哈平台层模板中每一层的数据结构)
+ * FloorConfig type definition matching Haha platform layer template floor structure
  */
 export interface FloorConfig {
-  /** 楼层ID / Floor ID */
-  id?: number;
+  /** 层号(从1开始) / Floor number (starting from 1) */
+  floor?: number;
 
-  /** 楼层名称 / Floor name */
-  name?: string;
-
-  /** 楼层编号 / Floor number */
-  floorNumber?: number;
-
-  /** 商品列表 / Product list on this floor */
-  products?: Array<{
-    /** 商品ID / Product ID */
-    productId?: number;
-    
-    /** 商品名称 / Product name */
-    productName?: string;
-    
-    /** 商品条码 / Product barcode */
-    barcode?: string;
-    
-    /** 数量 / Quantity */
-    quantity?: number;
-    
-    /** 排序顺序 / Sort order */
-    sortOrder?: number;
-  }>;
-
-  /** 最大容量 / Maximum capacity */
-  maxCapacity?: number;
-
-  /** 当前使用数量 / Current usage count */
-  currentCount?: number;
-
-  /** 状态 (0-禁用, 1-启用) / Status (0-disabled, 1-enabled) */
-  status?: number;
+  /** 该层商品列表 / Product list on this floor */
+  goods?: GoodsItem[];
 }
 
 /**
- * 层模版类型定义
- * LayerTemplate type definition for product layer template management
+ * 层模版类型定义(匹配后端 LayerTemplate 实体)
+ * LayerTemplate type definition matching backend LayerTemplate entity
  */
 export interface LayerTemplate {
   /** 主键ID / Primary key ID */
   id?: number;
 
-  /** 模版名称 / Template name */
+  /** 模板ID(哈哈平台返回) / Template ID from Haha platform */
+  templateId?: string;
+
+  /** 设备ID(SN号) / Device ID (SN number) */
+  deviceId?: string;
+
+  /** 模板名称 / Template name */
   templateName?: string;
 
-  /** 模版编码 / Template code */
-  templateCode?: string;
+  /** 设备类型 / Device type */
+  deviceType?: number;
 
-  /** 设备ID / Device ID */
-  deviceId?: number;
+  /** 机柜层数 / Cabinet shelf number */
+  shelfNum?: number;
 
-  /** 设备名称 / Device name */
-  deviceName?: string;
+  /** 左门层数据(JSON字符串) / Left door floor data (JSON string) */
+  leftFloors?: string;
 
-  /** 设备编码 / Device code */
-  deviceCode?: string;
+  /** 右门层数据(JSON字符串) / Right door floor data (JSON string) */
+  rightFloors?: string;
+
+  /** 商品规则 / Goods rule */
+  goodsRule?: string;
+
+  /** 同步状态 (0-未同步, 1-同步中, 2-已同步, 3-同步失败) */
+  syncStatus?: number;
+
+  /** 同步状态标签 / Sync status label */
+  syncStatusLabel?: string;
+
+  /** 同步状态颜色 / Sync status color */
+  syncStatusColor?: string;
 
-  /** 楼层配置列表 / Floor configuration list */
-  floorConfigs?: FloorConfig[];
+  /** 设备类型名称 / Device type name */
+  deviceTypeName?: string;
+
+  /** 左门层数据展示(格式化JSON) / Left door floor data display */
+  leftFloorsDisplay?: string;
+
+  /** 右门层数据展示(格式化JSON) / Right door floor data display */
+  rightFloorsDisplay?: string;
 
   /** 总楼层数 / Total floor count */
   totalFloors?: number;
 
-  /** 同步状态 (0-未同步, 1-已同步, 2-同步中, 3-同步失败) 
-   * Sync status (0-not synced, 1-synced, 2-syncing, 3-sync failed)
-   */
-  syncStatus?: number;
-
   /** 最后同步时间 / Last sync time */
   lastSyncTime?: string;
 
-  /** 状态 (0-禁用, 1-启用) / Status (0-disabled, 1-enabled) */
-  status?: number;
+  /** 商品规则展示 / Goods rule display */
+  goodsRuleDisplay?: string;
+
+  /** 最后同步时间 / Sync time */
+  syncTime?: string;
 
-  /** 备注 / Remarks */
-  remark?: string;
+  /** 状态文本 / Status text */
+  statusText?: string;
 
   /** 创建时间 / Create time */
   createTime?: string;
 
   /** 更新时间 / Update time */
   updateTime?: string;
-
-  /** 创建人 / Creator */
-  createBy?: string;
-
-  /** 更新人 / Updater */
-  updateBy?: string;
 }
 
 /**
@@ -193,7 +212,7 @@ export const getLayerTemplateByDeviceId = (deviceId: number) => {
  * @param data - 层模版数据 / Layer template data
  * @returns Promise<Result> - 创建结果的响应 / Response result of creation
  */
-export const createLayerTemplate = (data: Partial<LayerTemplate>) => {
+export const createLayerTemplate = (data: any) => {
   return http.request<Result>("post", "/layer-templates", { data });
 };
 
@@ -204,7 +223,7 @@ export const createLayerTemplate = (data: Partial<LayerTemplate>) => {
  * @param data - 更新数据 / Update data
  * @returns Promise<Result> - 更新结果的响应 / Response result of update
  */
-export const updateLayerTemplate = (id: number, data: Partial<LayerTemplate>) => {
+export const updateLayerTemplate = (id: number, data: any) => {
   return http.request<Result>("put", `/layer-templates/${id}`, { data });
 };
 

+ 1 - 1
haha-admin-web/src/router/modules/layer-template.ts

@@ -3,7 +3,7 @@ export default {
   component: () => import("@/views/layer-template/index.vue"),
   name: "LayerTemplate",
   meta: {
-    title: "层模版管理",
+    title: "设备层模版管理",
     icon: "ri:stack-line",
     rank: 8
   }

+ 485 - 308
haha-admin-web/src/views/layer-template/components/EditDialog.vue

@@ -6,31 +6,25 @@ import {
   createLayerTemplate,
   updateLayerTemplate
 } from "@/api/layer-template";
-import { getProductList } from "@/api/product";
+import type { FloorConfig, GoodsItem } from "@/api/layer-template";
 import type { FormInstance, FormRules } from "element-plus";
 
-/**
- * 楼层配置项类型定义
- */
-interface FloorItem {
-  /** 层号 / Floor number */
-  floor: number;
-  /** 该层选择的商品ID数组 / Selected product IDs for this floor */
-  goods: number[];
-}
-
 /**
  * 表单数据类型
  */
 interface FormData {
-  /** 设备ID / Device ID */
-  deviceId: string | number;
-  /** 模板名称 / Template name */
+  /** 模板名称 */
   templateName: string;
-  /** 左门楼层配置 / Left door floor configurations */
-  leftFloors: FloorItem[];
-  /** 右门楼层配置 / Right door floor configurations */
-  rightFloors: FloorItem[];
+  /** 设备类型: 1-静态柜(单门), 2-动态柜(双门) */
+  deviceType: number;
+  /** 模板类型: 1-层模板 */
+  templateType: number;
+  /** 设备层数 */
+  shelfNum: number;
+  /** 左门楼层配置 */
+  leftFloors: FloorConfig[];
+  /** 右门楼层配置 */
+  rightFloors: FloorConfig[];
 }
 
 const emit = defineEmits<{
@@ -45,36 +39,28 @@ const title = ref("新增层模版");
 const isEdit = computed(() => title.value === "修改");
 // 编辑时的记录ID
 const editId = ref<number | null>(null);
+// 编辑时的设备ID
+const editDeviceId = ref<string>("");
 
 // 表单引用
 const formRef = ref<FormInstance>();
-// 商品列表数据
-const productList = ref<any[]>([]);
-// 商品加载状态
-const productLoading = ref(false);
+// 当前选中的层 Tab
+const activeFloorTab = ref("left-1");
+// 右门当前选中的层 Tab
+const activeRightFloorTab = ref("right-1");
 
 // 表单数据
 const form = reactive<FormData>({
-  deviceId: "",
   templateName: "",
+  deviceType: 1,
+  templateType: 1,
+  shelfNum: 5,
   leftFloors: [],
   rightFloors: []
 });
 
 // 表单校验规则
 const rules = computed<FormRules>(() => ({
-  deviceId: [
-    {
-      required: true,
-      message: "请输入设备ID",
-      trigger: "blur"
-    },
-    {
-      pattern: /^[0-9]+$/,
-      message: "设备ID必须为数字",
-      trigger: "blur"
-    }
-  ],
   templateName: [
     {
       required: true,
@@ -87,97 +73,182 @@ const rules = computed<FormRules>(() => ({
       message: "模板名称长度在2到50个字符之间",
       trigger: "blur"
     }
+  ],
+  shelfNum: [
+    {
+      required: true,
+      message: "请输入设备层数",
+      trigger: "blur"
+    }
   ]
 }));
 
 /**
- * 获取商品列表
- * Fetch product list for selection
+ * 是否为双门柜
  */
-async function fetchProductList() {
-  productLoading.value = true;
-  try {
-    const { data } = await getProductList({
-      page: 1,
-      pageSize: 1000 // 获取足够多的商品供选择
-    });
-    productList.value = data.list || [];
-  } catch (error) {
-    console.error("获取商品列表失败:", error);
-    message("获取商品列表失败", { type: "error" });
-  } finally {
-    productLoading.value = false;
-  }
-}
+const isDoubleDoor = computed(() => form.deviceType === 2);
 
 /**
  * 重置表单数据
- * Reset form data to initial state
  */
 function resetForm() {
-  form.deviceId = "";
   form.templateName = "";
+  form.deviceType = 1;
+  form.templateType = 1;
+  form.shelfNum = 5;
   form.leftFloors = [];
   form.rightFloors = [];
   editId.value = null;
+  editDeviceId.value = "";
+  activeFloorTab.value = "left-1";
+  activeRightFloorTab.value = "right-1";
   nextTick(() => {
     formRef.value?.clearValidate();
   });
 }
 
+/**
+ * 根据 shelfNum 初始化层数据
+ * 当用户修改层数时,自动调整层数据
+ */
+function initFloorsByShelfNum() {
+  const num = form.shelfNum || 0;
+
+  // 调整左门层数
+  while (form.leftFloors.length < num) {
+    form.leftFloors.push({
+      floor: form.leftFloors.length + 1,
+      goods: []
+    });
+  }
+  while (form.leftFloors.length > num) {
+    form.leftFloors.pop();
+  }
+
+  // 调整右门层数
+  if (isDoubleDoor.value) {
+    while (form.rightFloors.length < num) {
+      form.rightFloors.push({
+        floor: form.rightFloors.length + 1,
+        goods: []
+      });
+    }
+    while (form.rightFloors.length > num) {
+      form.rightFloors.pop();
+    }
+  } else {
+    form.rightFloors = [];
+  }
+
+  // 更新默认 Tab
+  if (form.leftFloors.length > 0) {
+    activeFloorTab.value = `left-${form.leftFloors[0].floor}`;
+  }
+  if (form.rightFloors.length > 0) {
+    activeRightFloorTab.value = `right-${form.rightFloors[0].floor}`;
+  }
+}
+
+/**
+ * 监听 shelfNum 变化,自动调整层数据
+ */
+watch(
+  () => form.shelfNum,
+  () => {
+    if (form.shelfNum > 0 && form.shelfNum <= 20) {
+      initFloorsByShelfNum();
+    }
+  }
+);
+
+/**
+ * 监听设备类型变化
+ */
+watch(
+  () => form.deviceType,
+  () => {
+    initFloorsByShelfNum();
+  }
+);
+
 /**
  * 打开对话框
- * Open dialog
- * @param dialogTitle - 对话框标题
- * @param row - 编辑时的行数据(可选)
  */
 async function open(dialogTitle?: string, row?: any) {
   title.value = dialogTitle || "新增层模版";
   resetForm();
   visible.value = true;
 
-  // 加载商品列表
-  await fetchProductList();
-
   // 如果是编辑模式,回显数据
   if (row && row.id) {
     editId.value = row.id;
+    editDeviceId.value = row.deviceId || "";
     await loadTemplateData(row.id);
+  } else {
+    // 新增模式,初始化默认层数据
+    initFloorsByShelfNum();
   }
 }
 
 /**
  * 加载模版详情数据
- * Load template detail data by ID
- * @param id - 模版ID
  */
 async function loadTemplateData(id: number) {
   try {
-    const { data } = await getLayerTemplateById(id);
+    const res = await getLayerTemplateById(id);
+    const data = res.data;
     if (data) {
-      form.deviceId = data.deviceId || "";
       form.templateName = data.templateName || "";
+      form.deviceType = data.deviceType || 1;
+      form.shelfNum = data.shelfNum || 5;
+
+      // 解析左门层数据(JSON字符串 -> FloorConfig数组)
+      if (data.leftFloors) {
+        try {
+          const parsed = typeof data.leftFloors === "string"
+            ? JSON.parse(data.leftFloors)
+            : data.leftFloors;
+          if (Array.isArray(parsed)) {
+            form.leftFloors = parsed.map((item: any) => ({
+              floor: typeof item.floor === "string" ? parseInt(item.floor, 10) : (item.floor || 0),
+              goods: Array.isArray(item.goods) ? item.goods : []
+            }));
+          }
+        } catch (e) {
+          console.error("解析左门层数据失败:", e);
+          form.leftFloors = [];
+        }
+      }
 
-      // 解析左右门楼层配置
-      if (data.floorConfigs && Array.isArray(data.floorConfigs)) {
-        // 假设前半部分为左门,后半部分为右门(根据实际业务逻辑调整)
-        const midPoint = Math.ceil(data.floorConfigs.length / 2);
-        form.leftFloors = data.floorConfigs
-          .slice(0, midPoint)
-          .map((config: any) => ({
-            floor: config.floorNumber || config.name || 0,
-            goods:
-              config.products?.map((p: any) => p.productId).filter(
-                (id: number) => id != null
-              ) || []
-          }));
-        form.rightFloors = data.floorConfigs.slice(midPoint).map((config: any) => ({
-          floor: config.floorNumber || config.name || 0,
-          goods:
-            config.products?.map((p: any) => p.productId).filter(
-              (id: number) => id != null
-            ) || []
-        }));
+      // 解析右门层数据
+      if (data.rightFloors) {
+        try {
+          const parsed = typeof data.rightFloors === "string"
+            ? JSON.parse(data.rightFloors)
+            : data.rightFloors;
+          if (Array.isArray(parsed)) {
+            form.rightFloors = parsed.map((item: any) => ({
+              floor: typeof item.floor === "string" ? parseInt(item.floor, 10) : (item.floor || 0),
+              goods: Array.isArray(item.goods) ? item.goods : []
+            }));
+          }
+        } catch (e) {
+          console.error("解析右门层数据失败:", e);
+          form.rightFloors = [];
+        }
+      }
+
+      // 如果没有层数据但有 shelfNum,则初始化空层
+      if (form.leftFloors.length === 0 && form.shelfNum > 0) {
+        initFloorsByShelfNum();
+      }
+
+      // 设置默认 Tab
+      if (form.leftFloors.length > 0) {
+        activeFloorTab.value = `left-${form.leftFloors[0].floor}`;
+      }
+      if (form.rightFloors.length > 0) {
+        activeRightFloorTab.value = `right-${form.rightFloors[0].floor}`;
       }
     }
   } catch (error) {
@@ -187,111 +258,75 @@ async function loadTemplateData(id: number) {
 }
 
 /**
- * 添加左门楼层
- * Add left door floor
- */
-function addLeftFloor() {
-  const nextFloor =
-    form.leftFloors.length > 0
-      ? Math.max(...form.leftFloors.map(f => f.floor)) + 1
-      : 1;
-  form.leftFloors.push({
-    floor: nextFloor,
-    goods: []
-  });
-}
-
-/**
- * 删除左门楼层
- * Remove left door floor
- * @param index - 要删除的索引
+ * 删除某层中的商品
  */
-function removeLeftFloor(index: number) {
-  form.leftFloors.splice(index, 1);
-  // 重新编号
-  form.leftFloors.forEach((floor, idx) => {
-    floor.floor = idx + 1;
-  });
+function removeGoods(floorList: FloorConfig[], floorIndex: number, goodsIndex: number) {
+  floorList[floorIndex].goods?.splice(goodsIndex, 1);
 }
 
 /**
- * 添加右门楼层
- * Add right door floor
+ * 添加商品到某层(弹出输入框填写商品信息)
  */
-function addRightFloor() {
-  const nextFloor =
-    form.rightFloors.length > 0
-      ? Math.max(...form.rightFloors.map(f => f.floor)) + 1
-      : 1;
-  form.rightFloors.push({
-    floor: nextFloor,
-    goods: []
-  });
-}
-
-/**
- * 删除右门楼层
- * Remove right door floor
- * @param index - 要删除的索引
- */
-function removeRightFloor(index: number) {
-  form.rightFloors.splice(index, 1);
-  // 重新编号
-  form.rightFloors.forEach((floor, idx) => {
-    floor.floor = idx + 1;
+function addGoods(floorList: FloorConfig[], floorIndex: number) {
+  const floor = floorList[floorIndex];
+  if (!floor.goods) {
+    floor.goods = [];
+  }
+  floor.goods.push({
+    key: "",
+    label: "",
+    stock: 0,
+    c_price: "",
+    pic: "",
+    type: ""
   });
 }
 
 /**
- * 构造提交数据格式
- * Build submit data format matching backend DTO
+ * 构造提交数据格式(匹配哈哈平台 postLayerTypeInfo 接口格式)
  */
 function buildSubmitData() {
-  // 构造楼层配置数组
-  const floorConfigs = [
-    ...form.leftFloors.map(f => ({
-      floorNumber: f.floor,
-      name: `左门第${f.floor}层`,
-      products: f.goods.map(productId => ({
-        productId,
-        quantity: 1,
-        sortOrder: 0
-      }))
-    })),
-    ...form.rightFloors.map(f => ({
-      floorNumber: f.floor,
-      name: `右门第${f.floor}层`,
-      products: f.goods.map(productId => ({
-        productId,
-        quantity: 1,
-        sortOrder: 0
-      }))
-    }))
-  ];
+  // 构造哈哈平台所需的 products 结构
+  const products: Record<string, any> = {};
+
+  // 左门层数据
+  products.left = form.leftFloors.map(f => ({
+    floor: f.floor,
+    goods: (f.goods || []).map(g => g.key).filter(k => k && k.trim() !== "")
+  }));
+
+  // 右门层数据(双门柜才有)
+  if (isDoubleDoor.value && form.rightFloors.length > 0) {
+    products.right = form.rightFloors.map(f => ({
+      floor: f.floor,
+      goods: (f.goods || []).map(g => g.key).filter(k => k && k.trim() !== "")
+    }));
+  }
 
   return {
-    deviceId: parseInt(form.deviceId.toString(), 10),
+    deviceId: editDeviceId.value,
     templateName: form.templateName.trim(),
-    totalFloors: form.leftFloors.length + form.rightFloors.length,
-    floorConfigs,
-    status: 1 // 默认启用
+    deviceType: form.deviceType,
+    shelfNum: form.shelfNum,
+    leftFloors: form.leftFloors,
+    rightFloors: isDoubleDoor.value ? form.rightFloors : [],
+    // 保存完整的 products JSON(用于推送到哈哈平台)
+    productsJson: JSON.stringify(products)
   };
 }
 
 /**
  * 提交表单
- * Submit form
  */
 async function handleSubmit() {
   if (!formRef.value) return;
 
   try {
-    // 表单校验
     await formRef.value.validate();
 
     // 至少需要一层配置
-    if (form.leftFloors.length === 0 && form.rightFloors.length === 0) {
-      message("请至少添加一个楼层的配置", { type: "warning" });
+    if (form.leftFloors.length === 0) {
+      message("请至少配置一个楼层", { type: "warning" });
       return;
     }
 
@@ -299,26 +334,23 @@ async function handleSubmit() {
     let res;
 
     if (isEdit.value && editId.value) {
-      // 更新操作
       res = await updateLayerTemplate(editId.value, submitData);
     } else {
-      // 新增操作
       res = await createLayerTemplate(submitData);
     }
 
-    if (res.code === 0) {
+    if (res.code === 200 || res.code === 0) {
       message(
         `${isEdit.value ? "修改" : "新增"}层模版成功`,
         { type: "success" }
       );
       visible.value = false;
-      emit("success"); // 通知父组件刷新列表
+      emit("success");
     } else {
       message(res.message || "操作失败", { type: "error" });
     }
   } catch (error: any) {
     if (error !== false) {
-      // 非校验错误
       console.error("提交表单失败:", error);
       message("提交失败,请检查输入", { type: "error" });
     }
@@ -327,7 +359,6 @@ async function handleSubmit() {
 
 /**
  * 关闭对话框
- * Close dialog
  */
 function handleClose() {
   visible.value = false;
@@ -344,7 +375,7 @@ defineExpose({
   <el-dialog
     v-model="visible"
     :title="title"
-    width="800px"
+    width="900px"
     destroy-on-close
     :close-on-click-modal="false"
     @close="handleClose"
@@ -359,136 +390,282 @@ defineExpose({
       <!-- 基本信息 -->
       <el-divider content-position="left">基本信息</el-divider>
 
-      <el-form-item label="设备ID" prop="deviceId">
-        <el-input
-          v-model="form.deviceId"
-          placeholder="请输入设备ID"
-          :disabled="isEdit"
-          clearable
-        />
-      </el-form-item>
-
-      <el-form-item label="模板名称" prop="templateName">
-        <el-input
-          v-model="form.templateName"
-          placeholder="请输入模板名称"
-          clearable
-          maxlength="50"
-          show-word-limit
-        />
-      </el-form-item>
-
-      <!-- 左门配置 -->
-      <el-divider content-position="left">左门配置</el-divider>
-
-      <div class="floors-container">
-        <div
-          v-for="(floor, index) in form.leftFloors"
-          :key="'left-' + index"
-          class="floor-card"
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="模板名称" prop="templateName">
+            <el-input
+              v-model="form.templateName"
+              placeholder="请输入模板名称"
+              clearable
+              maxlength="50"
+              show-word-limit
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="设备层数" prop="shelfNum">
+            <el-input-number
+              v-model="form.shelfNum"
+              :min="1"
+              :max="20"
+              :step="1"
+              controls-position="right"
+              style="width: 100%"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <el-row :gutter="20">
+        <el-col :span="12">
+          <el-form-item label="设备类型">
+            <el-radio-group v-model="form.deviceType">
+              <el-radio :value="1">单门柜</el-radio>
+              <el-radio :value="2">双门柜</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="模板类型">
+            <el-radio-group v-model="form.templateType">
+              <el-radio :value="1">层模板</el-radio>
+              <el-radio :value="2">柜模板</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
+      </el-row>
+
+      <!-- 商品模板 - 左门 -->
+      <el-divider content-position="left">
+        商品模板 {{ isDoubleDoor ? "- 左门" : "" }}
+      </el-divider>
+
+      <el-tabs v-model="activeFloorTab" type="border-card">
+        <el-tab-pane
+          v-for="(floor, fIndex) in form.leftFloors"
+          :key="'left-' + floor.floor"
+          :label="'第' + floor.floor + '层'"
+          :name="'left-' + floor.floor"
         >
-          <el-card shadow="hover">
-            <template #header>
-              <div class="card-header">
-                <span>第 {{ floor.floor }} 层</span>
-                <el-button
-                  type="danger"
-                  size="small"
-                  text
-                  @click="removeLeftFloor(index)"
-                >
-                  删除此层
-                </el-button>
-              </div>
-            </template>
-            <el-select
-              v-model="floor.goods"
-              multiple
-              filterable
-              collapse-tags
-              collapse-tags-tooltip
-              placeholder="选择该层的商品"
-              :loading="productLoading"
-              class="w-full!"
+          <div class="floor-toolbar">
+            <el-button
+              type="primary"
+              size="small"
+              plain
+              @click="addGoods(form.leftFloors, fIndex)"
             >
-              <el-option
-                v-for="product in productList"
-                :key="product.id"
-                :label="`${product.name} (${product.barcode || '无条码'})`"
-                :value="product.id"
-              />
-            </el-select>
-          </el-card>
-        </div>
-
-        <el-button
-          type="primary"
-          plain
-          class="add-floor-btn"
-          @click="addLeftFloor"
-        >
-          + 添加左门层
-        </el-button>
-      </div>
-
-      <!-- 右门配置 -->
-      <el-divider content-position="left">右门配置</el-divider>
-
-      <div class="floors-container">
-        <div
-          v-for="(floor, index) in form.rightFloors"
-          :key="'right-' + index"
-          class="floor-card"
-        >
-          <el-card shadow="hover">
-            <template #header>
-              <div class="card-header">
-                <span>第 {{ floor.floor }} 层</span>
+              + 添加商品
+            </el-button>
+          </div>
+          <el-table
+            :data="floor.goods || []"
+            border
+            size="small"
+            style="width: 100%"
+            empty-text="暂无商品,请点击添加商品"
+          >
+            <el-table-column label="商品图片" width="80" align="center">
+              <template #default="{ row }">
+                <el-image
+                  v-if="row.pic"
+                  :src="row.pic"
+                  :preview-src-list="[row.pic]"
+                  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="商品名称" min-width="160">
+              <template #default="{ row }">
+                <el-input
+                  v-if="!row.label || row._editing"
+                  v-model="row.label"
+                  size="small"
+                  placeholder="请输入商品名称"
+                />
+                <span v-else>{{ row.label }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="商品编码" min-width="120">
+              <template #default="{ row }">
+                <el-input
+                  v-if="!row.key || row._editing"
+                  v-model="row.key"
+                  size="small"
+                  placeholder="商品code"
+                />
+                <span v-else class="text-gray-500">{{ row.key }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="商品售价" width="100" align="right">
+              <template #default="{ row }">
+                <span>{{ row.c_price || '-' }}</span>
+              </template>
+            </el-table-column>
+            <el-table-column label="优惠价" width="120">
+              <template #default="{ row }">
+                <el-input
+                  v-model="row.dis_price"
+                  size="small"
+                  placeholder="优惠价"
+                  clearable
+                  style="width: 90px"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column label="标准库存" width="100">
+              <template #default="{ row }">
+                <el-input-number
+                  v-model="row.stock"
+                  size="small"
+                  :min="0"
+                  :max="9999"
+                  controls-position="right"
+                  style="width: 80px"
+                />
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" width="80" align="center" fixed="right">
+              <template #default="{ $index }">
                 <el-button
                   type="danger"
                   size="small"
-                  text
-                  @click="removeRightFloor(index)"
+                  link
+                  @click="removeGoods(form.leftFloors, fIndex, $index)"
                 >
-                  删除此层
+                  删除
                 </el-button>
-              </div>
-            </template>
-            <el-select
-              v-model="floor.goods"
-              multiple
-              filterable
-              collapse-tags
-              collapse-tags-tooltip
-              placeholder="选择该层的商品"
-              :loading="productLoading"
-              class="w-full!"
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-tab-pane>
+      </el-tabs>
+
+      <!-- 商品模板 - 右门(仅双门柜显示) -->
+      <template v-if="isDoubleDoor">
+        <el-divider content-position="left">商品模板 - 右门</el-divider>
+
+        <el-tabs v-model="activeRightFloorTab" type="border-card">
+          <el-tab-pane
+            v-for="(floor, fIndex) in form.rightFloors"
+            :key="'right-' + floor.floor"
+            :label="'第' + floor.floor + '层'"
+            :name="'right-' + floor.floor"
+          >
+            <div class="floor-toolbar">
+              <el-button
+                type="primary"
+                size="small"
+                plain
+                @click="addGoods(form.rightFloors, fIndex)"
+              >
+                + 添加商品
+              </el-button>
+            </div>
+            <el-table
+              :data="floor.goods || []"
+              border
+              size="small"
+              style="width: 100%"
+              empty-text="暂无商品,请点击添加商品"
             >
-              <el-option
-                v-for="product in productList"
-                :key="product.id"
-                :label="`${product.name} (${product.barcode || '无条码'})`"
-                :value="product.id"
-              />
-            </el-select>
-          </el-card>
+              <el-table-column label="商品图片" width="80" align="center">
+                <template #default="{ row }">
+                  <el-image
+                    v-if="row.pic"
+                    :src="row.pic"
+                    :preview-src-list="[row.pic]"
+                    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="商品名称" min-width="160">
+                <template #default="{ row }">
+                  <el-input
+                    v-if="!row.label || row._editing"
+                    v-model="row.label"
+                    size="small"
+                    placeholder="请输入商品名称"
+                  />
+                  <span v-else>{{ row.label }}</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="商品编码" min-width="120">
+                <template #default="{ row }">
+                  <el-input
+                    v-if="!row.key || row._editing"
+                    v-model="row.key"
+                    size="small"
+                    placeholder="商品code"
+                  />
+                  <span v-else class="text-gray-500">{{ row.key }}</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="商品售价" width="100" align="right">
+                <template #default="{ row }">
+                  <span>{{ row.c_price || '-' }}</span>
+                </template>
+              </el-table-column>
+              <el-table-column label="优惠价" width="120">
+                <template #default="{ row }">
+                  <el-input
+                    v-model="row.dis_price"
+                    size="small"
+                    placeholder="优惠价"
+                    clearable
+                    style="width: 90px"
+                  />
+                </template>
+              </el-table-column>
+              <el-table-column label="标准库存" width="100">
+                <template #default="{ row }">
+                  <el-input-number
+                    v-model="row.stock"
+                    size="small"
+                    :min="0"
+                    :max="9999"
+                    controls-position="right"
+                    style="width: 80px"
+                  />
+                </template>
+              </el-table-column>
+              <el-table-column label="操作" width="80" align="center" fixed="right">
+                <template #default="{ $index }">
+                  <el-button
+                    type="danger"
+                    size="small"
+                    link
+                    @click="removeGoods(form.rightFloors, fIndex, $index)"
+                  >
+                    删除
+                  </el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </el-tab-pane>
+        </el-tabs>
+      </template>
+
+      <!-- 适用设备信息(仅编辑模式显示) -->
+      <template v-if="isEdit && editDeviceId">
+        <el-divider content-position="left">适用设备</el-divider>
+        <div class="device-info">
+          <el-tag type="info" size="large">
+            设备ID: {{ editDeviceId }}
+          </el-tag>
         </div>
-
-        <el-button
-          type="primary"
-          plain
-          class="add-floor-btn"
-          @click="addRightFloor"
-        >
-          + 添加右门层
-        </el-button>
-      </div>
+      </template>
     </el-form>
 
     <template #footer>
       <el-button @click="handleClose">取消</el-button>
-      <el-button type="primary" :loading="false" @click="handleSubmit">
-        确定
+      <el-button type="primary" @click="handleSubmit">
+        保存
       </el-button>
     </template>
   </el-dialog>
@@ -496,33 +673,21 @@ defineExpose({
 
 <style lang="scss" scoped>
 .template-form {
-  max-height: 65vh;
+  max-height: 70vh;
   overflow-y: auto;
   padding-right: 10px;
 
-  .floors-container {
+  .floor-toolbar {
+    margin-bottom: 12px;
     display: flex;
-    flex-direction: column;
-    gap: 12px;
-    margin-bottom: 16px;
-
-    .floor-card {
-      .card-header {
-        display: flex;
-        justify-content: space-between;
-        align-items: center;
-        font-weight: 500;
-      }
-
-      :deep(.el-card__body) {
-        padding-top: 16px;
-      }
-    }
+    justify-content: flex-end;
+  }
 
-    .add-floor-btn {
-      width: 100%;
-      border-style: dashed;
-    }
+  .device-info {
+    display: flex;
+    gap: 8px;
+    align-items: center;
+    flex-wrap: wrap;
   }
 
   :deep(.el-divider__text) {
@@ -530,5 +695,17 @@ defineExpose({
     font-weight: 600;
     color: var(--el-color-primary);
   }
+
+  :deep(.el-tabs__item) {
+    font-size: 13px;
+  }
+
+  :deep(.el-table) {
+    font-size: 13px;
+  }
+
+  :deep(.el-image) {
+    cursor: pointer;
+  }
 }
 </style>

+ 1 - 1
haha-admin-web/src/views/layer-template/index.vue

@@ -128,7 +128,7 @@ function handleSingleSync() {
 
     <!-- 表格区域 -->
     <PureTableBar
-      title="层模版管理"
+      title="设备设备层模版管理"
       :columns="columns"
       @refresh="onSearch"
     >

+ 6 - 6
haha-admin/src/main/java/com/haha/admin/controller/LayerTemplateController.java

@@ -19,7 +19,7 @@ import java.util.List;
 import java.util.Map;
 
 /**
- * 层模版管理控制器
+ * 设备层模版管理控制器
  */
 @Slf4j
 @RestController
@@ -95,7 +95,7 @@ public class LayerTemplateController {
      * @return 创建结果
      */
     @RequirePermission("layer-template:create")
-    @Log(module = "层模版管理", operation = OperationType.INSERT, summary = "创建层模版")
+    @Log(module = "设备层模版管理", operation = OperationType.INSERT, summary = "创建层模版")
     @PostMapping
     public Result<LayerTemplate> create(@RequestBody LayerTemplateCreateDTO dto) {
         LayerTemplate saved = layerTemplateService.create(dto);
@@ -109,7 +109,7 @@ public class LayerTemplateController {
      * @return 更新结果
      */
     @RequirePermission("layer-template:update")
-    @Log(module = "层模版管理", operation = OperationType.UPDATE, summary = "更新层模版")
+    @Log(module = "设备层模版管理", operation = OperationType.UPDATE, summary = "更新层模版")
     @PutMapping("/{id}")
     public Result<LayerTemplate> update(@PathVariable Long id, @RequestBody LayerTemplateUpdateDTO dto) {
         LayerTemplate saved = layerTemplateService.update(id, dto);
@@ -122,7 +122,7 @@ public class LayerTemplateController {
      * @return 删除结果
      */
     @RequirePermission("layer-template:delete")
-    @Log(module = "层模版管理", operation = OperationType.DELETE, summary = "删除层模版")
+    @Log(module = "设备层模版管理", operation = OperationType.DELETE, summary = "删除层模版")
     @DeleteMapping("/{id}")
     public Result<Void> delete(@PathVariable Long id) {
         boolean success = layerTemplateService.softDelete(id);
@@ -136,7 +136,7 @@ public class LayerTemplateController {
      */
     // TODO: 临时移除权限验证,修复后恢复
     // @RequirePermission("layer-template:update")
-    @Log(module = "层模版管理", operation = OperationType.SYNC, summary = "同步层模版")
+    @Log(module = "设备层模版管理", operation = OperationType.SYNC, summary = "同步层模版")
     @PostMapping("/{deviceId}/sync")
     public Result<Void> sync(@PathVariable String deviceId) {
         boolean success = layerTemplateService.syncFromHaha(deviceId);
@@ -150,7 +150,7 @@ public class LayerTemplateController {
      */
     // TODO: 临时移除权限验证,修复后恢复
     // @RequirePermission("layer-template:update")
-    @Log(module = "层模版管理", operation = OperationType.SYNC, summary = "批量同步层模版")
+    @Log(module = "设备层模版管理", operation = OperationType.SYNC, summary = "批量同步层模版")
     @PostMapping("/sync/batch")
     public Result<Map<String, Object>> batchSync(@RequestBody List<String> deviceIds) {
         Map<String, Object> syncResult = layerTemplateService.batchSync(deviceIds);

+ 10 - 5
haha-common/src/main/java/com/haha/common/dto/FloorConfig.java

@@ -8,23 +8,28 @@ import java.util.List;
 
 /**
  * 层数据配置DTO
- * 用于描述每一层的商品摆放信息
+ * 对应哈哈平台层模板中每一层的数据结构
+ *
+ * 哈哈平台API返回格式:
+ * {
+ *   "floor": "1",       // 层号
+ *   "goods": [...]      // 商品列表
+ * }
  */
 @Data
 public class FloorConfig {
 
     /**
      * 层号(从1开始)
+     * 注意:哈哈平台返回的是字符串类型
      */
     @NotNull(message = "层号不能为空")
     private Integer floor;
 
     /**
      * 该层的商品列表
-     * 支持两种格式:
-     * 1. 字符串数组:["goods1", "goods2"]
-     * 2. 对象数组:[{"productId": 1, "productName": "商品1"}]
+     * 每个商品包含 key(编码)、label(名称)、stock(库存)、pic(图片)、c_price(售价) 等字段
      */
     @NotEmpty(message = "商品列表不能为空")
-    private List<Object> goods;
+    private List<GoodsItem> goods;
 }

+ 66 - 0
haha-common/src/main/java/com/haha/common/dto/GoodsItem.java

@@ -0,0 +1,66 @@
+package com.haha.common.dto;
+
+import lombok.Data;
+
+/**
+ * 商品项DTO
+ * 对应哈哈平台层模板中每个商品的数据结构
+ *
+ * 哈哈平台API返回格式:
+ * {
+ *   "key": "zsnrj",         // 商品code
+ *   "label": "芝士牛肉卷",   // 商品名称
+ *   "stock": 0,             // 库存
+ *   "type": "食品",          // 商品分类
+ *   "pic": "https://...",   // 商品图片
+ *   "c_price": "0.02"       // 商品价格
+ * }
+ */
+@Data
+public class GoodsItem {
+
+    /**
+     * 商品code(哈哈平台的商品编码)
+     */
+    private String key;
+
+    /**
+     * 商品名称
+     */
+    private String label;
+
+    /**
+     * 库存/标准库存
+     */
+    private Integer stock;
+
+    /**
+     * 商品分类
+     */
+    private String type;
+
+    /**
+     * 商品图片URL
+     */
+    private String pic;
+
+    /**
+     * 商品售价
+     */
+    private String c_price;
+
+    /**
+     * 是否折扣
+     */
+    private String discount;
+
+    /**
+     * 活动价/优惠价
+     */
+    private String dis_price;
+
+    /**
+     * 算法识别图
+     */
+    private String rec_pic;
+}

+ 1 - 1
haha-entity/src/main/java/com/haha/entity/LayerTemplate.java

@@ -10,7 +10,7 @@ import java.time.LocalDateTime;
 
 /**
  * 设备层模版实体类
- * 对应哈哈平台设备层模版管理
+ * 对应哈哈平台设备设备层模版管理
  */
 @Data
 @TableName("t_layer_template")

+ 77 - 4
haha-miniapp/src/main/java/com/haha/miniapp/controller/DeviceController.java

@@ -1,17 +1,21 @@
 package com.haha.miniapp.controller;
 
 import cn.dev33.satoken.stp.StpUtil;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.haha.common.dto.FloorConfig;
 import com.haha.common.vo.OpenDoorVO;
 import com.haha.common.vo.Result;
+import com.haha.entity.LayerTemplate;
 import com.haha.service.DeviceService;
+import com.haha.service.LayerTemplateService;
 import com.haha.sdk.exception.HahaException;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
+import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -26,6 +30,11 @@ public class DeviceController {
     @Autowired
     private DeviceService deviceService;
 
+    @Autowired
+    private LayerTemplateService layerTemplateService;
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
     /**
      * 处理小程序扫码开门
      *
@@ -70,6 +79,70 @@ public class DeviceController {
         }
     }
 
+    /**
+     * 获取设备商品陈列信息
+     * 根据设备ID查询层模板,返回每层商品数据供小程序展示
+     *
+     * @param deviceId 设备ID(SN号)
+     * @return 商品陈列数据
+     */
+    @GetMapping("/products/{deviceId}")
+    public Result<Map<String, Object>> getDeviceProducts(@PathVariable String deviceId) {
+        try {
+            // 1. 登录校验
+            StpUtil.getLoginIdAsLong();
+
+            // 2. 查询设备层模板
+            LayerTemplate template = layerTemplateService.getByDeviceId(deviceId);
+            if (template == null) {
+                return Result.error(404, "该设备暂无商品信息");
+            }
+
+            // 3. 解析层数据
+            Map<String, Object> result = new HashMap<>();
+            result.put("templateName", template.getTemplateName());
+            result.put("deviceId", template.getDeviceId());
+            result.put("shelfNum", template.getShelfNum());
+            result.put("deviceType", template.getDeviceType());
+
+            // 解析左门层数据
+            if (template.getLeftFloors() != null && !template.getLeftFloors().isEmpty()) {
+                try {
+                    List<FloorConfig> leftFloors = objectMapper.readValue(
+                        template.getLeftFloors(), new TypeReference<List<FloorConfig>>() {});
+                    result.put("leftFloors", leftFloors);
+                } catch (Exception e) {
+                    log.warn("解析左门层数据失败, deviceId={}", deviceId, e);
+                    result.put("leftFloors", List.of());
+                }
+            } else {
+                result.put("leftFloors", List.of());
+            }
+
+            // 解析右门层数据
+            if (template.getRightFloors() != null && !template.getRightFloors().isEmpty()) {
+                try {
+                    List<FloorConfig> rightFloors = objectMapper.readValue(
+                        template.getRightFloors(), new TypeReference<List<FloorConfig>>() {});
+                    result.put("rightFloors", rightFloors);
+                } catch (Exception e) {
+                    log.warn("解析右门层数据失败, deviceId={}", deviceId, e);
+                    result.put("rightFloors", List.of());
+                }
+            } else {
+                result.put("rightFloors", List.of());
+            }
+
+            return Result.success("查询成功", result);
+
+        } catch (cn.dev33.satoken.exception.NotLoginException e) {
+            return Result.error(401, "登录已失效,请重新登录");
+        } catch (Exception e) {
+            log.error("查询设备商品信息异常, deviceId={}", deviceId, e);
+            return Result.error(500, "查询失败,请稍后重试");
+        }
+    }
+
     /**
      * 根据错误码返回友好的错误信息
      */

+ 59 - 1
haha-mp/src/api/device.ts

@@ -2,7 +2,7 @@
  * 设备相关API
  */
 
-import { post } from '../utils/request';
+import { post, get } from '../utils/request';
 
 /**
  * 扫码开门请求参数
@@ -51,3 +51,61 @@ export const checkDeviceStatus = (deviceId: string): Promise<DeviceStatusRespons
     deviceId
   });
 };
+
+/**
+ * 商品项数据
+ */
+export interface GoodsItem {
+  /** 商品code */
+  key?: string;
+  /** 商品名称 */
+  label?: string;
+  /** 库存 */
+  stock?: number;
+  /** 商品分类 */
+  type?: string;
+  /** 商品图片URL */
+  pic?: string;
+  /** 商品售价 */
+  c_price?: string;
+  /** 是否折扣 */
+  discount?: string;
+  /** 优惠价 */
+  dis_price?: string;
+}
+
+/**
+ * 层配置数据
+ */
+export interface FloorConfig {
+  /** 层号 */
+  floor: number;
+  /** 该层商品列表 */
+  goods: GoodsItem[];
+}
+
+/**
+ * 设备商品陈列响应数据
+ */
+export interface DeviceProductsResponse {
+  /** 模板名称 */
+  templateName?: string;
+  /** 设备ID */
+  deviceId?: string;
+  /** 设备层数 */
+  shelfNum?: number;
+  /** 设备类型 */
+  deviceType?: number;
+  /** 左门层数据 */
+  leftFloors: FloorConfig[];
+  /** 右门层数据 */
+  rightFloors: FloorConfig[];
+}
+
+/**
+ * 获取设备商品陈列信息
+ * @param deviceId 设备ID
+ */
+export const getDeviceProducts = (deviceId: string): Promise<DeviceProductsResponse> => {
+  return get<DeviceProductsResponse>(`/device/products/${deviceId}`);
+};

+ 8 - 0
haha-mp/src/pages.json

@@ -112,6 +112,14 @@
 				"navigationBarBackgroundColor": "#FFD700",
 				"navigationBarTextStyle": "black"
 			}
+		},
+		{
+			"path": "pages/products/products",
+			"style": {
+				"navigationBarTitleText": "柜内商品",
+				"navigationBarBackgroundColor": "#FFD700",
+				"navigationBarTextStyle": "black"
+			}
 		}
 	],
 	"globalStyle": {

+ 52 - 39
haha-mp/src/pages/index/index.vue

@@ -159,46 +159,24 @@ const scanCode = async () => {
         return;
       }
 
-      // 显示加载提示
-      uni.showLoading({
-        title: '正在开门...',
-        mask: true
+      // 弹出选择弹窗:查看柜内商品 or 直接开门
+      uni.showActionSheet({
+        itemList: ['查看柜内商品', '直接开门'],
+        success: async (actionRes) => {
+          if (actionRes.tapIndex === 0) {
+            // 查看柜内商品 -> 跳转到商品陈列页
+            uni.navigateTo({
+              url: '/pages/products/products?deviceId=' + deviceId
+            });
+          } else if (actionRes.tapIndex === 1) {
+            // 直接开门 -> 执行现有开门逻辑
+            await doOpenDoor(deviceId);
+          }
+        },
+        fail: () => {
+          console.log('用户取消选择');
+        }
       });
-
-      try {
-        // 调用真实接口扫码开门
-        const response = await scanDoor(deviceId);
-
-        // 隐藏加载提示
-        uni.hideLoading();
-
-        // 开门成功
-        uni.showToast({
-          title: '开门成功',
-          icon: 'success'
-        });
-
-        // 将设备信息存储到本地,供购物页面使用
-        uni.setStorageSync('currentDeviceId', response.deviceId);
-        uni.setStorageSync('currentOutTradeNo', response.outTradeNo);
-        uni.setStorageSync('currentOrderNo', response.orderNo);
-
-        // 清理可能存在的旧轮询状态标记
-        uni.removeStorageSync('shoppingPollingActive');
-
-        // 跳转到购物进行中页面
-        setTimeout(() => {
-          uni.navigateTo({
-            url: '/pages/shopping/shopping'
-          });
-        }, 1000);
-      } catch (error: any) {
-        // 隐藏加载提示
-        uni.hideLoading();
-
-        console.error('开门失败:', error);
-        // 错误信息已经在request工具中显示,这里不需要重复显示
-      }
     },
     fail: function (err) {
       console.log('扫码取消:', err);
@@ -210,6 +188,41 @@ const scanCode = async () => {
   });
 };
 
+/**
+ * 执行开门操作
+ */
+const doOpenDoor = async (deviceId: string) => {
+  uni.showLoading({
+    title: '正在开门...',
+    mask: true
+  });
+
+  try {
+    const response = await scanDoor(deviceId);
+
+    uni.hideLoading();
+    uni.showToast({
+      title: '开门成功',
+      icon: 'success'
+    });
+
+    // 将设备信息存储到本地,供购物页面使用
+    uni.setStorageSync('currentDeviceId', response.deviceId);
+    uni.setStorageSync('currentOutTradeNo', response.outTradeNo);
+    uni.setStorageSync('currentOrderNo', response.orderNo);
+    uni.removeStorageSync('shoppingPollingActive');
+
+    setTimeout(() => {
+      uni.navigateTo({
+        url: '/pages/shopping/shopping'
+      });
+    }, 1000);
+  } catch (error: any) {
+    uni.hideLoading();
+    console.error('开门失败:', error);
+  }
+};
+
 const goToMy = () => {
   uni.vibrateShort({ type: 'light' })
   uni.navigateTo({

+ 560 - 0
haha-mp/src/pages/products/products.vue

@@ -0,0 +1,560 @@
+<template>
+  <view class="products-page">
+    <!-- 加载状态 -->
+    <view v-if="loading" class="loading-container">
+      <view class="loading-spinner"></view>
+      <text class="loading-text">加载中...</text>
+    </view>
+
+    <!-- 错误状态 -->
+    <view v-else-if="errorMsg" class="error-container">
+      <text class="error-icon">!</text>
+      <text class="error-text">{{ errorMsg }}</text>
+      <view class="error-btn" @click="goBack">返回</view>
+    </view>
+
+    <!-- 商品内容 -->
+    <view v-else class="content">
+      <!-- 顶部设备信息 -->
+      <view class="device-header">
+        <view class="device-name">{{ templateName || '智能零售柜' }}</view>
+        <view class="device-info">
+          <text class="info-tag">{{ deviceTypeText }}</text>
+          <text class="info-tag">共{{ shelfNum }}层</text>
+        </view>
+      </view>
+
+      <!-- 双门柜Tab切换 -->
+      <view v-if="hasRightDoor" class="door-tabs">
+        <view
+          class="door-tab"
+          :class="{ active: activeDoor === 'left' }"
+          @click="activeDoor = 'left'"
+        >左门</view>
+        <view
+          class="door-tab"
+          :class="{ active: activeDoor === 'right' }"
+          @click="activeDoor = 'right'"
+        >右门</view>
+      </view>
+
+      <!-- 商品列表 -->
+      <view class="floors-list">
+        <view
+          v-for="floor in currentFloors"
+          :key="floor.floor"
+          class="floor-card"
+        >
+          <view class="floor-header">
+            <view class="floor-badge">{{ floor.floor }}</view>
+            <text class="floor-title">第{{ floor.floor }}层</text>
+            <text class="floor-count">{{ floor.goods?.length || 0 }}件商品</text>
+          </view>
+
+          <!-- 该层无商品 -->
+          <view v-if="!floor.goods || floor.goods.length === 0" class="empty-floor">
+            <text class="empty-text">该层暂无商品</text>
+          </view>
+
+          <!-- 商品列表 -->
+          <view v-else class="goods-list">
+            <view
+              v-for="(item, index) in floor.goods"
+              :key="index"
+              class="goods-item"
+            >
+              <!-- 商品图片 -->
+              <view class="goods-image-wrap">
+                <image
+                  v-if="item.pic"
+                  :src="item.pic"
+                  class="goods-image"
+                  mode="aspectFill"
+                />
+                <view v-else class="goods-image-placeholder">
+                  <text class="placeholder-text">暂无图片</text>
+                </view>
+              </view>
+
+              <!-- 商品信息 -->
+              <view class="goods-info">
+                <text class="goods-name">{{ item.label || '未知商品' }}</text>
+                <text v-if="item.type" class="goods-type">{{ item.type }}</text>
+                <view class="goods-price-row">
+                  <text class="goods-price">¥{{ item.c_price || '0.00' }}</text>
+                  <text
+                    v-if="item.dis_price && item.dis_price !== item.c_price"
+                    class="goods-discount-price"
+                  >¥{{ item.dis_price }}</text>
+                </view>
+                <view v-if="item.stock !== undefined && item.stock !== null" class="goods-stock">
+                  <text class="stock-label">库存: </text>
+                  <text :class="item.stock > 0 ? 'stock-normal' : 'stock-empty'">{{ item.stock }}</text>
+                </view>
+              </view>
+            </view>
+          </view>
+        </view>
+
+        <!-- 无任何层数据 -->
+        <view v-if="currentFloors.length === 0" class="empty-container">
+          <text class="empty-icon">📦</text>
+          <text class="empty-main-text">暂无商品信息</text>
+          <text class="empty-sub-text">该设备尚未配置商品</text>
+        </view>
+      </view>
+
+      <!-- 底部占位,避免被固定栏遮挡 -->
+      <view class="bottom-placeholder"></view>
+    </view>
+
+    <!-- 底部操作栏 -->
+    <view v-if="!loading && !errorMsg" class="bottom-bar">
+      <view class="btn-exit" @click="goBack">退出</view>
+      <view class="btn-open" @click="handleOpenDoor">开门购物</view>
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue';
+import { onLoad } from '@dcloudio/uni-app';
+import { getDeviceProducts, scanDoor } from '@/api/device';
+import type { FloorConfig } from '@/api/device';
+
+// 页面参数
+const deviceId = ref('');
+
+// 数据状态
+const loading = ref(true);
+const errorMsg = ref('');
+const templateName = ref('');
+const shelfNum = ref(0);
+const deviceType = ref(0);
+const leftFloors = ref<FloorConfig[]>([]);
+const rightFloors = ref<FloorConfig[]>([]);
+const opening = ref(false);
+
+// 双门柜切换
+const activeDoor = ref<'left' | 'right'>('left');
+
+// 是否双门柜
+const hasRightDoor = computed(() => rightFloors.value.length > 0);
+
+// 设备类型文字
+const deviceTypeText = computed(() => {
+  return hasRightDoor.value ? '双门柜' : '单门柜';
+});
+
+// 当前显示的层数据
+const currentFloors = computed(() => {
+  if (activeDoor.value === 'right' && hasRightDoor.value) {
+    return rightFloors.value;
+  }
+  return leftFloors.value;
+});
+
+// 页面加载
+onLoad((options: any) => {
+  if (options?.deviceId) {
+    deviceId.value = options.deviceId;
+    loadProducts();
+  } else {
+    errorMsg.value = '缺少设备ID参数';
+    loading.value = false;
+  }
+});
+
+// 加载商品数据
+const loadProducts = async () => {
+  loading.value = true;
+  errorMsg.value = '';
+
+  try {
+    const data = await getDeviceProducts(deviceId.value);
+    templateName.value = data.templateName || '';
+    shelfNum.value = data.shelfNum || 0;
+    deviceType.value = data.deviceType || 0;
+    leftFloors.value = data.leftFloors || [];
+    rightFloors.value = data.rightFloors || [];
+  } catch (error: any) {
+    console.error('加载商品数据失败:', error);
+    errorMsg.value = error.message || '加载失败,请稍后重试';
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 开门购物
+const handleOpenDoor = async () => {
+  if (opening.value) return;
+  opening.value = true;
+
+  uni.showLoading({
+    title: '正在开门...',
+    mask: true
+  });
+
+  try {
+    const response = await scanDoor(deviceId.value);
+
+    uni.hideLoading();
+    uni.showToast({
+      title: '开门成功',
+      icon: 'success'
+    });
+
+    // 存储设备信息
+    uni.setStorageSync('currentDeviceId', response.deviceId);
+    uni.setStorageSync('currentOutTradeNo', response.outTradeNo);
+    uni.setStorageSync('currentOrderNo', response.orderNo);
+    uni.removeStorageSync('shoppingPollingActive');
+
+    // 跳转到购物页面
+    setTimeout(() => {
+      uni.redirectTo({
+        url: '/pages/shopping/shopping'
+      });
+    }, 1000);
+  } catch (error: any) {
+    uni.hideLoading();
+    opening.value = false;
+    console.error('开门失败:', error);
+  }
+};
+
+// 返回
+const goBack = () => {
+  uni.navigateBack({
+    fail: () => {
+      uni.reLaunch({ url: '/pages/index/index' });
+    }
+  });
+};
+</script>
+
+<style lang="scss" scoped>
+.products-page {
+  min-height: 100vh;
+  background: #f5f5f5;
+}
+
+/* 加载状态 */
+.loading-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 60vh;
+}
+.loading-spinner {
+  width: 60rpx;
+  height: 60rpx;
+  border: 4rpx solid #e0e0e0;
+  border-top-color: #FFD700;
+  border-radius: 50%;
+  animation: spin 0.8s linear infinite;
+}
+@keyframes spin {
+  to { transform: rotate(360deg); }
+}
+.loading-text {
+  margin-top: 20rpx;
+  font-size: 28rpx;
+  color: #999;
+}
+
+/* 错误状态 */
+.error-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 60vh;
+  padding: 0 60rpx;
+}
+.error-icon {
+  width: 80rpx;
+  height: 80rpx;
+  line-height: 80rpx;
+  text-align: center;
+  background: #ff4d4f;
+  color: #fff;
+  border-radius: 50%;
+  font-size: 40rpx;
+  font-weight: bold;
+}
+.error-text {
+  margin-top: 24rpx;
+  font-size: 28rpx;
+  color: #666;
+  text-align: center;
+}
+.error-btn {
+  margin-top: 40rpx;
+  padding: 16rpx 60rpx;
+  background: #FFD700;
+  color: #333;
+  border-radius: 40rpx;
+  font-size: 28rpx;
+  font-weight: bold;
+}
+
+/* 设备信息头部 */
+.device-header {
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+  padding: 30rpx;
+  color: #333;
+}
+.device-name {
+  font-size: 36rpx;
+  font-weight: bold;
+}
+.device-info {
+  display: flex;
+  gap: 16rpx;
+  margin-top: 12rpx;
+}
+.info-tag {
+  font-size: 24rpx;
+  background: rgba(255,255,255,0.5);
+  padding: 4rpx 16rpx;
+  border-radius: 20rpx;
+  color: #555;
+}
+
+/* 门Tab切换 */
+.door-tabs {
+  display: flex;
+  background: #fff;
+  border-bottom: 1rpx solid #eee;
+}
+.door-tab {
+  flex: 1;
+  text-align: center;
+  padding: 24rpx 0;
+  font-size: 28rpx;
+  color: #666;
+  position: relative;
+}
+.door-tab.active {
+  color: #333;
+  font-weight: bold;
+}
+.door-tab.active::after {
+  content: '';
+  position: absolute;
+  bottom: 0;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 60rpx;
+  height: 4rpx;
+  background: #FFD700;
+  border-radius: 2rpx;
+}
+
+/* 楼层列表 */
+.floors-list {
+  padding: 20rpx;
+}
+.floor-card {
+  background: #fff;
+  border-radius: 16rpx;
+  margin-bottom: 20rpx;
+  overflow: hidden;
+  box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.05);
+}
+.floor-header {
+  display: flex;
+  align-items: center;
+  padding: 20rpx 24rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.floor-badge {
+  width: 44rpx;
+  height: 44rpx;
+  line-height: 44rpx;
+  text-align: center;
+  background: #FFD700;
+  color: #333;
+  border-radius: 10rpx;
+  font-size: 24rpx;
+  font-weight: bold;
+  margin-right: 16rpx;
+}
+.floor-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  flex: 1;
+}
+.floor-count {
+  font-size: 24rpx;
+  color: #999;
+}
+
+/* 空楼层 */
+.empty-floor {
+  padding: 40rpx 0;
+  text-align: center;
+}
+.empty-text {
+  font-size: 26rpx;
+  color: #ccc;
+}
+
+/* 商品列表 */
+.goods-list {
+  padding: 0 24rpx;
+}
+.goods-item {
+  display: flex;
+  padding: 20rpx 0;
+  border-bottom: 1rpx solid #f5f5f5;
+}
+.goods-item:last-child {
+  border-bottom: none;
+}
+
+/* 商品图片 */
+.goods-image-wrap {
+  width: 140rpx;
+  height: 140rpx;
+  border-radius: 12rpx;
+  overflow: hidden;
+  flex-shrink: 0;
+  background: #f9f9f9;
+}
+.goods-image {
+  width: 100%;
+  height: 100%;
+}
+.goods-image-placeholder {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background: #f0f0f0;
+}
+.placeholder-text {
+  font-size: 20rpx;
+  color: #ccc;
+}
+
+/* 商品信息 */
+.goods-info {
+  flex: 1;
+  margin-left: 20rpx;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+}
+.goods-name {
+  font-size: 28rpx;
+  color: #333;
+  font-weight: 500;
+  line-height: 1.4;
+  display: -webkit-box;
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: 2;
+  overflow: hidden;
+}
+.goods-type {
+  font-size: 22rpx;
+  color: #999;
+  margin-top: 6rpx;
+}
+.goods-price-row {
+  display: flex;
+  align-items: baseline;
+  gap: 12rpx;
+  margin-top: 8rpx;
+}
+.goods-price {
+  font-size: 32rpx;
+  color: #ff4d4f;
+  font-weight: bold;
+}
+.goods-discount-price {
+  font-size: 24rpx;
+  color: #999;
+  text-decoration: line-through;
+}
+.goods-stock {
+  margin-top: 6rpx;
+  font-size: 22rpx;
+}
+.stock-label {
+  color: #999;
+}
+.stock-normal {
+  color: #52c41a;
+}
+.stock-empty {
+  color: #ff4d4f;
+}
+
+/* 无数据 */
+.empty-container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 80rpx 0;
+}
+.empty-icon {
+  font-size: 80rpx;
+}
+.empty-main-text {
+  margin-top: 20rpx;
+  font-size: 30rpx;
+  color: #333;
+  font-weight: bold;
+}
+.empty-sub-text {
+  margin-top: 8rpx;
+  font-size: 26rpx;
+  color: #999;
+}
+
+/* 底部占位 */
+.bottom-placeholder {
+  height: 140rpx;
+}
+
+/* 底部操作栏 */
+.bottom-bar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  align-items: center;
+  gap: 20rpx;
+  padding: 20rpx 30rpx;
+  padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
+  background: #fff;
+  box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.08);
+}
+.btn-exit {
+  flex: 1;
+  text-align: center;
+  padding: 22rpx 0;
+  border-radius: 44rpx;
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #666;
+  background: #f0f0f0;
+}
+.btn-open {
+  flex: 2;
+  text-align: center;
+  padding: 22rpx 0;
+  border-radius: 44rpx;
+  font-size: 30rpx;
+  font-weight: bold;
+  color: #333;
+  background: linear-gradient(135deg, #FFD700, #FFC107);
+  box-shadow: 0 4rpx 12rpx rgba(255, 215, 0, 0.4);
+}
+</style>

+ 1 - 1
haha-service/src/main/java/com/haha/service/LayerTemplateService.java

@@ -10,7 +10,7 @@ import java.util.List;
 import java.util.Map;
 
 /**
- * 设备层模版管理服务接口
+ * 设备设备层模版管理服务接口
  * 提供设备层模版的CRUD操作和同步功能
  *
  * @author haha-service

+ 1 - 1
haha-service/src/main/java/com/haha/service/impl/LayerTemplateServiceImpl.java

@@ -31,7 +31,7 @@ import java.util.List;
 import java.util.Map;
 
 /**
- * 设备层模版管理服务实现类
+ * 设备设备层模版管理服务实现类
  * 提供设备层模版的完整业务逻辑实现,包括CRUD和同步功能
  *
  * @author haha-service