You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在WTForms FieldList中使用FileField?基于Flask-WTF实现文件上传

给FieldList动态表单加文件上传功能

下面一步步调整代码,实现动态文件字段的验证、上传和数据库存储:

1. 修改表单类

先创建包含文件字段的子表单,再用FieldList嵌套它,同时加上文件验证规则:

from flask_wtf import FlaskForm
from wtforms import StringField, SelectField, FieldList, FormField, SubmitField
from flask_wtf.file import FileField, FileRequired, FileAllowed

# 子表单:单个文件条目
class FileEntryForm(FlaskForm):
    file = FileField('上传文件', validators=[
        FileRequired(message='必须选择文件'),
        FileAllowed(['jpg', 'png', 'pdf'], '只支持图片和PDF')
    ])
    description = StringField('文件说明')

# 主表单:集成动态文件字段
class DynamicForm(FlaskForm):
    # 保留你原来的StringField、SelectField等字段
    files = FieldList(FormField(FileEntryForm), min_entries=1)  # 默认至少一个文件字段
    submit = SubmitField('提交')

2. 更新模板宏和表单渲染

渲染字段的宏要区分FileField和普通字段,且主表单必须加enctype="multipart/form-data"才能传递文件:

字段渲染宏

{% macro render_field(field) %}
    <div class="form-group mb-3">
        {{ field.label(class="form-label") }}
        {% if field.type == 'FileField' %}
            {{ field(class="form-control-file") }}
        {% else %}
            {{ field(class="form-control") }}
        {% endif %}
        {% for error in field.errors %}
            <small class="text-danger d-block mt-1">{{ error }}</small>
        {% endfor %}
    </div>
{% endmacro %}

{% macro render_file_entry(entry) %}
    <div class="form-entry border p-3 mb-3">
        {{ render_field(entry.file) }}
        {{ render_field(entry.description) }}
        <button type="button" class="btn btn-sm btn-danger remove-entry">删除</button>
    </div>
{% endmacro %}

主表单模板

<form method="POST" action="" enctype="multipart/form-data">
    {{ form.hidden_tag() }}
    
    <!-- 渲染你原来的其他字段 -->
    
    <!-- 动态文件字段区域 -->
    <div id="file-entries">
        {% for entry in form.files %}
            {{ render_file_entry(entry) }}
        {% endfor %}
    </div>
    
    <button type="button" id="add-file-btn" class="btn btn-primary mb-3">添加文件</button>
    {{ form.submit(class="btn btn-success") }}
</form>

3. 调整JS动态添加逻辑

动态生成的文件字段必须符合WTForms的命名规则(比如files-0-file、files-1-file),还要处理删除后的索引重排,避免后端解析出错:

// 添加文件字段按钮事件
document.getElementById('add-file-btn').addEventListener('click', function() {
    const container = document.getElementById('file-entries');
    const entryCount = container.children.length;
    
    // 生成符合命名规则的新条目
    const newEntry = document.createElement('div');
    newEntry.className = 'form-entry border p-3 mb-3';
    newEntry.innerHTML = `
        <div class="form-group mb-3">
            <label class="form-label" for="files-${entryCount}-file">上传文件</label>
            <input type="file" class="form-control-file" id="files-${entryCount}-file" name="files-${entryCount}-file" required>
        </div>
        <div class="form-group mb-3">
            <label class="form-label" for="files-${entryCount}-description">文件说明</label>
            <input type="text" class="form-control" id="files-${entryCount}-description" name="files-${entryCount}-description">
        </div>
        <button type="button" class="btn btn-sm btn-danger remove-entry">删除</button>
    `;
    
    container.appendChild(newEntry);
    
    // 绑定删除按钮事件
    newEntry.querySelector('.remove-entry').addEventListener('click', function() {
        container.removeChild(newEntry);
        reindexEntries(); // 删除后重排索引
    });
});

// 重排表单条目索引,确保name属性连续
function reindexEntries() {
    const entries = document.querySelectorAll('.form-entry');
    entries.forEach((entry, index) => {
        const fileInput = entry.querySelector('[name^="files-"]');
        const descInput = entry.querySelector('[name$="-description"]');
        
        if (fileInput) {
            fileInput.name = `files-${index}-file`;
            fileInput.id = `files-${index}-file`;
        }
        if (descInput) {
            descInput.name = `files-${index}-description`;
            descInput.id = `files-${index}-description`;
        }
    });
}

// 初始化已有删除按钮的事件
document.querySelectorAll('.remove-entry').forEach(btn => {
    btn.addEventListener('click', function() {
        this.parentElement.remove();
        reindexEntries();
    });
});

4. 路由处理文件保存和数据库更新

在提交路由里,验证表单后逐个处理文件上传,再把文件路径存到SQLite:

import os
from flask import render_template, redirect, url_for
from werkzeug.utils import secure_filename
from app import app, db
from app.forms import DynamicForm
from app.models import FileRecord  # 对应你的模型

# 配置上传文件夹,确保目录存在
app.config['UPLOAD_FOLDER'] = os.path.join(app.root_path, 'uploads')
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)

@app.route('/dynamic-form', methods=['GET', 'POST'])
def dynamic_form():
    form = DynamicForm()
    if form.validate_on_submit():
        # 遍历每个动态文件条目
        for entry in form.files.data:
            file_obj = entry['file']
            desc = entry['description']
            
            # 生成安全文件名,避免重名和路径注入
            safe_filename = secure_filename(file_obj.filename)
            # 拼接保存路径
            save_path = os.path.join(app.config['UPLOAD_FOLDER'], safe_filename)
            # 保存文件到服务器
            file_obj.save(save_path)
            
            # 把文件路径和描述存到SQLite(存相对路径更灵活)
            new_record = FileRecord(
                file_path=save_path.replace(app.root_path, '', 1),
                description=desc
            )
            db.session.add(new_record)
        
        db.session.commit()
        return redirect(url_for('submit_success'))  # 跳转到成功页
    
    return render_template('dynamic_form.html', form=form)

5. 模型修改

更新你的SQLite模型,添加存储文件路径和描述的字段:

from app import db

class FileRecord(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    file_path = db.Column(db.String(255), nullable=False)  # 存文件相对路径
    description = db.Column(db.String(255))  # 可选的文件描述
    
    def __repr__(self):
        return f'<File {self.file_path}>'

关键注意事项

  • 必须给表单加enctype="multipart/form-data",否则无法传递文件。
  • 动态生成的字段name属性必须严格按{FieldList名称}-{索引}-{子字段名称}的格式,WTForms才能正确解析。
  • 用secure_filename()处理文件名,避免路径注入等安全问题。
  • 如果需要限制文件大小,可以在Flask配置里加MAX_CONTENT_LENGTH = 16 * 1024 * 1024(比如限制16MB)。

内容的提问来源于stack exchange,提问作者Artistica Digital

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 09:50:17