【Python 实战】---- 实现一个可选择、配置操作的批量文件上传工具(二)上传界面事件实现

118 阅读9分钟

1. 前言

很早之前,我实现过一个【Python 实战】---- 接口自动化:60行代码,如何通过Python requests实现图片上传,这个的确简化了前端开发对图片操作的时间,但是随着项目开发的越多,遇到一个新的问题,就是图片需要上传到每个项目的自己服务器中,而且每个项目的参数也不一样,有的需要加密,有的不需要,这就会出现我们需要根据不同的项目,对代码进行上传的微调,然后再打包成工具使用,最后就会发现有很多工具,完全达不到一个程序员的优雅,因此就想到使用 GUI 配置管理,将所有的工具进行配置管理。上一篇将读取配置文件和基础界面结构实现,这一篇就是对界面上按钮输入框的事件处理,主要事件:选择上传配置、上传文件、导出文件、复制文件、预览文件。

2. 选择事件【选择使用那个配置项进行上传】

2.1 实现分析

  1. 从配置下拉框(config_combobox)中获取用户选择的配置显示名称。
  2. 检查是否选择了配置,如果没有选择则将当前配置设置为None
  3. 如果选择了配置,则通过config_name_map映射获取实际的配置名称(处理显示名称与内部名称不一致的情况)。
  4. 使用配置管理器(config_manager)根据实际配置名称获取配置数据。
  5. 如果成功获取到配置数据:
    • 将配置数据封装成UploadConfig对象并赋值给current_config属性
    • 调用日志回调函数显示成功加载配置的消息
  6. 如果未能获取到配置数据:
    • 将当前配置设置为None
    • 调用日志回调函数显示加载配置失败的消息
  7. 当没有选择任何配置时,同样将当前配置设置为None

2.2 实现代码

    def load_selected_config(self):
        """加载选中的配置"""
        selected_display_name = self.config_combobox.get()
        if selected_display_name:
            # 通过映射获取实际的配置名称
            selected_name = self.config_name_map.get(selected_display_name, selected_display_name)
            config_data = self.config_manager.get_config(selected_name)
            if config_data:
                self.current_config = UploadConfig(config_data)
                self.log_callback(f"已加载配置: {selected_display_name}")
            else:
                self.current_config = None
                self.log_callback(f"无法加载配置: {selected_display_name}")
        else:
            self.current_config = None

2.3 实现效果

输入图片说明

3. 开始上传

3.1 实现分析

  1. 验证是否已选择上传配置,未选择则弹出错误提示并返回。
  2. 获取用户选择的目录路径并验证其有效性,无效则弹出错误提示并返回。
  3. 解析用户输入的文件类型后缀列表并验证,为空则弹出错误提示并返回。
  4. 禁用上传按钮防止重复点击,并在日志中记录开始上传的信息。
  5. 创建 FileUploader 实例并调用其 upload_images 方法执行实际的文件上传操作。
  6. 根据上传结果:
    • 如果有上传结果,统计成功和失败的文件数量并在日志中显示上传完成信息,同时保存上传结果供后续导出使用
    • 如果没有上传结果,在日志中显示未找到匹配文件的信息
  7. 重新启用上传按钮。

3.2 实现代码

def start_upload(self):
    """开始上传"""
    if not self.current_config:
        messagebox.showerror("错误", "请先选择一个配置")
        return
        
    dir_path = self.dir_entry.get()
    if not dir_path or not os.path.exists(dir_path):
        messagebox.showerror("错误", "请选择有效的目录")
        return

    suffixes = [s.strip() for s in self.type_entry.get().split(',') if s.strip()]
    if not suffixes:
        messagebox.showerror("错误", "请输入有效的文件类型")
        return

    self.upload_btn.config(state=tk.DISABLED)
    self.log_callback("开始上传...")
    
    # 创建上传器实例
    uploader = FileUploader(self.current_config)
    
    # 执行上传
    results = uploader.upload_images(dir_path, suffixes, self.log_callback)
    
    if results:
        success_count = len([r for r in results if r['result']])
        self.log_callback(f"上传完成。成功: {success_count}, 失败: {len(results) - success_count}")
        
        # 保存上传结果供导出使用
        self.upload_results = results
    else:
        self.log_callback("没有找到匹配的文件进行上传")
    
    self.upload_btn.config(state=tk.NORMAL)

4. 导出文件

4.1 实现分析

  1. 检查是否存在上传结果(upload_results),不存在则弹出警告提示并返回。
  2. 验证是否已选择上传配置(current_config),未选择则弹出错误提示并返回。
  3. 获取用户选择的目录路径并验证其有效性,无效则弹出错误提示并返回。
  4. 获取用户输入的前缀:
    • 如果有输入前缀,则设置到当前配置中
    • 如果没有输入前缀且当前配置中有前缀属性,则删除该属性
  5. 创建 FileUploader 实例并调用其 write_results_to_icon_js 方法将上传结果写入 icon.js 文件。
  6. 根据导出结果弹出相应的提示信息:
    • 导出成功则显示成功消息
    • 导出失败则显示错误消息

4.2 实现代码

def export_icon_js(self):
    """导出icon.js文件"""
    if not hasattr(self, 'upload_results'):
        messagebox.showwarning("警告", "请先上传文件")
        return
        
    if not self.current_config:
        messagebox.showerror("错误", "请先选择一个配置")
        return
        
    # 获取用户选择的目录
    dir_path = self.dir_entry.get()
    if not dir_path or not os.path.exists(dir_path):
        messagebox.showerror("错误", "请选择有效的目录")
        return
        
    # 设置前缀
    prefix = self.prefix_entry.get().strip()
    if prefix:
        self.current_config.prefix = prefix
    else:
        # 如果没有输入前缀,确保不使用配置中的前缀
        if hasattr(self.current_config, 'prefix'):
            delattr(self.current_config, 'prefix')
        
    uploader = FileUploader(self.current_config)
    if uploader.write_results_to_icon_js(self.upload_results, dir_path):
        messagebox.showinfo("成功", "icon.js文件导出成功")
    else:
        messagebox.showerror("错误", "icon.js文件导出失败")

5. 复制文件和预览文件

5.1 实现分析

  1. 检查是否存在上传结果(upload_results),不存在则弹出警告提示并返回。
  2. 验证是否已选择上传配置(current_config),未选择则弹出错误提示并返回。
  3. 获取用户选择的目录路径并验证其有效性,无效则弹出错误提示并返回。
  4. 获取用户输入的前缀:
    • 如果有输入前缀,则设置到当前配置中
    • 如果没有输入前缀且当前配置中有前缀属性,则删除该属性
  5. 创建 FileUploader 实例用于处理数据格式转换。
  6. 遍历上传结果,为每个成功的上传项:
    • 使用文件名生成 key 值
    • 根据配置的前缀生成完整的 key 名称(采用驼峰命名规则)
    • 获取上传结果中的内容字段值
    • 结合图片路径前缀生成最终值
    • 将 key-value 对添加到 icon_data 字典中
  7. 根据 icon_data 字典生成符合 JavaScript 模块格式的文件内容字符串。
  8. 清空剪贴板并添加生成的内容,然后更新剪贴板。
  9. 弹出成功提示信息。

5.2 实现代码

def copy_to_clipboard(self):
    """复制icon.js内容到剪贴板"""
    if not hasattr(self, 'upload_results'):
        messagebox.showwarning("警告", "请先上传文件")
        return
        
    if not self.current_config:
        messagebox.showerror("错误", "请先选择一个配置")
        return
        
    # 获取用户选择的目录
    dir_path = self.dir_entry.get()
    if not dir_path or not os.path.exists(dir_path):
        messagebox.showerror("错误", "请选择有效的目录")
        return
        
    # 设置前缀
    prefix = self.prefix_entry.get().strip()
    if prefix:
        self.current_config.prefix = prefix
    else:
        # 如果没有输入前缀,确保不使用配置中的前缀
        if hasattr(self.current_config, 'prefix'):
            delattr(self.current_config, 'prefix')
            
    # 生成icon.js内容但不写入文件
    uploader = FileUploader(self.current_config)
    icon_data = {}
    
    # 构建 icon_data 对象
    for item in self.upload_results:
        if item["result"].get("code") == self.current_config.success_code:
            # 使用原文件名生成key
            file_name = os.path.basename(item["fileName"])
            file_name_without_ext = os.path.splitext(file_name)[0]
            
            # 生成key
            key = f"{file_name_without_ext}Icon"
            
            # 添加前缀(如果配置了前缀)并转换为驼峰命名
            prefix = getattr(self.current_config, 'prefix', '')
            if prefix:
                # 将前缀和key转换为驼峰命名
                prefix = uploader.to_camel_case(prefix)
                key = uploader.to_camel_case(key)
                key = f"{prefix}{key}"
            
            # 获取原始值
            original_value = item["result"].get(self.current_config.content_field)
            
            # 获取图片路径前缀并拼接
            img_prefix = self.img_prefix_entry.get() if hasattr(self, 'img_prefix_entry') else ''
            if img_prefix and original_value:
                icon_data[key] = img_prefix + original_value
            else:
                icon_data[key] = original_value
    
    # 生成文件内容
    content = "export default {\n"
    for key, value in icon_data.items():
        content += f'  "{key}": "{value}",\n'
    content += "}\n"
    
    # 复制到剪贴板
    self.root.clipboard_clear()
    self.root.clipboard_append(content)
    self.root.update()
    messagebox.showinfo("成功", "icon.js内容已复制到剪贴板")

5.3 预览实现[创建预览窗口]

  1. 首先创建一个名为"icon.js 预览"的顶级窗口(Toplevel),并设置窗口标题和尺寸为600x400。
  2. 在窗口中创建一个框架(Frame),用于容纳文本框和滚动条组件。
  3. 创建一个文本框(Text widget)用于显示内容,并将传入的content参数插入到文本框中。
  4. 添加垂直和水平滚动条,并与文本框进行关联,使用户可以滚动查看内容。
  5. 使用网格布局(grid)管理文本框和滚动条的位置,设置适当的权重使组件能够随窗口大小变化而自适应调整。
  6. 在窗口底部添加一个按钮框架,并在其中放置一个"关闭"按钮,点击后可销毁预览窗口。
# 创建预览窗口
preview_window = tk.Toplevel(self.root)
preview_window.title("icon.js 预览")
preview_window.geometry("600x400")

# 创建文本框和滚动条
text_frame = ttk.Frame(preview_window)
text_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

text_widget = tk.Text(text_frame, wrap=tk.NONE)
text_widget.insert(tk.END, content)

# 添加滚动条
v_scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=text_widget.yview)
h_scrollbar = ttk.Scrollbar(text_frame, orient=tk.HORIZONTAL, command=text_widget.xview)
text_widget.configure(yscrollcommand=v_scrollbar.set, xscrollcommand=h_scrollbar.set)

# 布局
text_widget.grid(row=0, column=0, sticky="nsew")
v_scrollbar.grid(row=0, column=1, sticky="ns")
h_scrollbar.grid(row=1, column=0, sticky="ew")

text_frame.grid_rowconfigure(0, weight=1)
text_frame.grid_columnconfigure(0, weight=1)

# 添加关闭按钮
button_frame = ttk.Frame(preview_window)
button_frame.pack(fill=tk.X, padx=10, pady=5)

close_btn = ttk.Button(button_frame, text="关闭", command=preview_window.destroy)
close_btn.pack(side=tk.RIGHT)

5.4 预览效果

输入图片说明

6. 日志的显示和清空

6.1 代码实现

  1. log_callback(self, message) 函数:这是一个日志回调函数,用于在GUI界面中显示日志信息。

    • 将传入的消息(message)添加到日志文本框(self.log_text)的末尾,并在消息后添加换行符
    • 自动滚动日志文本框,确保最新添加的日志信息可见(see(tk.END))
    • 更新界面的空闲任务,确保日志立即显示到界面上(update_idletasks())
  2. clear_log(self) 函数:这是一个清空日志的函数。

    • 清除日志文本框(self.log_text)中的所有内容,从第一行第一个字符开始删除到文本框末尾
def log_callback(self, message):
    """日志回调函数"""
    self.log_text.insert(tk.END, message + "\n")
    self.log_text.see(tk.END)
    self.root.update_idletasks()

def clear_log(self):
    """清空日志"""
    self.log_text.delete(1.0, tk.END)

6.2 实现效果

输入图片说明

7. 添加文件名前缀

7.1 源文件名

输入图片说明

7.2 添加文件名前缀

输入图片说明 输入图片说明

7.3 说明

可以看到统一在图片的名称前边加了一个前缀,这样就可以根据不同模块的图片,将图片分类,即便不同模块图片命名相同,也不会发生冲突。

8. 图片路径前缀

8.1 添加前缀

输入图片说明

8.2 添加后效果

输入图片说明

8.3 说明

这里是方便有些接口只返回图片在服务器中的id,需要将返回参数作为一个参数读取图片的处理,就可以统一给图片这样添加域名。

9. 总结

  1. 上传的基本功能就实现了,没有太多的难点,就是按照步骤依次实现就好。
  2. 其实还有一些功能可以完善,比如导出的文件名可以进行修改,比如导出的文件格式可以进行选择等等。
  3. 就像第一个版本,可以看到那会没有预览和复制,也没有文件前缀和图片前缀,这些都是在使用过程中,发现不是很方便,为了更加方便快捷,完善了对应的功能,后期如果发现还需要更多功能,就能在此基础上不断的完善。就像《黑客与画家》所讲,我们先不管产品有什么问题,需要先做出来使用,在使用的过程中不断的发现问题,修改问题,完善工具,这样最后就能成为符合我们要求的产品。