Переглянути джерело

feat(photo): 添加货号搜索功能并优化照片列表查询接口

- 修改 Go 后端接口 GetPhotoListApp 添加 goodsArtNo 参数
- 更新 CameraMachineHandler 的 GetPhotoList 方法支持货号过滤
- 在前端 CutoutPage 和 ShadowRename 组件中添加货号搜索输入框
- 实现搜索功能,支持按货号筛选照片列表
- 添加搜索按钮和回车键触发搜索
- 将每页显示数量从 10 条改为 20 条
- 更新 Wails 类型定义和 JavaScript 绑定以支持新参数
rambo 1 тиждень тому
батько
коміт
f0ab6854e0

+ 5 - 0
app.go

@@ -280,3 +280,8 @@ func (a *App) RenameFolderFileNameApp(goodsArtNos []string) ([]interface{}, erro
 
 	return a.cameraMachineHandler.RenameFolderFileName(goodsArtNos)
 }
+
+// HandlerUploadSkuImages 处理SKU图片上传
+func (a *App) HandlerUploadSkuImages(sourceDir string) error {
+	return a.directoryHandler.HandlerUploadSkuImages(sourceDir)
+}

+ 3 - 3
frontend/src/components/Home.vue

@@ -44,9 +44,9 @@ const router = useRouter()
 
 const views = ref<ViewInfo[]>([
   {
-    name: '白底图批量导出',
-    path: '/copy_800_tool',
-    desc: "白底图批量导出",
+    name: '白底图批量上传',
+    path: '/upload_800_tool',
+    desc: "白底图批量上传",
     icon: pzIcon
   },
   {

+ 155 - 0
frontend/src/components/Tools_800_Upload.vue

@@ -0,0 +1,155 @@
+<script setup lang="ts">
+import {ref,nextTick, watch} from "vue"
+import { ElNotification ,ElMessage} from 'element-plus'
+import { ElScrollbar } from 'element-plus'
+import { Folder } from '@element-plus/icons-vue'
+import {SelectDirectory,HandlerUploadSkuImages} from '../../wailsjs/go/main/App'
+import { EventsOn } from '../../wailsjs/runtime'
+import {ProcessingInterface} from "../interfaces/Tools800Interface";
+const  select_root_path = ref<string|null>('')
+const  is_show_empty = ref<boolean>(true)
+select_root_path.value = localStorage.getItem('first_select_root_path')
+const progressing = ref<boolean>(false)
+const  progressList = ref<ProcessingInterface[]>([])
+const scrollbarRef = ref<InstanceType<typeof ElScrollbar> | null>(null)
+const hasShownCompleteMessage = ref<boolean>(false) // 防止重复显示完成消息
+const select_path_click = async () => {
+  try {
+    // 调用 Wails 绑定的方法
+    const path = await SelectDirectory()
+    if (path) {
+      select_root_path.value = path
+      console.error('选择目录:', select_root_path.value)
+      localStorage.setItem('first_select_root_path', select_root_path.value)
+    }
+  } catch (error) {
+    console.error('选择目录失败:', error)
+    select_root_path.value = ""
+  }
+}
+
+const startUploadSkuImages = async () => {
+  console.log('开始上传SKU图片')
+  if(!select_root_path.value){
+    ElNotification({
+      title: '警告',
+      message: '请选择[第一步]需要处理的目录',
+      type: 'warning',
+    })
+    return
+  }
+  is_show_empty.value = false
+  hasShownCompleteMessage.value = false // 重置完成消息标志
+  try {
+    progressing.value = true
+    progressList.value = []
+    // 调用后端SKU图片上传方法
+    await HandlerUploadSkuImages(select_root_path.value)
+  } catch (error) {
+    progressing.value = false
+    ElMessage.error(`上传出错: ${error}`)
+  }
+}
+// 监听处理进度事件
+EventsOn('copy800-progress', (data: any) => {
+  console.log('处理进度:', data)
+  progressList.value.push({
+    progress: data.progress,
+    message: data.message
+  })
+  if (data.progress === 100 && !hasShownCompleteMessage.value) {
+    hasShownCompleteMessage.value = true // 标记已显示完成消息
+    progressing.value = false
+    if (!data.success) {
+      ElMessage.error(`处理失败: ${data.message}`)
+    }
+  }
+})
+// 监听进度列表变化,自动滚动到底部
+watch(() => progressList.value.length, () => {
+  nextTick(() => {
+    if (scrollbarRef.value) {
+      const scrollbar = scrollbarRef.value.wrapRef
+      if (scrollbar) {
+        scrollbar.scrollTop = scrollbar.scrollHeight
+      }
+    }
+  })
+})
+</script>
+
+<template>
+<!--复制800*800目录文件-->
+<div>
+  <div class="container">
+    <br/>
+    <el-alert class="container-1" title="第一步:请选择需要处理的目录" type="success" effect="dark" :closable="false">
+      <el-input
+          class="first-select"
+          v-model="select_root_path"
+          readonly
+          placeholder="请选择需要处理的目录,请选择到日期级;如:2025-01-01"
+      >
+        <template #prepend>
+          <el-button color="#626aef" :icon="Folder" @click="select_path_click()">选择目录</el-button>
+        </template>
+      </el-input>
+    </el-alert>
+    <br/>
+    <el-card  shadow="never" class="progress-card">
+      <p style="text-align: left">处理进度:</p>
+      <el-scrollbar height="400px"  ref="scrollbarRef">
+        <el-empty v-if="progressList.length==0" description="请根据上述提示选择目录" :image-size="100" />
+        <div v-else style="padding-bottom: 15%">
+          <el-text class="mx-1" type="success" v-for="item in progressList">
+            {{item.message}}<br/>
+          </el-text>
+        </div>
+      </el-scrollbar>
+    </el-card>
+    <div class="bottom-container">
+      <el-button :disabled="progressing" type="primary" effect="dark" class="handler_btn" @click="startUploadSkuImages()">上传SKU图片</el-button>
+    </div>
+  </div>
+</div>
+</template>
+
+<style scoped>
+.container{
+  display: flex;
+  flex-direction: column;
+  align-items: center; /* 添加此属性实现水平居中 */
+  min-height: 93vh;
+  width: 95%;
+  margin: 0 auto;
+}
+.first-select{
+  width: 88vw;
+  margin: 0 auto;
+}
+.bottom-container{
+  position: absolute;
+  bottom: 15%;
+  padding: 0 5%;
+  left: 0;
+  text-align: center;
+  display: flex;
+  gap: 10px;
+  justify-content: center;
+}
+.handler_btn{
+  flex: 1;
+  max-width: 45vw;
+}
+:deep(.el-card__body){
+padding: 0 3%;
+}
+.progress-card{
+  max-height: 380px;
+  min-height: 380px;
+  background-color: #cfd9df;
+  width: 95%;
+  margin-top: 3%;
+  overflow-y: hidden;
+}
+</style>

+ 5 - 5
frontend/src/router/index.ts

@@ -1,6 +1,6 @@
 import {createRouter, createWebHashHistory, Router,RouteRecordRaw} from "vue-router";
 import home from "../components/Home.vue";
-import Tools_800_Copy from "../components/Tools_800_Copy.vue";
+import Tools_800_Upload from "../components/Tools_800_Upload.vue";
 import ProductList from "../components/ProductList.vue";
 import ExternalPage from "../components/ExternalPage.vue";
 import CutoutPage from "../components/CutoutPage.vue";
@@ -14,10 +14,10 @@ const routes:RouteRecordRaw[] = [
              }
     },
     {
-        path:"/copy_800_tool", //路径描述
-        name:"copy_800_tool",
-        component: Tools_800_Copy, // 主动引用,无论是否访问均加载页面
-        meta: { title: "复制800*800目录文件",hideBackButton: false }
+        path:"/upload_800_tool", //路径描述
+        name:"upload_800_tool",
+        component: Tools_800_Upload, // 主动引用,无论是否访问均加载页面
+        meta: { title: "白底图批量上传",hideBackButton: false }
     },
     {
         path:"/product_list", //路径描述

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

@@ -12,6 +12,8 @@ export function HandlerDirectory(arg1:string,arg2:string):Promise<void>;
 
 export function HandlerOutPutDirectory():Promise<Array<handlers.ImageResult>>;
 
+export function HandlerUploadSkuImages(arg1:string):Promise<void>;
+
 export function MakeProducts(arg1:Array<handlers.ImageResult>):Promise<Record<string, any>>;
 
 export function OpenFolder(arg1:string):Promise<void>;

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

@@ -22,6 +22,10 @@ export function HandlerOutPutDirectory() {
   return window['go']['main']['App']['HandlerOutPutDirectory']();
 }
 
+export function HandlerUploadSkuImages(arg1) {
+  return window['go']['main']['App']['HandlerUploadSkuImages'](arg1);
+}
+
 export function MakeProducts(arg1) {
   return window['go']['main']['App']['MakeProducts'](arg1);
 }

+ 303 - 0
handlers/directory.go

@@ -1,11 +1,16 @@
 package handlers
 
 import (
+	"Vali-Tools/utils"
+	"bytes"
 	"context"
 	"encoding/base64"
+	"encoding/json"
 	"fmt"
 	"github.com/wailsapp/wails/v2/pkg/runtime"
 	"io"
+	"mime/multipart"
+	"net/http"
 	"os"
 	"path/filepath"
 	"strings"
@@ -201,6 +206,34 @@ type ImageResult struct {
 	ImageBase64 string `json:"image_base64"`
 }
 
+// UploadSkuResult SKU图片上传结果
+type UploadSkuResult struct {
+	FileName  string `json:"FileName"`
+	ImageType string `json:"ImageType"`
+	Size      int64  `json:"Size"`
+}
+
+// UploadSkuResponse SKU图片上传响应
+type UploadSkuResponse struct {
+	Data    []UploadSkuResult `json:"data"`
+	Success bool              `json:"success"`
+	Code    int               `json:"code"`
+}
+
+// BatchFileItem 批量上传文件项
+type BatchFileItem struct {
+	FilePath  string
+	ImageType string
+	FileName  string
+}
+
+// ActionNamesResponse 动作名称响应
+type ActionNamesResponse struct {
+	Code int      `json:"code"`
+	Msg  string   `json:"msg"`
+	Data []string `json:"data"`
+}
+
 // sendProgress 发送进度更新
 func (dh *DirectoryHandler) sendProgress(result ProcessResult) {
 	runtime.EventsEmit(dh.ctx, "copy800-progress", result)
@@ -325,3 +358,273 @@ func (dh *DirectoryHandler) GetApplicationDirectory() (string, error) {
 	}
 	return filepath.Dir(execPath), nil
 }
+
+// HandlerUploadSkuImages 处理SKU图片上传
+func (dh *DirectoryHandler) HandlerUploadSkuImages(sourceDir string) error {
+	// 检查源目录是否存在
+	if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
+		return fmt.Errorf("源目录不存在: %s", sourceDir)
+	}
+
+	// 发送初始进度
+	dh.sendProgress(ProcessResult{
+		Success:  true,
+		Message:  "开始扫描目录...",
+		Progress: 0,
+	})
+
+	// 遍历和处理逻辑
+	err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			dh.sendProgress(ProcessResult{
+				Success:  false,
+				Message:  fmt.Sprintf("访问路径出错: %v", err),
+				Progress: 0,
+			})
+			return err
+		}
+
+		// 处理 800x800 目录的逻辑
+		if info.IsDir() && strings.Contains(info.Name(), "800x800") {
+			fmt.Printf("👍👍👍找到800x800目录: %s\n", path)
+
+			// 获取上级目录名称作为Key参数
+			parentDirName := filepath.Base(filepath.Dir(path))
+
+			// 读取800x800目录下的所有文件
+			files, err := os.ReadDir(path)
+			if err != nil {
+				dh.sendProgress(ProcessResult{
+					Success:  false,
+					Message:  fmt.Sprintf("读取目录失败 %s: %v", path, err),
+					Progress: 0,
+				})
+				return err
+			}
+
+			// 尝试从接口获取动作名称列表
+			var actionNames []string
+			actionNamesResponse, err := dh.getActionNamesByGoods(parentDirName)
+			if err == nil && actionNamesResponse.Code == 0 && len(actionNamesResponse.Data) > 0 {
+				actionNames = actionNamesResponse.Data
+				fmt.Printf("📋 获取到动作名称列表 [%s]: %v\n", parentDirName, actionNames)
+			} else {
+				fmt.Printf("️ 获取动作名称失败或为空,将使用默认逻辑 [%s]\n", parentDirName)
+			}
+
+			// 收集所有有效的图片文件
+			var validFiles []BatchFileItem
+			fileIndex := 0
+
+			for _, file := range files {
+				if file.IsDir() {
+					continue // 跳过子目录
+				}
+
+				// 检查是否为有效的图片文件
+				if !dh.isValidImageFile(file.Name()) {
+					continue // 跳过非图片文件
+				}
+
+				filePath := filepath.Join(path, file.Name())
+
+				// 确定ImageType:如果接口返回了数据且索引有效,则使用接口返回的值
+				var imageType string
+				if len(actionNames) > 0 && fileIndex < len(actionNames) {
+					imageType = actionNames[fileIndex]
+					fmt.Printf("️  使用接口返回的ImageType [%s]: %s\n", file.Name(), imageType)
+				} else {
+					// 使用文件名(不含扩展名)作为ImageType参数
+					imageType = strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))
+					fmt.Printf("🏷️  使用默认ImageType [%s]: %s\n", file.Name(), imageType)
+				}
+				fileIndex++
+
+				validFiles = append(validFiles, BatchFileItem{
+					FilePath:  filePath,
+					ImageType: imageType,
+					FileName:  file.Name(),
+				})
+			}
+
+			// 如果没有有效文件,跳过
+			if len(validFiles) == 0 {
+				fmt.Printf("⚠️ 目录 %s 中没有有效的图片文件\n", path)
+				return nil
+			}
+
+			fmt.Printf("📦 开始批量上传 [%s]: 共 %d 个文件\n", parentDirName, len(validFiles))
+
+			// 批量上传该目录下的所有文件(一次性请求)
+			var response UploadSkuResponse
+			err = dh.uploadSkuImagesBatch(validFiles, parentDirName, &response)
+
+			if err != nil {
+				dh.sendProgress(ProcessResult{
+					Success:  false,
+					Message:  fmt.Sprintf("[%s] 批量上传失败: %v", parentDirName, err),
+					Progress: 0,
+				})
+				fmt.Printf("❌ 批量上传失败: %v\n", err)
+			} else if !response.Success {
+				dh.sendProgress(ProcessResult{
+					Success:  false,
+					Message:  fmt.Sprintf("[%s] 批量上传失败: API返回错误 code=%d", parentDirName, response.Code),
+					Progress: 0,
+				})
+				fmt.Printf("❌ 批量上传失败: API返回错误 code=%d\n", response.Code)
+			} else {
+				dh.sendProgress(ProcessResult{
+					Success:  true,
+					Message:  fmt.Sprintf("[%s] 批量上传成功: 共 %d 个文件", parentDirName, len(response.Data)),
+					Progress: 0,
+				})
+				fmt.Printf("✅ 批量上传成功: %+v\n", response)
+			}
+		}
+
+		return nil
+	})
+
+	// 发送完成进度
+	if err != nil {
+		dh.sendProgress(ProcessResult{
+			Success:  false,
+			Message:  fmt.Sprintf("处理失败: %v", err),
+			Progress: 100,
+		})
+	} else {
+		dh.sendProgress(ProcessResult{
+			Success:  true,
+			Message:  "所有文件处理完成",
+			Progress: 100,
+		})
+	}
+
+	return err
+}
+
+// uploadSkuImagesBatch 批量上传SKU图片(同一个Key下的多个文件一次性上传)
+func (dh *DirectoryHandler) uploadSkuImagesBatch(files []BatchFileItem, key string, result interface{}) error {
+	// 创建multipart表单
+	var buf bytes.Buffer
+	writer := multipart.NewWriter(&buf)
+
+	// 添加Key字段
+	err := writer.WriteField("Key", key)
+	if err != nil {
+		return fmt.Errorf("写入Key字段失败: %w", err)
+	}
+
+	// 添加多个ImageType和File字段对
+	for i, file := range files {
+		// 添加ImageType字段
+		err = writer.WriteField("ImageType", file.ImageType)
+		if err != nil {
+			return fmt.Errorf("写入ImageType字段失败 [%d]: %w", i, err)
+		}
+
+		// 打开文件
+		fileHandle, err := os.Open(file.FilePath)
+		if err != nil {
+			return fmt.Errorf("打开文件失败 [%s]: %w", file.FilePath, err)
+		}
+
+		// 添加文件字段
+		fileWriter, err := writer.CreateFormFile("File", file.FileName)
+		if err != nil {
+			fileHandle.Close()
+			return fmt.Errorf("创建表单文件字段失败 [%s]: %w", file.FileName, err)
+		}
+
+		// 复制文件内容到表单
+		_, err = io.Copy(fileWriter, fileHandle)
+		fileHandle.Close()
+		if err != nil {
+			return fmt.Errorf("复制文件内容失败 [%s]: %w", file.FileName, err)
+		}
+
+		fmt.Printf("📤 已添加文件 [%d/%d]: %s (ImageType: %s)\n", i+1, len(files), file.FileName, file.ImageType)
+	}
+
+	// 关闭表单写入器
+	err = writer.Close()
+	if err != nil {
+		return fmt.Errorf("关闭表单写入器失败: %w", err)
+	}
+
+	// 创建请求
+	req, err := http.NewRequestWithContext(dh.ctx, "POST", utils.UploadSkuImage, &buf)
+	if err != nil {
+		return fmt.Errorf("创建请求失败: %w", err)
+	}
+
+	// 设置内容类型为multipart表单
+	req.Header.Set("Content-Type", writer.FormDataContentType())
+
+	// 创建HTTP客户端并执行请求
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	if err != nil {
+		return fmt.Errorf("请求失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	// 读取响应体
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return fmt.Errorf("读取响应体失败: %w", err)
+	}
+
+	// 检查HTTP状态码
+	if resp.StatusCode >= 400 {
+		return fmt.Errorf("HTTP请求失败 status=%d body=%s", resp.StatusCode, string(body))
+	}
+
+	// 解析JSON响应
+	if err := json.Unmarshal(body, result); err != nil {
+		return fmt.Errorf("解析JSON失败: %w, body=%s", err, string(body))
+	}
+
+	return nil
+}
+
+// getActionNamesByGoods 获取动作名称列表
+func (dh *DirectoryHandler) getActionNamesByGoods(goodsArtNo string) (*ActionNamesResponse, error) {
+	// 创建请求
+	url := fmt.Sprintf("%s?goods_art_no=%s", utils.GetActionNames, goodsArtNo)
+	req, err := http.NewRequestWithContext(dh.ctx, "GET", url, nil)
+	if err != nil {
+		return nil, fmt.Errorf("创建请求失败: %w", err)
+	}
+
+	// 设置请求头
+	req.Header.Set("Content-Type", "application/json")
+
+	// 创建HTTP客户端并执行请求
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, fmt.Errorf("请求失败: %w", err)
+	}
+	defer resp.Body.Close()
+
+	// 读取响应体
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, fmt.Errorf("读取响应体失败: %w", err)
+	}
+
+	// 检查HTTP状态码
+	if resp.StatusCode >= 400 {
+		return nil, fmt.Errorf("HTTP请求失败 status=%d body=%s", resp.StatusCode, string(body))
+	}
+
+	// 解析JSON响应
+	var response ActionNamesResponse
+	if err := json.Unmarshal(body, &response); err != nil {
+		return nil, fmt.Errorf("解析JSON失败: %w, body=%s", err, string(body))
+	}
+
+	return &response, nil
+}

+ 29 - 0
qodana.yaml

@@ -0,0 +1,29 @@
+#-------------------------------------------------------------------------------#
+#               Qodana analysis is configured by qodana.yaml file               #
+#             https://www.jetbrains.com/help/qodana/qodana-yaml.html            #
+#-------------------------------------------------------------------------------#
+version: "1.0"
+
+#Specify inspection profile for code analysis
+profile:
+  name: qodana.starter
+
+#Enable inspections
+#include:
+#  - name: <SomeEnabledInspectionId>
+
+#Disable inspections
+#exclude:
+#  - name: <SomeDisabledInspectionId>
+#    paths:
+#      - <path/where/not/run/inspection>
+
+#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
+#bootstrap: sh ./prepare-qodana.sh
+
+#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
+#plugins:
+#  - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
+
+#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
+linter: jetbrains/qodana-go:latest

+ 4 - 0
utils/apis.go

@@ -7,4 +7,8 @@ const (
 	UploadImages = "/api/upload"
 	// 创建产品册
 	CreateProduct = "/api/ai_image/auto_photo/create_product"
+	// UploadSkuImage 上传SKU图片
+	UploadSkuImage = "http://jhdsoft.gicp.net:13180/pdm/api/vPDM/Upload/UploadSkuImage"
+	// GetActionNames 获取动作名称列表
+	GetActionNames = "http://127.0.0.1:7074/get_action_names_by_goods"
 )