Nicegui 3.4.0 ui.upload 文件上传与表单提交 案例1

40 阅读8分钟

image.png

代码:

from nicegui import ui, events
import os
import time
import sqlite3

# 创建上传文件保存目录
UPLOAD_DIR = 'uploads'
if not os.path.exists(UPLOAD_DIR):
    os.makedirs(UPLOAD_DIR)

class FileUploadApp:
    def __init__(self):
        self.files = []
        self.form_data = {
            'name': '',
            'email': '',
            'phone': '',
            'message': ''
        }
        
        # 初始化SQLite数据库
        self.init_database()
        
        self.create_ui()
    
    def init_database(self):
        # 创建数据库连接
        self.conn = sqlite3.connect('form_submissions.db', check_same_thread=False)
        self.cursor = self.conn.cursor()
        
        # 创建表结构
        self.cursor.execute('''
        CREATE TABLE IF NOT EXISTS submissions (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            email TEXT NOT NULL,
            phone TEXT,
            message TEXT,
            file_names TEXT,
            created_at DATETIME DEFAULT CURRENT_TIMESTAMP
        )
        ''')
        
        self.conn.commit()
    
    def create_ui(self):
        # 设置页面标题和样式
        ui.page_title('文件上传与表单提交')
        ui.label('文件上传与表单提交').classes('text-2xl font-bold mb-4 w-full max-w-2xl mx-auto')
        
        # 创建表单
        with ui.card().classes('w-full max-w-2xl mx-auto'):
            # 姓名字段
            ui.label('姓名').classes('text-lg font-semibold')
            self.name_input = ui.input(
                placeholder='请输入您的姓名',
                on_change=lambda e: self.form_data.update({'name': e.value})
            ).classes('w-full mb-4').props('dense')
            
            # 邮箱字段
            ui.label('邮箱').classes('text-lg font-semibold')
            self.email_input = ui.input(
                placeholder='请输入您的邮箱',
                on_change=lambda e: self.form_data.update({'email': e.value})
            ).classes('w-full mb-4').props('dense')
            
            # 电话字段
            ui.label('电话').classes('text-lg font-semibold')
            self.phone_input = ui.input(
                placeholder='请输入您的电话',
                on_change=lambda e: self.form_data.update({'phone': e.value})
            ).classes('w-full mb-4').props('dense')
            
            # 留言字段
            ui.label('留言').classes('text-lg font-semibold')
            self.message_textarea = ui.textarea(
                placeholder='请输入您的留言',
                on_change=lambda e: self.form_data.update({'message': e.value})
            ).classes('w-full mb-4').props('dense')
            
            # 文件上传组件
            ui.label('文件上传').classes('text-lg font-semibold')
            self.upload = ui.upload(
                label='选择文件',
                on_upload=self.handle_upload,
                multiple=True,
                max_files=5,
                max_file_size=1024 * 1024 * 10,  # 10MB
            ).classes('w-full mb-4')
            
            # 上传文件列表显示
            self.file_list = ui.column().classes('mb-4')
            
            # 提交按钮
            ui.button(
                '提交表单',
                on_click=self.handle_submit,
                color='primary',
                icon='send'
            ).classes('w-full')
        
        # 结果显示区域
        self.result_card = ui.card().classes('w-full max-w-2xl mx-auto mt-4 hidden')
        with self.result_card:
            self.result_text = ui.label().classes('text-lg')
    
    async def handle_upload(self, event: events.UploadEventArguments):
        # 保存上传的文件
        timestamp = int(time.time())
        file_path = os.path.join(UPLOAD_DIR, f'{timestamp}_{event.file.name}')
        with open(file_path, 'wb') as f:
            f.write(await event.file.read())
        
        # 添加到文件列表
        self.files.append({
            'name': event.file.name,
            'path': file_path,
            'size': event.file.size()  # size 是方法,需要调用它
        })
        
        # 更新文件列表显示
        self.update_file_list()
    
    def update_file_list(self):
        # 清空文件列表
        self.file_list.clear()
        
        if not self.files:
            with self.file_list:
                ui.label('暂无上传文件').classes('text-gray-500')
            return
        
        # 显示已上传文件
        for i, file in enumerate(self.files):
            with self.file_list:
                with ui.row().classes('items-center justify-between w-full p-2 border-b'):
                    ui.label(f'{i+1}. {file["name"]} ({self.format_file_size(file["size"])})')
                    ui.button(
                        '删除',
                        on_click=lambda idx=i: self.remove_file(idx),
                        color='red',
                        icon='delete'
                    ).classes('text-sm py-1 px-2')
    
    def remove_file(self, index):
        # 删除文件
        file = self.files.pop(index)
        if os.path.exists(file['path']):
            os.remove(file['path'])
        
        # 更新文件列表显示
        self.update_file_list()
    
    def format_file_size(self, size_bytes):
        # 格式化文件大小
        # 检查是否是方法对象,如果是则调用它
        if callable(size_bytes):
            size_bytes = size_bytes()
        
        if size_bytes < 1024:
            return f'{size_bytes} B'
        elif size_bytes < 1024 * 1024:
            return f'{size_bytes / 1024:.2f} KB'
        else:
            return f'{size_bytes / (1024 * 1024):.2f} MB'
    
    def handle_submit(self):
        # 验证表单数据
        if not self.form_data['name']:
            ui.notify('请输入姓名', color='red')
            return
        
        if not self.form_data['email']:
            ui.notify('请输入邮箱', color='red')
            return
        
        # 检查邮箱格式
        if '@' not in self.form_data['email']:
            ui.notify('请输入有效的邮箱地址', color='red')
            return
        
        # 保存到数据库
        file_names = ', '.join([file['name'] for file in self.files])
        self.cursor.execute('''
        INSERT INTO submissions (name, email, phone, message, file_names)
        VALUES (?, ?, ?, ?, ?)
        ''', (
            self.form_data['name'],
            self.form_data['email'],
            self.form_data['phone'],
            self.form_data['message'],
            file_names
        ))
        self.conn.commit()
        
        # 显示提交结果
        self.result_text.text = f"""
        表单提交成功!\n\n
        个人信息:\n
        姓名:{self.form_data['name']}\n
        邮箱:{self.form_data['email']}\n
        电话:{self.form_data['phone']}\n
        留言:{self.form_data['message']}\n\n
        上传文件:{len(self.files)} 个\n
        {chr(10).join([f'  - {file["name"]}' for file in self.files])}\n\n
        数据已保存到数据库!
        """
        
        # 显示结果卡片
        self.result_card.classes(remove='hidden')
        
        # 滚动到结果区域
        ui.run_javascript('document.querySelector(".nicegui-content").scrollTop = document.body.scrollHeight')
        
        # 清空表单
        self.form_data = {
            'name': '',
            'email': '',
            'phone': '',
            'message': ''
        }
        
        # 重置UI组件
        self.name_input.value = ''
        self.email_input.value = ''
        self.phone_input.value = ''
        self.message_textarea.value = ''
        
        # 清空上传文件
        self.files = []
        self.update_file_list()
        
        # 重置上传组件
        self.upload.reset()

# 创建应用实例
app = FileUploadApp()

# 运行应用
ui.run(
    title='文件上传与表单提交',
    favicon='📁',
    port=8080,
    reload=True
)

测试数据是否正常:

import sqlite3

# 连接到数据库
conn = sqlite3.connect('form_submissions.db')
cursor = conn.cursor()

# 查看表结构
print("表结构:")
cursor.execute("PRAGMA table_info(submissions)")
table_info = cursor.fetchall()
for column in table_info:
    print(f"列名:{column[1]},类型:{column[2]}")

# 查询所有数据
print("\n所有数据:")
cursor.execute("SELECT * FROM submissions")
data = cursor.fetchall()

if not data:
    print("数据库中暂无数据")
else:
    for row in data:
        print(f"ID: {row[0]}")
        print(f"姓名: {row[1]}")
        print(f"邮箱: {row[2]}")
        print(f"电话: {row[3]}")
        print(f"留言: {row[4]}")
        print(f"文件: {row[5]}")
        print(f"提交时间: {row[6]}")
        print("-" * 30)

# 关闭连接
cursor.close()
conn.close()

image.png

1. 应用简介

这是一个基于 NiceGUI 3.4.0 开发的文件上传与表单提交应用,支持多文件上传、表单验证和数据持久化存储。

2. 功能特点

2.1 核心功能

  • 文件上传:支持多文件上传(最多5个文件)
  • 文件限制:单个文件最大10MB
  • 表单提交:包含姓名、邮箱、电话和留言字段
  • 表单验证:姓名和邮箱为必填项,邮箱格式验证
  • 数据存储:使用SQLite3持久化存储表单数据
  • 界面反馈:提交成功后显示结果信息
  • 自动重置:提交后自动清空表单和文件列表

2.2 用户体验

  • 响应式设计:适配不同屏幕尺寸
  • 实时反馈:上传进度和错误提示
  • 直观操作:简洁明了的用户界面

3. 技术栈

  • 后端框架:Python 3.13
  • 前端框架:NiceGUI 3.4.0
  • 数据库:SQLite3
  • 文件存储:本地文件系统

4. 安装和运行

4.1 环境要求

  • Python 3.10+
  • pip 包管理工具

4.2 安装依赖

pip install nicegui

4.3 运行应用

python up01.py

应用将在以下地址运行:

5. 使用指南

5.1 上传文件

  1. 点击"选择文件"按钮
  2. 从本地选择要上传的文件(最多5个)
  3. 文件将自动上传并显示在文件列表中
  4. 可以点击"删除"按钮移除不需要的文件

5.2 填写表单

  1. 姓名:必填项,输入您的姓名
  2. 邮箱:必填项,输入有效的邮箱地址
  3. 电话:选填项,输入您的联系电话
  4. 留言:选填项,输入您的留言内容

5.3 提交表单

  1. 确认所有必填项已填写
  2. 点击"提交表单"按钮
  3. 系统将验证表单数据
  4. 验证通过后,数据将保存到数据库
  5. 提交成功后显示结果信息
  6. 表单和文件列表将自动重置

6. 数据库说明

6.1 数据库文件

  • 文件名:form_submissions.db
  • 位置:应用程序同级目录

6.2 表结构

CREATE TABLE submissions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT NOT NULL,
    phone TEXT,
    message TEXT,
    file_names TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

6.3 字段说明

  • id:自动递增的主键
  • name:用户姓名
  • email:用户邮箱
  • phone:用户电话
  • message:用户留言
  • file_names:上传的文件名列表(逗号分隔)
  • created_at:提交时间(自动生成)

6.4 数据查询

可以使用 test_db.py 脚本查看数据库内容:

python test_db.py

7. 代码结构

7.1 主要类和方法

FileUploadApp
  • __init__():初始化应用
  • init_database():初始化数据库连接和表结构
  • create_ui():创建用户界面
  • handle_upload():处理文件上传
  • update_file_list():更新文件列表显示
  • remove_file():删除上传的文件
  • format_file_size():格式化文件大小显示
  • handle_submit():处理表单提交

7.2 目录结构

├── up01.py              # 主应用文件
├── up01_README.md       # 应用说明文档
├── form_submissions.db  # SQLite数据库文件
├── uploads/             # 上传文件存储目录
└── test_db.py           # 数据库测试脚本

8. 常见问题

8.1 上传文件失败

  • 检查文件大小是否超过10MB
  • 检查文件数量是否超过5个
  • 检查网络连接是否正常

8.2 表单提交失败

  • 确认姓名和邮箱已填写
  • 确认邮箱格式正确(包含@符号)

8.3 数据库连接错误

  • 确保应用程序有写入权限
  • 检查是否有其他进程占用数据库文件

9. 技术细节

9.1 文件上传机制

  • 使用 NiceGUI 的 ui.upload 组件
  • 支持异步文件读取
  • 文件保存到 uploads 目录,使用时间戳+原文件名命名

9.2 数据持久化

  • 使用 SQLite3 数据库存储表单数据
  • 支持多线程访问(check_same_thread=False
  • 参数化查询防止SQL注入

9.3 UI设计

  • 使用 NiceGUI 的 with 上下文管理器嵌套组件

  • 响应式布局,自适应不同屏幕尺寸

  • 组件样式通过 .classes() 方法设置

  • 实现基本文件上传功能

  • 实现表单提交和验证

  • 集成SQLite3数据存储

简化代码:

from nicegui import ui, events
import os
import sqlite3

UPLOAD_DIR = 'uploads'
os.makedirs(UPLOAD_DIR, exist_ok=True)

class FileUploadApp:
    def __init__(self):
        self.files = []
        self.init_database()
        self.create_ui()
    
    def init_database(self):
        self.conn = sqlite3.connect('form_submissions.db', check_same_thread=False)
        self.cursor = self.conn.cursor()
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS submissions (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT,
                email TEXT,
                phone TEXT,
                message TEXT,
                file_names TEXT,
                created_at DATETIME DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        self.conn.commit()
    
    def create_ui(self):
        ui.page_title('文件上传与表单提交')
        ui.label('文件上传与表单提交').classes('text-2xl font-bold mb-4 w-full max-w-2xl mx-auto')
        
        with ui.card().classes('w-full max-w-2xl mx-auto'):
            # 表单字段
            self.name = ui.input('姓名', placeholder='请输入您的姓名').classes('w-full mb-4')
            self.email = ui.input('邮箱', placeholder='请输入您的邮箱').classes('w-full mb-4')
            self.phone = ui.input('电话', placeholder='请输入您的电话').classes('w-full mb-4')
            self.message = ui.textarea('留言', placeholder='请输入您的留言').classes('w-full mb-4')
            
            # 文件上传
            ui.label('文件上传').classes('text-lg font-semibold')
            self.upload = ui.upload(
                label='选择文件',
                on_upload=self.handle_upload,
                multiple=True,
                max_files=5
            ).classes('w-full mb-4')
            
            self.file_list = ui.column().classes('mb-4')
            
            # 提交按钮
            ui.button('提交表单', on_click=self.submit, color='primary').classes('w-full')
        
        self.result = ui.label().classes('w-full max-w-2xl mx-auto mt-4 hidden')
    
    async def handle_upload(self, e: events.UploadEventArguments):
        file_path = os.path.join(UPLOAD_DIR, e.file.name)
        with open(file_path, 'wb') as f:
            f.write(await e.file.read())
        
        self.files.append({
            'name': e.file.name,
            'path': file_path
        })
        self.update_file_list()
    
    def update_file_list(self):
        self.file_list.clear()
        if not self.files:
            with self.file_list:
                ui.label('暂无上传文件').classes('text-gray-500')
            return
        
        for i, file in enumerate(self.files):
            with self.file_list:
                with ui.row().classes('items-center justify-between w-full p-2 border-b'):
                    ui.label(f'{i+1}. {file["name"]}')
                    ui.button('删除', on_click=lambda idx=i: self.remove_file(idx), color='red', icon='delete')
    
    def remove_file(self, index):
        file = self.files.pop(index)
        if os.path.exists(file['path']):
            os.remove(file['path'])
        self.update_file_list()
    
    def submit(self):
        # 简单验证
        if not self.name.value or not self.email.value or '@' not in self.email.value:
            ui.notify('请填写正确的姓名和邮箱', color='red')
            return
        
        # 保存到数据库
        file_names = ', '.join([f['name'] for f in self.files])
        self.cursor.execute('''
            INSERT INTO submissions (name, email, phone, message, file_names)
            VALUES (?, ?, ?, ?, ?)
        ''', (self.name.value, self.email.value, self.phone.value, self.message.value, file_names))
        self.conn.commit()
        
        # 显示结果
        self.result.text = f"""
        提交成功!
        姓名:{self.name.value}
        邮箱:{self.email.value}
        文件:{file_names or '无'}
        """
        self.result.classes(remove='hidden')
        
        # 清空表单
        self.name.value = self.email.value = self.phone.value = self.message.value = ''
        self.files.clear()
        self.upload.reset()
        self.update_file_list()

app = FileUploadApp()
ui.run(title='文件上传与表单提交', port=8080)