NiceGUI 3.4.0 中 ui.upload 文件上传功能的几个实用例子

42 阅读7分钟

image.png

基础文件上传例子

from nicegui import ui, events
import os
import aiofiles

# 创建上传目录
UPLOAD_DIR = "uploaded_files"
os.makedirs(UPLOAD_DIR, exist_ok=True)

# 基础文件上传处理器
async def handle_upload(e: events.UploadEventArguments):
    """处理上传的文件"""
    file = e.file  # 获取文件对象
    
    # 获取文件信息
    filename = file.name
    content_type = file.content_type
    file_size = file.size()
    
    ui.notify(f'📤 正在上传: {filename} ({file_size} bytes)')
    
    # 保存文件
    save_path = os.path.join(UPLOAD_DIR, filename)
    
    try:
        # 使用异步方式保存文件
        await file.save(save_path)
        ui.notify(f'✅ 文件 {filename} 上传成功!', color='positive')
        
        # 显示文件信息
        update_file_info(filename, content_type, file_size, save_path)
        
    except Exception as ex:
        ui.notify(f'❌ 上传失败: {str(ex)}', color='negative')

def update_file_info(filename, content_type, size, path):
    """更新文件信息显示"""
    info_container.clear()
    with info_container:
        ui.label('📋 文件信息').classes('text-lg font-bold mb-2')
        with ui.card().classes('w-full'):
            ui.label(f'文件名: {filename}')
            ui.label(f'类型: {content_type}')
            ui.label(f'大小: {size} bytes ({size/1024:.2f} KB)')
            ui.label(f'保存路径: {path}')

# 创建界面
ui.label('📁 NiceGUI 文件上传示例').classes('text-2xl font-bold mb-4')

# 文件信息容器
info_container = ui.column().classes('w-full mb-4')

# 基础上传组件
ui.upload(
    on_upload=handle_upload,
    label='选择文件或拖拽到此处',
    multiple=False,
    auto_upload=True,  # 选择文件后自动上传
).classes('max-w-full')

ui.run(title='文件上传示例')

image.png

增强版:多文件上传与类型处理

from nicegui import ui, events
import os
import json
import pandas as pd
import io
from pathlib import Path

# 上传配置
UPLOAD_DIR = Path("uploaded_files")
UPLOAD_DIR.mkdir(exist_ok=True)

# 文件处理函数
async def handle_file_upload(e: events.UploadEventArguments):
    """增强版文件上传处理器"""
    file = e.file
    
    # 显示基本信息
    info_area.clear()
    with info_area:
        ui.label('📋 文件信息').classes('text-xl font-bold mb-3')
        
        with ui.card().classes('w-full mb-4'):
            ui.label(f'文件名: {file.name}').classes('font-bold')
            ui.label(f'类型: {file.content_type or "未知"}')
            ui.label(f'大小: {file.size()} 字节 ({file.size()/1024/1024:.2f} MB)')
        
        # 根据文件类型处理内容
        await process_file_by_type(file)

async def process_file_by_type(file):
    """根据文件类型处理内容"""
    content_type = file.content_type or ''
    
    try:
        if content_type == 'application/json':
            await handle_json_file(file)
        elif content_type.startswith('text/'):
            await handle_text_file(file)
        elif content_type.startswith('image/'):
            await handle_image_file(file)
        elif content_type == 'text/csv' or file.name.endswith('.csv'):
            await handle_csv_file(file)
        else:
            await handle_binary_file(file)
            
    except Exception as ex:
        ui.notify(f'处理文件时出错: {str(ex)}', color='negative')

async def handle_json_file(file):
    """处理JSON文件"""
    with info_area:
        ui.label('🎯 JSON文件内容:').classes('text-lg font-bold mb-2')
        try:
            json_data = await file.json()
            ui.json(json_data)
            
            # 保存文件
            save_path = UPLOAD_DIR / file.name
            await file.save(save_path)
            ui.notify('JSON文件保存成功!', color='positive')
            
        except Exception as ex:
            ui.notify(f'JSON解析失败: {str(ex)}', color='negative')

async def handle_text_file(file):
    """处理文本文件"""
    with info_area:
        ui.label('📝 文本文件内容:').classes('text-lg font-bold mb-2')
        try:
            text_content = await file.text()
            
            # 显示内容预览
            with ui.card().classes('w-full'):
                ui.label('内容预览:').classes('font-bold mb-2')
                preview_text = text_content[:1000] + '...' if len(text_content) > 1000 else text_content
                ui.label(preview_text).classes('font-mono text-sm')
            
            # 保存文件
            save_path = UPLOAD_DIR / file.name
            await file.save(save_path)
            ui.notify('文本文件保存成功!', color='positive')
            
        except Exception as ex:
            ui.notify(f'文本读取失败: {str(ex)}', color='negative')

async def handle_csv_file(file):
    """处理CSV文件"""
    with info_area:
        ui.label('📊 CSV文件内容:').classes('text-lg font-bold mb-2')
        try:
            csv_content = await file.text()
            
            # 使用pandas读取CSV
            df = pd.read_csv(io.StringIO(csv_content))
            
            ui.label(f'数据概览 - 共 {len(df)} 行,{len(df.columns)} 列')
            
            # 显示前几行数据
            with ui.card().classes('w-full'):
                ui.label('前5行数据:').classes('font-bold mb-2')
                ui.html(df.head().to_html(classes='table-auto'))
            
            # 保存文件
            save_path = UPLOAD_DIR / file.name
            await file.save(save_path)
            ui.notify('CSV文件保存成功!', color='positive')
            
        except Exception as ex:
            ui.notify(f'CSV处理失败: {str(ex)}', color='negative')

async def handle_image_file(file):
    """处理图片文件"""
    with info_area:
        ui.label('🖼️ 图片文件').classes('text-lg font-bold mb-2')
        
        # 保存图片文件
        save_path = UPLOAD_DIR / file.name
        await file.save(save_path)
        
        # 显示图片
        ui.image(str(save_path)).classes('w-full max-w-md')
        ui.notify('图片文件保存成功!', color='positive')

async def handle_binary_file(file):
    """处理二进制文件"""
    with info_area:
        ui.label('📁 二进制文件').classes('text-lg font-bold mb-2')
        
        # 保存文件
        save_path = UPLOAD_DIR / file.name
        await file.save(save_path)
        
        ui.label(f'文件已保存到: {save_path}')
        ui.notify('二进制文件保存成功!', color='positive')

def handle_rejected():
    """处理被拒绝的文件"""
    ui.notify('文件上传被拒绝(可能超过大小限制)', color='negative')

def handle_multi_upload(e: events.MultiUploadEventArguments):
    """处理多文件上传"""
    ui.notify(f'📤 准备上传 {len(e.files)} 个文件...')
    
    for file in e.files:
        ui.notify(f'正在处理: {file.name}')

# 创建界面
ui.label('🚀 增强版文件上传系统').classes('text-2xl font-bold mb-4')

# 文件信息展示区域
info_area = ui.column().classes('w-full mb-6')

# 增强版上传组件
with ui.card().classes('w-full mb-6'):
    ui.label('📤 文件上传').classes('text-lg font-bold mb-3')
    
    ui.upload(
        on_upload=handle_file_upload,
        on_multi_upload=handle_multi_upload,
        on_rejected=handle_rejected,
        multiple=True,  # 允许多文件上传
        auto_upload=False,  # 手动点击上传
        max_file_size=50_000_000,  # 50MB 限制
        max_files=10,  # 最多10个文件
        label='选择文件或拖拽到此处',
    ).classes('max-w-full').props('accept=*')

# 文件类型说明
with ui.expansion('📖 支持的文件类型').classes('w-full'):
    ui.markdown('''
    ### 支持的文件类型:
    - **JSON文件** (.json): 自动解析并显示结构
    - **文本文件** (.txt, .md, .py等): 显示内容预览
    - **CSV文件** (.csv): 使用pandas显示数据表格
    - **图片文件** (.jpg, .png, .gif等): 显示图片预览
    - **其他文件**: 保存为二进制文件
    
    ### 限制:
    - 单个文件最大 50MB
    - 最多同时上传 10 个文件
    ''')

ui.run(title='增强版文件上传系统')

实用版:带文件管理的上传系统

from nicegui import ui, events
import os
import shutil
from datetime import datetime
from pathlib import Path
import asyncio
from starlette.responses import FileResponse

class FileManager:
    def __init__(self):
        self.upload_dir = Path("uploaded_files")
        self.upload_dir.mkdir(exist_ok=True)
        
        self.setup_ui()
        self.refresh_file_list()
        self.setup_download_route()
    
    def setup_download_route(self):
        """设置下载路由"""
        from nicegui import app
        
        @app.get('/download/{filename}')
        async def download_file_route(filename: str):
            file_path = self.upload_dir / filename
            if file_path.exists() and file_path.is_file():
                return FileResponse(
                    path=str(file_path),
                    filename=filename,
                    media_type='application/octet-stream'
                )
            else:
                return {"error": "文件不存在"}
    
    def setup_ui(self):
        """设置界面"""
        ui.label('📁 文件管理系统').classes('text-2xl font-bold mb-4')
        
        # 上传区域
        with ui.card().classes('w-full mb-6'):
            ui.label('📤 文件上传').classes('text-lg font-bold mb-3')
            
            self.upload_component = ui.upload(
                on_upload=self.handle_upload,
                on_rejected=self.handle_rejected,
                multiple=True,
                auto_upload=True,
                max_file_size=100_000_000,  # 100MB
            ).classes('max-w-full')
            
            # 上传统计
            self.upload_stats = ui.label('准备就绪').classes('text-sm text-gray-600 mt-2')
        
        # 文件列表区域
        with ui.card().classes('w-full'):
            ui.label('📋 文件列表').classes('text-lg font-bold mb-3')
            
            # 操作按钮
            with ui.row().classes('mb-3'):
                ui.button('🔄 刷新', on_click=self.refresh_file_list).props('size=sm')
                ui.button('🗑️ 清空所有', on_click=self.clear_all_files).props('size=sm color=negative')
                self.total_size_label = ui.label('').classes('ml-auto')
            
            # 文件列表容器
            self.file_list_container = ui.column().classes('w-full')
    
    async def handle_upload(self, e: events.UploadEventArguments):
        """处理文件上传"""
        file = e.file
        
        # 生成唯一文件名(避免重名)
        original_name = file.name
        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        base_name = Path(original_name).stem
        extension = Path(original_name).suffix
        unique_name = f"{base_name}_{timestamp}{extension}"
        
        save_path = self.upload_dir / unique_name
        
        try:
            # 保存文件
            await file.save(save_path)
            
            # 更新统计
            file_size = file.size()
            self.upload_stats.text = f'✅ 已上传: {original_name} ({file_size} bytes)'
            
            # 刷新文件列表
            self.refresh_file_list()
            
            ui.notify(f'文件 {original_name} 上传成功!', color='positive')
            
        except Exception as ex:
            ui.notify(f'上传失败: {str(ex)}', color='negative')
    
    def handle_rejected(self):
        """处理被拒绝的文件"""
        ui.notify('文件上传被拒绝(可能超过大小限制)', color='negative')
    
    def refresh_file_list(self):
        """刷新文件列表"""
        self.file_list_container.clear()
        
        files = []
        total_size = 0
        
        # 获取所有文件
        for file_path in self.upload_dir.iterdir():
            if file_path.is_file():
                stat = file_path.stat()
                files.append({
                    'name': file_path.name,
                    'size': stat.st_size,
                    'modified': datetime.fromtimestamp(stat.st_mtime),
                    'path': file_path
                })
                total_size += stat.st_size
        
        # 更新总大小显示
        self.total_size_label.text = f'总大小: {total_size / 1024 / 1024:.2f} MB'
        
        if not files:
            with self.file_list_container:
                ui.label('暂无文件').classes('text-gray-500 text-center py-8')
            return
        
        # 按修改时间排序
        files.sort(key=lambda x: x['modified'], reverse=True)
        
        # 显示文件列表
        with self.file_list_container:
            for file_info in files:
                self.create_file_item(file_info)
    
    def create_file_item(self, file_info):
        """创建文件项"""
        with ui.card().classes('w-full mb-2'):
            with ui.row().classes('items-center justify-between w-full'):
                # 文件信息
                with ui.row().classes('items-center'):
                    # 文件图标
                    ui.icon('insert_drive_file').classes('text-blue-500 mr-2')
                    
                    # 文件名称和大小
                    with ui.column().classes('mr-4'):
                        ui.label(file_info['name']).classes('font-bold')
                        size_mb = file_info['size'] / 1024 / 1024
                        ui.label(f'{size_mb:.2f} MB').classes('text-sm text-gray-600')
                    
                    # 修改时间
                    ui.label(file_info['modified'].strftime('%Y-%m-%d %H:%M')).classes(
                        'text-sm text-gray-500'
                    )
                
                # 操作按钮
                with ui.row().classes('gap-1'):
                    ui.button(
                        '',
                        icon='download',
                        on_click=lambda p=file_info['path']: self.download_file(p)
                    ).props('size=sm flat color=primary')
                    
                    ui.button(
                        '',
                        icon='delete',
                        on_click=lambda p=file_info['path']: self.delete_file(p)
                    ).props('size=sm flat color=negative')
    
    def download_file(self, file_path: Path):
        """下载文件"""
        try:
            # 使用JavaScript触发下载
            filename = file_path.name
            download_url = f'/download/{filename}'
            
            ui.run_javascript(f'''
                const link = document.createElement('a');
                link.href = '{download_url}';
                link.download = '{filename}';
                link.style.display = 'none';
                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);
            ''')
            
        except Exception as ex:
            ui.notify(f'下载失败: {str(ex)}', color='negative')
    
    def delete_file(self, file_path: Path):
        """删除文件"""
        def confirm_delete():
            try:
                file_path.unlink()  # 删除文件
                self.refresh_file_list()
                ui.notify(f'文件 {file_path.name} 已删除', color='positive')
            except Exception as ex:
                ui.notify(f'删除失败: {str(ex)}', color='negative')
        
        # 确认对话框
        with ui.dialog() as dialog, ui.card():
            ui.label(f'确定要删除文件 "{file_path.name}" 吗?')
            with ui.row():
                ui.button('确定', on_click=lambda: [confirm_delete(), dialog.close()])
                ui.button('取消', on_click=dialog.close)
        
        dialog.open()
    
    def clear_all_files(self):
        """清空所有文件"""
        def confirm_clear():
            try:
                # 删除upload目录下的所有文件
                for file_path in self.upload_dir.iterdir():
                    if file_path.is_file():
                        file_path.unlink()
                
                self.refresh_file_list()
                ui.notify('所有文件已清空', color='positive')
            except Exception as ex:
                ui.notify(f'清空失败: {str(ex)}', color='negative')
        
        # 确认对话框
        with ui.dialog() as dialog, ui.card():
            ui.label('确定要清空所有文件吗?此操作不可恢复。')
            with ui.row():
                ui.button('确定', on_click=lambda: [confirm_clear(), dialog.close()])
                ui.button('取消', on_click=dialog.close)
        
        dialog.open()

# 创建应用
FileManager()
ui.run(title='文件管理系统')

关键要点说明

  1. 基础用法

    ui.upload(on_upload=handle_upload, auto_upload=True)
    
  2. 文件事件处理

    async def handle_upload(e: events.UploadEventArguments):
        file = e.file  # 获取文件对象
        filename = file.name
        content = await file.text()  # 或 read(), json(), save(path)
    
  3. 多文件上传

    ui.upload(
        on_upload=single_file_handler,
        on_multi_upload=multi_file_handler,
        multiple=True
    )
    
  4. 文件限制

    ui.upload(
        max_file_size=10_000_000,  # 10MB
        max_files=5,  # 最多5个文件
        accepted_file_types='.json,.txt,.csv'  # 接受的文件类型
    )
    

这些例子展示了 NiceGUI 3.4.0 中 ui.upload 的完整用法,包括基础上传、文件类型处理、多文件上传和文件管理等功能。