Ver Fonte

feat: ERP商品关联分页+同步日志优化+关联弹窗图片预览

- 列表改为分页模式,每页 10/20/50 条可选
- 同步完成后整齐打印未找到商品清单
- 关联弹窗加宽至 680px,ERP商品信息改为图片卡片
- 本地商品下拉框每行展示缩略图+名称+编码
- 下拉面板最大宽度限制 600px

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline há 5 dias atrás
pai
commit
ca894cfbee

+ 3 - 3
haha-admin-web/src/api/erpGoods.ts

@@ -10,9 +10,9 @@ type Result = {
 export const syncErpGoods = () =>
   http.request<Result>("post", "/erp-goods/sync");
 
-/** 获取已缓存的 ERP 商品列表 */
-export const getErpGoodsList = (keyword?: string) =>
-  http.request<Result>("get", "/erp-goods", { params: { keyword } });
+/** 分页获取已缓存的 ERP 商品列表 */
+export const getErpGoodsList = (params: { keyword?: string; page: number; pageSize: number }) =>
+  http.request<Result>("get", "/erp-goods", { params });
 
 /** 关联本地商品与 ERP 商品 */
 export const bindErpGoods = (productId: number, erpGoodsId: number) =>

+ 95 - 19
haha-admin-web/src/views/erp-goods/index.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { ref, onMounted } from "vue";
+import { ref, reactive, onMounted } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
 import Refresh from "~icons/ep/refresh";
@@ -22,6 +22,8 @@ const loading = ref(false);
 const syncing = ref(false);
 const keyword = ref("");
 const dataList = ref<any[]>([]);
+const total = ref(0);
+const pagination = reactive({ currentPage: 1, pageSize: 10 });
 const productOptions = ref<any[]>([]);
 const bindDialogVisible = ref(false);
 const currentErpGoods = ref<any>(null);
@@ -31,10 +33,14 @@ const bindLoading = ref(false);
 async function fetchList() {
   loading.value = true;
   try {
-    const res = await getErpGoodsList(keyword.value || undefined);
+    const res = await getErpGoodsList({
+      keyword: keyword.value || undefined,
+      page: pagination.currentPage,
+      pageSize: pagination.pageSize
+    });
     if (res.code === 200) {
-      dataList.value = res.data || [];
-      // 检查各 ERP 商品的关联状态
+      dataList.value = res.data?.list || [];
+      total.value = res.data?.total || 0;
       await fetchBoundStatus();
     }
   } finally {
@@ -42,6 +48,22 @@ async function fetchList() {
   }
 }
 
+function handlePageChange(page: number) {
+  pagination.currentPage = page;
+  fetchList();
+}
+
+function handleSizeChange(size: number) {
+  pagination.currentPage = 1;
+  pagination.pageSize = size;
+  fetchList();
+}
+
+function handleSearch() {
+  pagination.currentPage = 1;
+  fetchList();
+}
+
 async function fetchBoundStatus() {
   try {
     const res = await getProductList({ page: 1, pageSize: 1000 });
@@ -146,13 +168,13 @@ onMounted(() => {
           placeholder="搜索商品名称或编码"
           clearable
           class="w-[240px]!"
-          @keyup.enter="fetchList"
+          @keyup.enter="handleSearch"
         >
           <template #prefix>
             <el-icon><component :is="useRenderIcon(Search)" /></el-icon>
           </template>
         </el-input>
-        <el-button @click="fetchList">搜索</el-button>
+        <el-button @click="handleSearch">搜索</el-button>
         <el-button
           type="primary"
           :icon="useRenderIcon(Refresh)"
@@ -226,40 +248,88 @@ onMounted(() => {
           </template>
         </el-table-column>
       </el-table>
+
+      <div class="flex justify-end mt-4">
+        <el-pagination
+          v-model:current-page="pagination.currentPage"
+          v-model:page-size="pagination.pageSize"
+          :total="total"
+          :page-sizes="[10, 20, 50]"
+          layout="total, sizes, prev, pager, next, jumper"
+          background
+          @current-change="handlePageChange"
+          @size-change="handleSizeChange"
+        />
+      </div>
     </div>
 
     <!-- 关联对话框 -->
     <el-dialog
       v-model="bindDialogVisible"
       title="关联本地商品"
-      width="520px"
+      width="680px"
       :close-on-click-modal="false"
     >
+      <!-- ERP 商品信息卡片 -->
+      <div class="flex items-center gap-4 mb-5 p-4 bg-gray-50 rounded-lg">
+        <el-image
+          v-if="currentErpGoods?.picPath"
+          :src="currentErpGoods.picPath"
+          :preview-src-list="[currentErpGoods.picPath]"
+          fit="cover"
+          preview-teleported
+          style="width:72px;height:72px;border-radius:6px;flex-shrink:0"
+        />
+        <div
+          v-else
+          class="w-[72px] h-[72px] rounded-md flex items-center justify-center bg-gray-200 text-gray-400 text-xs flex-shrink-0"
+        >
+          无图
+        </div>
+        <div class="min-w-0">
+          <div class="text-sm text-gray-500">{{ currentErpGoods?.goodsCode }}</div>
+          <div class="text-base font-medium mt-1 truncate">{{ currentErpGoods?.goodsName }}</div>
+          <div class="text-xs text-gray-400 mt-1">
+            规格: {{ currentErpGoods?.specs || '-' }} &nbsp;|&nbsp; 售价: ¥{{ currentErpGoods?.salePrice || '-' }}
+          </div>
+        </div>
+      </div>
+
       <el-form label-width="100px">
-        <el-form-item label="ERP 商品">
-          <el-input
-            :model-value="
-              currentErpGoods?.goodsCode +
-              ' - ' +
-              currentErpGoods?.goodsName
-            "
-            disabled
-          />
-        </el-form-item>
         <el-form-item label="选择本地商品">
           <el-select
             v-model="selectedProductId"
-            placeholder="请选择要关联的本地商品"
+            placeholder="请搜索并选择要关联的本地商品"
             clearable
             filterable
             class="w-full!"
+            popper-class="erp-bind-select-popper"
           >
             <el-option
               v-for="p in productOptions"
               :key="p.id"
               :label="`[${p.code || '无编码'}] ${p.name}`"
               :value="p.id"
-            />
+            >
+              <div class="flex items-center gap-3 py-0.5">
+                <el-image
+                  v-if="p.pic"
+                  :src="p.pic"
+                  fit="cover"
+                  style="width:40px;height:40px;border-radius:4px;flex-shrink:0"
+                />
+                <div
+                  v-else
+                  class="w-[40px] h-[40px] rounded flex items-center justify-center bg-gray-100 text-gray-300 flex-shrink-0"
+                >
+                  <el-icon size="16"><component :is="useRenderIcon(Search)" /></el-icon>
+                </div>
+                <div class="min-w-0">
+                  <div class="text-sm truncate">{{ p.name }}</div>
+                  <div class="text-xs text-gray-400">{{ p.code || '无编码' }}</div>
+                </div>
+              </div>
+            </el-option>
           </el-select>
         </el-form-item>
       </el-form>
@@ -272,3 +342,9 @@ onMounted(() => {
     </el-dialog>
   </div>
 </template>
+
+<style lang="scss">
+.erp-bind-select-popper {
+  max-width: 600px !important;
+}
+</style>

+ 8 - 4
haha-admin/src/main/java/com/haha/admin/controller/ErpGoodsController.java

@@ -1,8 +1,10 @@
 package com.haha.admin.controller;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.haha.admin.annotation.RequirePermission;
 import com.haha.common.annotation.Log;
 import com.haha.common.enums.OperationType;
+import com.haha.common.vo.PageResult;
 import com.haha.common.vo.Result;
 import com.haha.entity.ErpGoods;
 import com.haha.service.ErpGoodsService;
@@ -10,7 +12,6 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.List;
 import java.util.Map;
 
 /**
@@ -36,12 +37,15 @@ public class ErpGoodsController {
     }
 
     /**
-     * 查询已缓存的 ERP 商品列表
+     * 分页查询已缓存的 ERP 商品列表
      */
     @RequirePermission("erp:goods:read")
     @GetMapping
-    public Result<List<ErpGoods>> list(@RequestParam(required = false) String keyword) {
-        return Result.success("查询成功", erpGoodsService.listAll(keyword));
+    public Result<PageResult<ErpGoods>> list(@RequestParam(required = false) String keyword,
+                                             @RequestParam(defaultValue = "1") int page,
+                                             @RequestParam(defaultValue = "10") int pageSize) {
+        IPage<ErpGoods> pageResult = erpGoodsService.listAll(keyword, page, pageSize);
+        return Result.success("查询成功", PageResult.of(pageResult));
     }
 
     /**

+ 6 - 19
haha-service/src/main/java/com/haha/service/ErpGoodsService.java

@@ -1,39 +1,26 @@
 package com.haha.service;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.haha.entity.ErpGoods;
 
-import java.util.List;
-
 /**
  * ERP 商品信息服务接口
  */
 public interface ErpGoodsService extends IService<ErpGoods> {
 
-    /**
-     * 从 ERP 同步系统商品数据到本地缓存
-     */
     void syncFromErp();
 
     /**
-     * 查询已缓存的 ERP 商品列表
+     * 分页查询已缓存的 ERP 商品列表
      *
-     * @param keyword 搜索关键词(商品名称/编码,可选)
+     * @param keyword  搜索关键词(商品名称/编码,可选)
+     * @param page     当前页码
+     * @param pageSize 每页条数
      */
-    List<ErpGoods> listAll(String keyword);
+    IPage<ErpGoods> listAll(String keyword, int page, int pageSize);
 
-    /**
-     * 将本地商品与 ERP 商品一对一关联
-     *
-     * @param productId  本地商品ID
-     * @param erpGoodsId ERP商品主键ID(ERP内部goodsId)
-     */
     void bindGoods(Long productId, Long erpGoodsId);
 
-    /**
-     * 解除本地商品与 ERP 商品的关联
-     *
-     * @param productId 本地商品ID
-     */
     void unbindGoods(Long productId);
 }

+ 13 - 8
haha-service/src/main/java/com/haha/service/impl/ErpGoodsServiceImpl.java

@@ -2,6 +2,8 @@ package com.haha.service.impl;
 
 import com.alibaba.fastjson2.JSON;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.haha.common.exception.BusinessException;
 import com.haha.entity.ErpGoods;
@@ -55,8 +57,8 @@ public class ErpGoodsServiceImpl extends ServiceImpl<ErpGoodsMapper, ErpGoods> i
 
         log.info("开始逐条同步ERP商品: 共 {} 个本地商品", products.size());
         int syncedCount = 0;
-        int missCount = 0;
         int total = products.size();
+        List<String> missedNames = new java.util.ArrayList<>();
 
         for (int i = 0; i < total; i++) {
             Product product = products.get(i);
@@ -85,13 +87,12 @@ public class ErpGoodsServiceImpl extends ServiceImpl<ErpGoodsMapper, ErpGoods> i
                         syncedCount++;
                     }
                 } else {
-                    log.debug("ERP中未找到商品: goodsCode={}, productId={}", goodsCode, product.getId());
-                    missCount++;
+                    missedNames.add(goodsCode);
                 }
 
             } catch (QdbException e) {
-                log.error("查询ERP商品失败: goodsCode={}, productId={}, error={}",
-                        goodsCode, product.getId(), e.getMessage());
+                log.error("查询ERP商品失败: goodsCode={}, error={}", goodsCode, e.getMessage());
+                missedNames.add(goodsCode);
             }
 
             if ((i + 1) % 10 == 0 && i + 1 < total) {
@@ -104,11 +105,15 @@ public class ErpGoodsServiceImpl extends ServiceImpl<ErpGoodsMapper, ErpGoods> i
             }
         }
 
-        log.info("ERP商品同步完成: 处理 {} 条, 命中 {} 条, 未找到 {} 条", total, syncedCount, missCount);
+        log.info("ERP商品同步完成: 处理 {} 条, 命中 {} 条, 未找到 {} 条", total, syncedCount, missedNames.size());
+        if (!missedNames.isEmpty()) {
+            log.info("以下商品在ERP中未找到 ({} 条):\n{}",
+                    missedNames.size(), String.join("\n", missedNames));
+        }
     }
 
     @Override
-    public List<ErpGoods> listAll(String keyword) {
+    public IPage<ErpGoods> listAll(String keyword, int page, int pageSize) {
         LambdaQueryWrapper<ErpGoods> wrapper = new LambdaQueryWrapper<ErpGoods>()
                 .eq(ErpGoods::getStatus, 1);
         if (StringUtils.hasText(keyword)) {
@@ -117,7 +122,7 @@ public class ErpGoodsServiceImpl extends ServiceImpl<ErpGoodsMapper, ErpGoods> i
                     .or()
                     .like(ErpGoods::getGoodsCode, keyword));
         }
-        return list(wrapper.orderByAsc(ErpGoods::getGoodsName));
+        return page(new Page<>(page, pageSize), wrapper.orderByAsc(ErpGoods::getGoodsName));
     }
 
     @Override