浏览代码

```
feat(app): 添加批量抠图功能和拍照记录管理

- 新增 GetPhotoList 和 GetPhotoListApp 接口用于获取拍照记录列表
- 新增 RemoveBackgroundApp 接口用于批量抠图处理
- 新增 OpenFolder 接口用于打开指定文件夹
- 实现 CameraMachineHandler 处理器,包含拍照记录获取和背景移除功能
- 添加 CutoutPage.vue 组件实现批量抠图界面
- 集成抠图功能到主页,添加相关图标和路由配置
- 优化价格输入组件,支持小数点输入并去除前导零
- 增加网络请求超时时间,添加请求日志输出
- 更新前端文件系统配置,添加图片压缩和base64转换功能
```

rambo 14 小时之前
父节点
当前提交
1a46603511

+ 54 - 8
app.go

@@ -4,23 +4,26 @@ import (
 	"Vali-Tools/handlers" // 根据实际项目路径调整
 	"Vali-Tools/utils"
 	"context"
+	"encoding/json"
 	"fmt"
 	"github.com/wailsapp/wails/v2/pkg/options"
 	"github.com/wailsapp/wails/v2/pkg/runtime"
 	"os"
+	"os/exec"
 	"strings"
 )
 
 // App struct
 type App struct {
-	ctx              context.Context
-	Token            string
-	Env              string
-	directoryHandler *handlers.DirectoryHandler
-	dialogHandler    *handlers.DialogHandler
-	UrlHost          string // 根据实际项目路径调整
-	HomePage         string //首次要打开得页面路径
-	handlerRequest   *handlers.HandlerRequests
+	ctx                  context.Context
+	Token                string
+	Env                  string
+	directoryHandler     *handlers.DirectoryHandler
+	dialogHandler        *handlers.DialogHandler
+	UrlHost              string // 根据实际项目路径调整
+	HomePage             string //首次要打开得页面路径
+	handlerRequest       *handlers.HandlerRequests
+	cameraMachineHandler *handlers.CameraMachineHandler
 }
 
 var wailsContext *context.Context
@@ -53,6 +56,7 @@ func (a *App) startup(ctx context.Context) {
 		println("Token:", a.Token)
 		println("UrlHost", a.UrlHost)
 		a.handlerRequest = handlers.NewHandlerRequests(ctx, a.Token, a.UrlHost)
+		a.cameraMachineHandler = handlers.NewCameraMachineHandler(ctx, a.Token)
 		fmt.Printf("获取到了Token信息: %s\n", a.Token)
 	}
 }
@@ -228,3 +232,45 @@ func (a *App) MakeProducts(imageProduct []handlers.ImageResult) map[string]inter
 	}
 	return nil
 }
+
+// GetPhotoList 获取拍照记录列表
+func (a *App) GetPhotoList() (*handlers.PhotoRecordResponse, error) {
+	request, err := a.handlerRequest.MakeGetRequest(handlers.GetPhotoRecordUrl)
+	if err != nil {
+		return nil, err
+	}
+
+	// 将返回结果转换为 map
+	//resultMap, ok := request.(map[string]interface{})
+	//if !ok {
+	//	return nil, fmt.Errorf("无法将响应转换为 map")
+	//}
+
+	// 解析 JSON 数据
+	jsonData, err := json.Marshal(request)
+	if err != nil {
+		return nil, fmt.Errorf("序列化响应数据失败: %v", err)
+	}
+
+	var response handlers.PhotoRecordResponse
+	if err := json.Unmarshal(jsonData, &response); err != nil {
+		return nil, fmt.Errorf("解析JSON数据失败: %v", err)
+	}
+
+	fmt.Printf("数据列表获取成功,共 %d 条记录\n", len(response.List))
+	return &response, nil
+}
+func (a *App) GetPhotoListApp(page string) (*handlers.PhotoRecordResponse, error) {
+
+	return a.cameraMachineHandler.GetPhotoList(page)
+}
+
+func (a *App) RemoveBackgroundApp(goodsArtNos []string) (string, error) {
+
+	return a.cameraMachineHandler.RemoveBackground(goodsArtNos)
+}
+func (a *App) OpenFolder(path string) error {
+	var cmd *exec.Cmd
+	cmd = exec.Command("explorer", path)
+	return cmd.Start()
+}

二进制
frontend/src/assets/images/kt.png


+ 421 - 0
frontend/src/components/CutoutPage.vue

@@ -0,0 +1,421 @@
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue';
+import { GetPhotoListApp,RemoveBackgroundApp,OpenFolder } from '../../wailsjs/go/main/App';
+import { ElMessageBox ,ElLoading} from 'element-plus'
+// 页面状态
+const loading = ref(false);
+const loadingMore = ref(false);
+const error = ref<string | null>(null);
+const fullscreenLoading = ref(false)
+// 数据
+const photoList = ref<any[]>([]);
+const selectedProducts = ref<number[]>([]); // 存储选中的货号索引
+
+// 分页状态
+const currentPage = ref(1);
+const pageSize = ref(10); // 每页显示10条记录
+const totalCount = ref(0);
+const totalPages = ref(1);
+const hasPrev = ref(false);
+const hasNext = ref(true); // 默认有下一页,直到加载完成
+
+// 计算已选择的图片数量
+const selectedCount = computed(() => {
+  return selectedProducts.value.length;
+});
+
+// 获取拍照列表数据
+const fetchPhotoList = async (page: number = 1, append = false) => {
+  if (page === 1) {
+    loading.value = true;
+  } else {
+    loadingMore.value = true;
+  }
+
+  error.value = null;
+
+  try {
+    // 调用API时传递分页参数
+    const data = await GetPhotoListApp(page.toString());
+    console.log("获取的拍照列表数据:", data);
+
+    if (data) {
+      if (append) {
+        // 追加数据到现有列表
+        photoList.value = [...photoList.value, ...data.list];
+      } else {
+        // 替换整个列表
+        photoList.value = data.list || [];
+      }
+
+      // 设置分页信息
+      currentPage.value = data.current_page || 1;
+      pageSize.value = data.size || 10;
+      totalCount.value = data.total_count || 0;
+      totalPages.value = data.total_pages || 1;
+      hasPrev.value = data.has_prev || false;
+      hasNext.value = data.has_next || false;
+    } else {
+      console.warn('返回的数据格式不正确:', data);
+      error.value = '获取的数据格式不正确';
+    }
+  } catch (err) {
+    console.error('获取拍照列表失败:', err);
+    error.value = err instanceof Error ? err.message : '获取数据失败';
+  } finally {
+    loading.value = false;
+    loadingMore.value = false;
+  }
+};
+
+// 加载更多数据
+const loadMore = async () => {
+  if (!hasNext.value) return;
+
+  const nextPage = currentPage.value + 1;
+  await fetchPhotoList(nextPage, true); // 追加数据
+};
+
+// 刷新数据(重新从第一页开始)
+const refreshData = async () => {
+  await fetchPhotoList(1, false);
+};
+
+// 检查货号是否被选中
+const isProductSelected = (index: number) => {
+  // 由于列表现在是追加的,我们需要根据实际数据的索引判断
+  return selectedProducts.value.includes(index);
+};
+
+// 处理货号选择变化
+const handleProductSelectionChange = (index: number) => {
+  const isSelected = selectedProducts.value.includes(index);
+  if (isSelected) {
+    // 如果已选中,则取消选中
+    selectedProducts.value = selectedProducts.value.filter(i => i !== index);
+  } else {
+    // 如果未选中,则添加到选中列表
+    selectedProducts.value.push(index);
+  }
+};
+
+// 发起批量抠图的事件处理函数
+const handleBatchCutout = async() => {
+  console.log('发起批量抠图请求');
+  console.log('选中的货号索引:', selectedProducts.value);
+  // 获取选中的货号和对应的图片
+  const selectedData = selectedProducts.value.map(index => {
+    // 确保索引在当前列表范围内
+    if (index >= 0 && index < photoList.value.length) {
+      return photoList.value[index].goods_art_no;
+    }
+    return null;
+  }).filter(item => item !== null);
+  console.log('选中的数据:', selectedData);
+  const loading = ElLoading.service({
+    lock: true,
+    text: '处理中,请稍后...',
+    background: 'rgba(0, 0, 0, 0.7)',
+  })
+  try {
+    // 这里可以调用 API 或触发其他逻辑
+    const output_folder = await RemoveBackgroundApp(selectedData)
+    if (output_folder) {
+      console.log('抠图成功,输出文件夹:', output_folder)
+      await ElMessageBox.confirm('抠图成功,点击确认打开目录',{
+        closeOnClickModal: false, // 点击遮罩层不关闭
+        closeOnPressEscape: false, // 按ESC键不关闭
+        showCancelButton: false,   // 隐藏取消按钮
+      }).then(async () => {
+        await OpenFolder(output_folder)
+      })
+      selectedProducts.value = []
+    } else {
+      console.error('抠图失败')
+      await ElMessageBox.confirm('抠图失败,请稍后重试',{
+        closeOnClickModal: false, // 点击遮罩层不关闭
+        closeOnPressEscape: false, // 按ESC键不关闭
+        showCancelButton: false,   // 隐藏取消按钮
+      })
+    }
+  }catch (err){
+    loading.close ()
+    await ElMessageBox.confirm('抠图失败,请稍后重试',{
+      closeOnClickModal: false, // 点击遮罩层不关闭
+      closeOnPressEscape: false, // 按ESC键不关闭
+      showCancelButton: false,   // 隐藏取消按钮
+    })
+  }finally {
+    loading.close ()
+  }
+};
+
+onMounted(() => {
+  fetchPhotoList(1, false);
+});
+</script>
+
+<template>
+  <div class="cutout-page">
+    <!-- 加载状态 -->
+    <div v-if="loading" class="loading-state">
+      <el-spin size="large" />
+      <p>正在加载拍照记录,请稍候...</p>
+    </div>
+
+    <!-- 错误状态 -->
+    <div v-else-if="error" class="error-state">
+      <el-alert
+          :title="'错误: ' + error"
+          type="error"
+          :closable="false"
+      />
+      <el-button @click="refreshData" type="primary" style="margin-top: 20px;">
+        重新加载
+      </el-button>
+    </div>
+
+    <!-- 正常内容 -->
+    <div v-else>
+      <!-- 货号卡片列表 -->
+      <div class="product-cards-container">
+        <div
+            v-for="(photo, index) in photoList"
+            :key="`${photo.goods_art_no}-${index}`"
+            class="product-card"
+            :class="{ 'selected': isProductSelected(index) }"
+        >
+          <!-- 卡片头部 -->
+          <div class="card-header">
+            <div class="checkbox-wrapper">
+              <el-checkbox
+                  :model-value="isProductSelected(index)"
+                  @change="handleProductSelectionChange(index)"
+              ></el-checkbox>
+            </div>
+            <div class="card-info">
+              <span class="product-code">{{ photo.goods_art_no }}</span>
+              <span class="timestamp">{{ photo.action_time }}</span>
+            </div>
+          </div>
+
+          <!-- 图片区域 -->
+          <div class="image-container">
+            <div class="image-row">
+              <div
+                  v-for="(img, imgIndex) in photo.items"
+                  :key="imgIndex"
+                  class="image-item"
+              >
+                <el-image
+                    :src="`data:image/jpg;base64,${img}`"
+                    alt="产品图"
+                    class="product-image"
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 加载更多按钮 -->
+      <div class="load-more-container">
+        <el-button
+            v-if="hasNext"
+            :loading="loadingMore"
+            @click="loadMore"
+            type="primary"
+            plain
+        >
+          {{ loadingMore ? '加载中...' : '点击加载更多' }}
+        </el-button>
+        <div v-else class="no-more-data">
+          已加载全部数据
+        </div>
+      </div>
+    </div>
+
+    <!-- 底部按钮区域(固定在底部) -->
+    <div class="fixed-bottom-bar">
+      <div class="selected-info">
+        已选择 <span class="selected-count">{{ selectedCount }}</span> 个货号
+      </div>
+      <el-button
+          type="primary"
+          @click="handleBatchCutout"
+          :disabled="selectedProducts.length === 0"
+          size="medium"
+      >
+        发起批量抠图
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.cutout-page {
+  padding: 20px;
+  background-color: #f9f9f9;
+  min-height: 100vh;
+  padding-bottom: 10px; /* 为固定底部按钮留出空间 */
+}
+
+.product-cards-container {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(48%, 1fr));
+  gap: 16px;
+  margin-bottom: 16px; /* 调整间距 */
+}
+
+/* 加载更多容器 */
+.load-more-container {
+  margin: 20px 0;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding-bottom: 80px; /* 为底部按钮留出空间 */
+}
+
+.no-more-data {
+  color: #999;
+  font-size: 14px;
+  padding: 10px;
+}
+
+/* 货号卡片 */
+.product-card {
+  background-color: white;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  border: 1px solid #ddd;
+  overflow: hidden;
+  transition: all 0.3s ease;
+  min-width: 0;
+  width: 100%;
+}
+
+.product-card:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.product-card.selected {
+  border-color: #409eff;
+  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
+}
+
+/* 卡片头部 */
+.card-header {
+  padding: 12px 16px;
+  background-color: #eef2ff;
+  border-bottom: 1px solid #d9d9d9;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.checkbox-wrapper {
+  width: 20px;
+  height: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.card-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+
+.product-code {
+  font-weight: bold;
+  color: #333;
+  font-size: 14px;
+}
+
+.timestamp {
+  color: #666;
+  font-size: 12px;
+}
+
+/* 图片容器 */
+.image-container {
+  padding: 16px;
+}
+
+.image-row {
+  display: flex;
+  gap: 8px;
+  overflow-x: auto;
+  padding: 8px 0;
+}
+
+.image-item {
+  position: relative;
+  border-radius: 4px;
+  overflow: hidden;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.product-image {
+  max-width: 100%;
+  max-height: 100%;
+  object-fit: contain;
+  padding: 8px;
+}
+
+/* 固定底部按钮栏 */
+.fixed-bottom-bar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  padding: 16px 20px;
+  background-color: white;
+  border-top: 1px solid #ddd;
+  box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  z-index: 100;
+}
+
+.selected-info {
+  color: #666;
+  font-size: 14px;
+}
+
+.selected-count {
+  color: #409eff;
+  font-weight: bold;
+}
+
+.el-button {
+  padding: 12px 24px;
+  font-size: 16px;
+}
+
+/* 加载状态 */
+.loading-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 20px;
+  text-align: center;
+}
+
+/* 错误状态 */
+.error-state {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 20px;
+  text-align: center;
+}
+</style>

+ 11 - 4
frontend/src/components/Home.vue

@@ -38,6 +38,7 @@ import { useRouter } from 'vue-router'
 import { ViewInfo } from "../interfaces/HomeINterface"
 import pzIcon from '../assets/images/pz.png'
 import gyIcon from '../assets/images/gy.png'
+import ktIcon from '../assets/images/kt.png'
 const router = useRouter()
 
 const views = ref<ViewInfo[]>([
@@ -53,6 +54,12 @@ const views = ref<ViewInfo[]>([
     desc: "制作产品册",
     icon: gyIcon
   },
+  {
+    name: '批量抠图',
+    path: '/cut_out',
+    desc: "批量抠图",
+    icon: ktIcon
+  },
 ])
 
 const goToTool = (path: string) => {
@@ -91,13 +98,13 @@ const goToTool = (path: string) => {
   cursor: pointer;
   transition: all 0.3s ease;
   border-radius: 12px;
-  height: 200px;
+  height: 150px;
 }
 .tool-card {
   cursor: pointer;
   transition: all 0.3s ease;
   border-radius: 12px;
-  height: 200px;
+  height: 150px;
   border: 1px solid #e4e7ed;
   background: #37353E;
 }
@@ -136,11 +143,11 @@ const goToTool = (path: string) => {
 }
 .tool-name {
   flex: 2; /* 占据1份空间 */
-  font-size: 1.5rem;
+  font-size: 1.2rem;
   font-weight: 500;
   text-align: center;
   color: #ffffff;
-  line-height: 1.4;
+  line-height: 1.2;
   display: flex;
   align-items: center;
   justify-content: center;

+ 26 - 2
frontend/src/components/ProductList.vue

@@ -59,9 +59,10 @@
                 v-model="product.price"
                 placeholder="请输入价格"
                 size="default"
-                type="number"
+                type="text"
+                inputmode="decimal"
                 class="full-width-input"
-                @input="saveProduct(index)"
+                @input="handlePriceInput(index)"
             />
           </div>
         </div>
@@ -123,6 +124,29 @@ const generateCatalog = async () => {
     loading.close()
   }
 }
+
+// 处理价格输入,去除前导零
+const handlePriceInput = (index: number) => {
+  const value = products.value[index].price
+  if (value) {
+    // 去除前导零(但保留单独的"0")
+    let formattedValue = value.replace(/^0+(\d)/, '$1')
+    // 确保不会出现空字符串(当输入"0"时)
+    if (formattedValue === '') {
+      formattedValue = '0'
+    }
+    // 只保留数字和一个小数点
+    formattedValue = formattedValue.replace(/[^0-9.]/g, '')
+    // 限制小数点数量
+    const dotIndex = formattedValue.indexOf('.')
+    if (dotIndex !== -1) {
+      formattedValue = formattedValue.substring(0, dotIndex + 1) + formattedValue.substring(dotIndex).replace(/\./g, '')
+    }
+    products.value[index].price = formattedValue
+  }
+  saveProduct(index)
+}
+
 // 异步获取应用参数
 const fetchOutPut = async () => {
   const loading = ElLoading.service({

+ 9 - 0
frontend/src/router/index.ts

@@ -3,6 +3,7 @@ import home from "../components/Home.vue";
 import Tools_800_Copy from "../components/Tools_800_Copy.vue";
 import ProductList from "../components/ProductList.vue";
 import ExternalPage from "../components/ExternalPage.vue";
+import CutoutPage from "../components/CutoutPage.vue";
 
 const routes:RouteRecordRaw[] = [
     {
@@ -31,6 +32,14 @@ const routes:RouteRecordRaw[] = [
         meta: {
             title: "产品报价图册",hideBackButton: false // 如果需要隐藏返回按钮
         }
+    },
+    {
+        path: '/cut_out',
+        name: 'CutoutPage',
+        component: CutoutPage,
+        meta: {
+            title: "批量抠图",hideBackButton: false // 如果需要隐藏返回按钮
+        }
     }
 ]
 

+ 5 - 0
frontend/vite.config.ts

@@ -4,4 +4,9 @@ import vue from '@vitejs/plugin-vue'
 // https://vitejs.dev/config/
 export default defineConfig({
   plugins: [vue()],
+  server:{
+    fs: {
+      strict: false,
+    },
+  }
 })

+ 8 - 0
frontend/wailsjs/go/main/App.d.ts

@@ -4,10 +4,18 @@ import {handlers} from '../models';
 
 export function GetAppArgument():Promise<any>;
 
+export function GetPhotoList():Promise<handlers.PhotoRecordResponse>;
+
+export function GetPhotoListApp(arg1:string):Promise<handlers.PhotoRecordResponse>;
+
 export function HandlerDirectory(arg1:string,arg2:string):Promise<void>;
 
 export function HandlerOutPutDirectory():Promise<Array<handlers.ImageResult>>;
 
 export function MakeProducts(arg1:Array<handlers.ImageResult>):Promise<Record<string, any>>;
 
+export function OpenFolder(arg1:string):Promise<void>;
+
+export function RemoveBackgroundApp(arg1:Array<string>):Promise<string>;
+
 export function SelectDirectory():Promise<string>;

+ 16 - 0
frontend/wailsjs/go/main/App.js

@@ -6,6 +6,14 @@ export function GetAppArgument() {
   return window['go']['main']['App']['GetAppArgument']();
 }
 
+export function GetPhotoList() {
+  return window['go']['main']['App']['GetPhotoList']();
+}
+
+export function GetPhotoListApp(arg1) {
+  return window['go']['main']['App']['GetPhotoListApp'](arg1);
+}
+
 export function HandlerDirectory(arg1, arg2) {
   return window['go']['main']['App']['HandlerDirectory'](arg1, arg2);
 }
@@ -18,6 +26,14 @@ export function MakeProducts(arg1) {
   return window['go']['main']['App']['MakeProducts'](arg1);
 }
 
+export function OpenFolder(arg1) {
+  return window['go']['main']['App']['OpenFolder'](arg1);
+}
+
+export function RemoveBackgroundApp(arg1) {
+  return window['go']['main']['App']['RemoveBackgroundApp'](arg1);
+}
+
 export function SelectDirectory() {
   return window['go']['main']['App']['SelectDirectory']();
 }

+ 4 - 1
go.mod

@@ -4,7 +4,10 @@ go 1.23.0
 
 toolchain go1.24.9
 
-require github.com/wailsapp/wails/v2 v2.10.2
+require (
+	github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
+	github.com/wailsapp/wails/v2 v2.10.2
+)
 
 require (
 	github.com/bep/debounce v1.2.1 // indirect

+ 2 - 0
go.sum

@@ -34,6 +34,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
 github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
+github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
 github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=

+ 259 - 0
handlers/camera_machine_handler.go

@@ -0,0 +1,259 @@
+package handlers
+
+import (
+	"bytes"
+	"context"
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"github.com/nfnt/resize"
+	"image"
+	"image/jpeg"
+	"image/png"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+const (
+	cameraMachineUrl = "http://127.0.0.1:7074"
+	// GetPhotoRecordUrl 获取照片记录
+	GetPhotoRecordUrl = "/get_photo_records"
+	// RemoveBackground 移除背景
+	RemoveBackground = "/remove_background"
+)
+
+type CameraMachineHandler struct {
+	ctx            context.Context
+	handlerRequest *HandlerRequests
+	token          string
+}
+
+func NewCameraMachineHandler(ctx context.Context, token string) *CameraMachineHandler {
+	return &CameraMachineHandler{
+		token: token,
+		ctx:   ctx,
+	}
+}
+
+// PhotoRecord 表示单个照片记录
+type PhotoRecord struct {
+	ID              int     `json:"id"`
+	ImagePath       string  `json:"image_path"`
+	ImageDealMode   int     `json:"image_deal_mode"`
+	UpdateTime      string  `json:"update_time"`
+	DeleteTime      *string `json:"delete_time"`
+	ActionID        int     `json:"action_id"`
+	GoodsArtNo      string  `json:"goods_art_no"`
+	ImageIndex      int     `json:"image_index"`
+	PhotoCreateTime string  `json:"photo_create_time"`
+	CreateTime      string  `json:"create_time"`
+}
+
+// PhotoRecordItem 表示包含照片记录和动作名称的项目
+type PhotoRecordItem struct {
+	PhotoRecord PhotoRecord `json:"PhotoRecord"`
+	ActionName  string      `json:"action_name"`
+}
+
+// PhotoRecordList 表示单个货号的照片记录列表
+type PhotoRecordList struct {
+	GoodsArtNo string   `json:"goods_art_no"`
+	ActionTime string   `json:"action_time"`
+	Items      []string `json:"items"`
+}
+
+// PhotoRecordResponse 表示完整的API响应
+type PhotoRecordResponse struct {
+	List        []PhotoRecordList `json:"list"`
+	CurrentPage int               `json:"current_page"`
+	Size        int               `json:"size"`
+	TotalCount  int               `json:"total_count"`
+	TotalPages  int               `json:"total_pages"`
+	HasPrev     bool              `json:"has_prev"`
+	HasNext     bool              `json:"has_next"`
+}
+
+// convertImageToBase64 将图片文件转换为 base64 编码
+// compressAndEncodeImage 压缩图片并转换为 base64 编码
+func (t *CameraMachineHandler) compressAndEncodeImage(imagePath string, maxWidth uint) (string, error) {
+	// 检查文件是否存在
+	if _, err := os.Stat(imagePath); os.IsNotExist(err) {
+		return "", fmt.Errorf("图片文件不存在: %s", imagePath)
+	}
+
+	// 读取图片文件
+	file, err := os.Open(imagePath)
+	if err != nil {
+		return "", fmt.Errorf("打开图片文件失败: %v", err)
+	}
+	defer file.Close()
+
+	// 根据文件扩展名确定图片类型
+	ext := strings.ToLower(filepath.Ext(imagePath))
+
+	var img image.Image
+	switch ext {
+	case ".jpg", ".jpeg":
+		img, err = jpeg.Decode(file)
+	case ".png":
+		img, err = png.Decode(file)
+	default:
+		return "", fmt.Errorf("不支持的图片格式: %s", ext)
+	}
+
+	if err != nil {
+		return "", fmt.Errorf("解码图片失败: %v", err)
+	}
+
+	// 获取原始图片尺寸
+	originalBounds := img.Bounds()
+	originalWidth := uint(originalBounds.Dx())
+	originalHeight := uint(originalBounds.Dy())
+
+	// 如果原始宽度小于等于最大宽度,则不需要压缩
+	var resizedImg image.Image
+	if originalWidth <= maxWidth {
+		resizedImg = img
+	} else {
+		// 按比例计算新高度
+		newHeight := uint(float64(originalHeight) * float64(maxWidth) / float64(originalWidth))
+		// 调整图片大小
+		resizedImg = resize.Resize(maxWidth, newHeight, img, resize.Lanczos3)
+	}
+
+	// 将压缩后的图片编码为 JPEG 格式
+	var buf bytes.Buffer
+	err = jpeg.Encode(&buf, resizedImg, &jpeg.Options{Quality: 80})
+	if err != nil {
+		return "", fmt.Errorf("编码压缩后的图片失败: %v", err)
+	}
+
+	// 将图片数据转换为 base64 编码
+	base64Data := base64.StdEncoding.EncodeToString(buf.Bytes())
+
+	return base64Data, nil
+}
+
+// GetPhotoList 获取拍照记录列表
+func (t *CameraMachineHandler) GetPhotoList(page string) (*PhotoRecordResponse, error) {
+	t.handlerRequest = NewHandlerRequests(t.ctx, t.token, cameraMachineUrl)
+	request, err := t.handlerRequest.MakeGetRequest(GetPhotoRecordUrl + "?page=" + page)
+	if err != nil {
+		return nil, err
+	}
+
+	// 将返回结果转换为 map 以提取 data 字段
+	resultMap, ok := request.(map[string]interface{})
+	if !ok {
+		return nil, fmt.Errorf("无法将响应转换为 map")
+	}
+
+	// 提取 data 字段
+	dataValue, exists := resultMap["data"]
+	if !exists {
+		return nil, fmt.Errorf("响应中不存在 data 字段")
+	}
+
+	// 将 data 字段序列化为 JSON
+	dataJson, err := json.Marshal(dataValue)
+	if err != nil {
+		return nil, fmt.Errorf("序列化 data 字段失败: %v", err)
+	}
+	// 先解析为原始响应结构
+	var originalResponse struct {
+		List []struct {
+			GoodsArtNo string            `json:"goods_art_no"`
+			ActionTime string            `json:"action_time"`
+			Items      []PhotoRecordItem `json:"items"`
+		} `json:"list"`
+		CurrentPage int  `json:"current_page"`
+		Size        int  `json:"size"`
+		TotalCount  int  `json:"total_count"`
+		TotalPages  int  `json:"total_pages"`
+		HasPrev     bool `json:"has_prev"`
+		HasNext     bool `json:"has_next"`
+	}
+	// 解析为 PhotoRecordResponse 结构
+	if err := json.Unmarshal(dataJson, &originalResponse); err != nil {
+		return nil, fmt.Errorf("解析原始数据失败: %v", err)
+	}
+
+	// 创建新的响应结构,将图片路径转换为 base64
+	response := &PhotoRecordResponse{
+		CurrentPage: originalResponse.CurrentPage,
+		Size:        originalResponse.Size,
+		TotalCount:  originalResponse.TotalCount,
+		TotalPages:  originalResponse.TotalPages,
+		HasPrev:     originalResponse.HasPrev,
+		HasNext:     originalResponse.HasNext,
+	}
+
+	for _, list := range originalResponse.List {
+		var base64Items []string
+		for idx, item := range list.Items {
+			if idx >= 1 {
+				break
+			}
+			// 将图片路径转换为 base64
+			base64Image, err := t.compressAndEncodeImage(item.PhotoRecord.ImagePath, 400)
+			if err != nil {
+				fmt.Printf("转换图片失败 %s: %v\n", item.PhotoRecord.ImagePath, err)
+				continue
+			}
+			base64Items = append(base64Items, base64Image)
+		}
+
+		photoRecordList := PhotoRecordList{
+			GoodsArtNo: list.GoodsArtNo,
+			ActionTime: list.ActionTime,
+			Items:      base64Items,
+		}
+		response.List = append(response.List, photoRecordList)
+	}
+
+	fmt.Printf("数据列表获取成功,共 %d 条记录\n", len(response.List))
+	return response, nil
+}
+
+// RemoveBackground 批量处理抠图操作
+func (t *CameraMachineHandler) RemoveBackground(goodsArtNos []string) (string, error) {
+	t.handlerRequest = NewHandlerRequests(t.ctx, t.token, cameraMachineUrl)
+	postData := map[string]interface{}{}
+	postData["goods_art_nos"] = goodsArtNos
+	postData["token"] = t.token
+	request, err := t.handlerRequest.MakePostRequest(RemoveBackground, postData)
+	if err != nil {
+		return "", err
+	}
+	// 将返回结果转换为 map 以提取 data 字段
+	resultMap, ok := request.(map[string]interface{})
+	if !ok {
+		return "", fmt.Errorf("无法将响应转换为 map")
+	}
+
+	// 提取 data 字段
+	dataValue, exists := resultMap["data"]
+	if !exists {
+		return "", fmt.Errorf("响应中不存在 data 字段")
+	}
+	// 解析 data 字段为 map
+	dataMap, ok := dataValue.(map[string]interface{})
+	if !ok {
+		return "", fmt.Errorf("data 字段不是有效的 map 类型")
+	}
+
+	// 获取 output_folder 值
+	outputFolder, exists := dataMap["output_folder"]
+	if !exists {
+		return "", fmt.Errorf("data 字段中不存在 output_folder")
+	}
+
+	outputFolderPath, ok := outputFolder.(string)
+	if !ok {
+		return "", fmt.Errorf("output_folder 不是字符串类型")
+	}
+
+	fmt.Printf("输出文件夹路径: %s\n", outputFolderPath)
+	return outputFolderPath, nil
+}

+ 0 - 1
main.go

@@ -23,7 +23,6 @@ func main() {
 		DisableResize: true, //禁止调整尺寸
 		AssetServer: &assetserver.Options{
 			Assets: assets,
-			//Handler: utils.NewFileLoader(),
 		},
 		BackgroundColour: &options.RGBA{R: 255, G: 255, B: 255, A: 1},
 		OnStartup:        app.startup,

+ 61 - 20
utils/file.go

@@ -1,22 +1,63 @@
 package utils
 
-//type FileLoader struct {
-//	http.Handler
-//}
-//
-//func NewFileLoader() *FileLoader {
-//	return &FileLoader{}
-//}
-//
-//func (h *FileLoader) ServeHTTP(res http.ResponseWriter, req *http.Request) {
-//	var err error
-//	requestedFilename := strings.TrimPrefix(req.URL.Path, "/")
-//	println("Requesting file:", requestedFilename)
-//	fileData, err := os.ReadFile(requestedFilename)
-//	if err != nil {
-//		res.WriteHeader(http.StatusBadRequest)
-//		res.Write([]byte(fmt.Sprintf("Could not load file %s", requestedFilename)))
-//	}
-//
-//	res.Write(fileData)
-//}
+import (
+	"net/http"
+	"path/filepath"
+	"strings"
+)
+
+type FileLoader struct {
+	http.Handler
+}
+
+func NewFileLoader() *FileLoader {
+	return &FileLoader{}
+}
+
+func (f *FileLoader) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	if isImageRequest(r.URL.Path) {
+		// 从URL参数获取图片路径
+		imagePath := r.URL.Query().Get("path")
+		if imagePath != "" {
+			// 安全检查:防止路径遍历攻击
+			//if IsValidImagePath(imagePath) {
+			//	http.ServeFile(w, r, imagePath)
+			//	return
+			//}
+		}
+	}
+
+	// 默认处理其他请求
+	http.NotFound(w, r)
+}
+
+// 检查是否是图片请求
+func isImageRequest(path string) bool {
+	ext := strings.ToLower(filepath.Ext(path))
+	return ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".gif" || ext == ".webp"
+}
+
+// IsValidImagePath 验证图片路径是否安全
+func IsValidImagePath(imagePath string) bool {
+	// 清理路径
+	cleanPath := filepath.Clean(imagePath)
+	absPath, err := filepath.Abs(cleanPath)
+	if err != nil {
+		return false
+	}
+
+	// 限制在允许的目录范围内,这里可以根据你的项目结构调整
+	// 例如,只允许访问项目目录下的特定文件夹
+	allowedDir := "/path/to/your/images/directory" // 替换为实际的允许目录
+
+	// 你也可以使用相对路径,如项目根目录下的 images 文件夹
+	projectRoot, _ := filepath.Abs(".")
+	allowedDir = filepath.Join(projectRoot, "images")
+
+	relPath, err := filepath.Rel(allowedDir, absPath)
+	if err != nil {
+		return false
+	}
+
+	return !strings.HasPrefix(relPath, "..")
+}

+ 2 - 1
utils/network_client.go

@@ -24,7 +24,7 @@ type HTTPClient struct {
 func NewHTTPClient(baseURL, token string) *HTTPClient {
 	return &HTTPClient{
 		client: &http.Client{
-			Timeout: 60 * time.Second,
+			Timeout: 6000 * time.Second,
 		},
 		baseURL:       baseURL,
 		Authorization: token,
@@ -39,6 +39,7 @@ func (c *HTTPClient) SetToken(token string) {
 // Get 发起GET请求
 func (c *HTTPClient) Get(ctx context.Context, endpoint string, headers map[string]string) (*http.Response, error) {
 	url := c.baseURL + endpoint
+	fmt.Printf("请求接口地址:%v\n", url)
 	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
 	if err != nil {
 		return nil, fmt.Errorf("创建请求失败: %w", err)