在SSM+vue项目中上传表单数据和文件

发布于:2025-07-10 ⋅ 阅读:(21) ⋅ 点赞:(0)

从前端向后端发送multipart/form-data 类型数据(主要用于文件上传或表单提交)

如发送如下信息:

前端代码vue文件:(配置了服务器代理)

<template>
  <div class="content">
    <el-form :model="form" ref="form" :rules="rules"  label-width="100px" class="demo-form">
      <div class="infoAdd">
        <div class="title">申请信息</div>
        <el-form-item label="申请人" prop="proposer_Name">
          <el-input v-model="form.proposer_Name" placeholder="请输入申请人姓名"></el-input>
        </el-form-item>
        
        <el-form-item label="联系电话" prop="telephone">
          <el-input v-model="form.telephone" placeholder="请输入手机号码"></el-input>
        </el-form-item>
        
        <el-form-item label="身份证号" prop="cardId">
          <el-input v-model="form.cardId" placeholder="请输入身份证号"></el-input>
        </el-form-item>
        
        <el-form-item label="营业执照" prop="charterName">
          <el-input v-model="form.charterName" placeholder="请输入营业执照(名称)"></el-input>
        </el-form-item>
        
        <el-form-item label="所在区域" prop="location">
          <el-cascader
            v-model="form.location"
            :options="areaOptions"
            :props="{ 
                value: 'name',    // 绑定选项的value字段
                label: 'name',    // 绑定选项的label字段
                children: 'children', // 绑定子级字段
                expandTrigger: 'hover' }"
            clearable
            @change="handleAreaChange"
            placeholder="请选择所在区域"
            style="width: 100%;"
          ></el-cascader>
        </el-form-item>
        
        <el-form-item label="详细地址" prop="address">
          <el-input v-model="form.address" placeholder="请输入详细地址"></el-input>
        </el-form-item>
        
        <el-form-item label="踏勘时间" prop="expDate">
          <el-date-picker
            v-model="form.expDate"
            type="date"
            placeholder="请选择踏勘时间"
            value-format="yyyy-MM-dd"
            style="width: 100%;"
          ></el-date-picker>
        </el-form-item>
        
        <el-form-item label="申请说明" prop="applyState">
          <el-input
            type="textarea"
            v-model="form.applyState"
            placeholder="请输入详细的申请说明"
            :rows="4"
          ></el-input>
        </el-form-item>
      </div>
      
      <div class="infoAdd">
    
        <div class="title" style="margin-top: 20px;">已上传资料</div>
        <div class="items">
          <!-- 上传的图片 -->
          <div class="itemAdd" v-for="(img, index) in uploadedImages" :key="'img-'+index">
            <img :src="img.preview" alt="上传图片">
            <div class="cancel" @click="removeUploadedItem('image', index)">×</div>
          </div>
          
          <!-- 上传的视频 -->
          <div class="itemAdd" v-for="(video, index) in uploadedVideos" :key="'video-'+index">
            <video controls width="100" height="80">
              <source :src="video.preview" :type="video.type">
              您的浏览器不支持视频预览
            </video>
            <div class="cancel" @click="removeUploadedItem('video', index)">×</div>
          </div>
          
          <!-- 上传的文件 -->
          <div class="itemAdd file-item" v-for="(file, index) in uploadedDocs" :key="'file-'+index">
            <div class="file-icon">
              <i class="el-icon-document"></i>
            </div>
            <div class="file-name">{{ truncateFileName(file.name) }}</div>
            <div class="cancel" @click="removeUploadedItem('doc', index)">×</div>
          </div>
        </div>
      </div>
      
      <div class="infoAdd">
        <div class="title">上传资料</div>
        <div class="upload-container">
          <div class="upload-item" @click="triggerUpload('img')">
            <div class="upload-icon">+</div>
            <div class="upload-text">图片</div>
            <input type="file" ref="img" @change="handleImageUpload" hidden accept="image/*" multiple>
          </div>
          <div class="upload-item" @click="triggerUpload('video')">
            <div class="upload-icon">+</div>
            <div class="upload-text">录像</div>
            <input type="file" ref="video" @change="handleVideoUpload" hidden accept="video/*" multiple>
          </div>
          <div class="upload-item" @click="triggerUpload('file')">
            <div class="upload-icon">+</div>
            <div class="upload-text">文件</div>
            <input type="file" ref="file" @change="handleFileUpload" hidden multiple>
          </div>
        </div>
      </div>
      
      <div class="sumBtn">
        <el-button type="primary" @click="submitForm('form')" size="large">提交</el-button>
      </div>
    </el-form>
  </div>
</template>

<script>
import chinaRegionData from '@/assets/data/china-region.json';
export default {  
  name: 'addInfo',
    data() {
        // 手机号校验函数
    const validatePhone = (rule, value, callback) => {
        if (!value) {
        return callback(new Error('请输入联系电话'));
        }
        if (!/^1[3-9]\d{9}$/.test(value)) {
        return callback(new Error('请输入正确的手机号码'));
        }
        callback();
    };

    // 身份证校验函数
    const validateIdCard = (rule, value, callback) => {
        if (!value) {
        return callback(new Error('请输入身份证号'));
        }
        // 简单校验18位(实际项目应使用更严格校验)
        if (!/(^\d{15}$)|(^\d{17}(\d|X|x)$)/.test(value)) {
        return callback(new Error('请输入有效的身份证号'));
        }
        callback();
    };
    return {
      form: {
        proposer_Name: '',
        telephone: '',
        cardId: '',
        charterName: '',
        location: [],
        address: '',
        expDate: '',
        applyState: ''
      },
      areaOptions: chinaRegionData,
      uploadedImages: [], // 上传的图片
      uploadedVideos: [], // 上传的视频
      uploadedDocs: []  , // 上传的文件
      rules: {
        proposer_Name: [
            { required: true, message: '请输入申请人姓名', trigger: 'blur' },
            { min: 2, max: 10, message: '长度在2到10个字符', trigger: 'blur' }
        ],
        telephone: [
            { required: true, validator: validatePhone, trigger: 'blur' }
        ],
        cardId: [
            { required: true, validator: validateIdCard, trigger: 'blur' }
        ],
        charterName: [
            { required: true, message: '请输入营业执照名称', trigger: 'blur' }
        ],
        location: [
            { type: 'array', required: true, message: '请选择所在区域', trigger: 'change' }
        ],
        address: [
            { required: true, message: '请输入详细地址', trigger: 'blur' },
            { min: 5, max: 100, message: '长度在5到100个字符', trigger: 'blur' }
        ],
        expDate: [
            { required: true, message: '请选择踏勘时间', trigger: 'change' }
        ],
        applyState: [
            { required: true, message: '请输入申请说明', trigger: 'blur' },
            { min: 10, message: '至少输入10个字符', trigger: 'blur' }
        ]
      },
    }
  },
  methods: {
    handleAreaChange(value) {
      console.log('Area changed:', value);
    },
    triggerUpload(type) {
      this.$refs[type].click();
    },
    handleImageUpload(event) {
      const files = event.target.files;
      if (!files || files.length === 0) return;
      
      Array.from(files).forEach(file => {
        if (!file.type.match('image.*')) {
          this.$message.error(`${file.name} 不是有效的图片文件`);
          return;
        }
        
        const reader = new FileReader();
        reader.onload = (e) => {
          this.uploadedImages.push({
            file: file,
            preview: e.target.result,
            type: file.type
          });
        };
        reader.readAsDataURL(file);
      });
       console.log("uploadedImages:"+this.uploadedImages)
    },
    handleVideoUpload(event) {
      const files = event.target.files;
      if (!files || files.length === 0) return;
      
      Array.from(files).forEach(file => {
        if (!file.type.match('video.*')) {
          this.$message.error(`${file.name} 不是有效的视频文件`);
          return;
        }
        
        const reader = new FileReader();
        reader.onload = (e) => {
          this.uploadedVideos.push({
            file: file,
            preview: e.target.result,
            type: file.type
          });
        };
        reader.readAsDataURL(file);
      });
      console.log("uploadedVideos:"+this.uploadedVideos)
    },
    handleFileUpload(event) {
      const files = event.target.files;
      if (!files || files.length === 0) return;
      
      Array.from(files).forEach(file => {
        this.uploadedDocs.push({
          file: file,
          name: file.name,
          type: file.type,
          size: file.size
        });
      });
      this.$message.success(`已添加 ${files.length} 个文件`);
      console.log("uploadedDocs:"+this.uploadedDocs)
    },
    removeUploadedItem(type, index) {
      if (type === 'image') {
        this.uploadedImages.splice(index, 1);
      } else if (type === 'video') {
        this.uploadedVideos.splice(index, 1);
      } else {
        this.uploadedDocs.splice(index, 1);
      }
      this.$message.success('已删除');
    },
    truncateFileName(name) {
      if (name.length > 10) {
        return name.substring(0, 8) + '...';
      }
      return name;
    },
    submitForm(formName) {
      this.$refs[formName].validate((valid)=>{
        if (!valid) {
            this.$message.error('请检查表单填写是否正确');
            return false;
        }
        
        // 检查是否上传了文件(如果需要)
        if (this.uploadedImages.length === 0 && 
            this.uploadedVideos.length === 0 && 
            this.uploadedDocs.length === 0) {
            this.$message.warning('请至少上传一份资料');
            return;
        }
        
        console.log("from"+this.form)
            // Prepare form data
        const formData = new FormData();
        formData.append('proposer_Name', this.form.proposer_Name);
        formData.append('telephone', this.form.telephone);
        formData.append('cardId', this.form.cardId);
        formData.append('charterName', this.form.charterName);
        formData.append('location', this.form.location.join('/'));
        formData.append('address', this.form.address);
        formData.append('expDate', this.form.expDate);
        formData.append('applyState', this.form.applyState);
        formData.append('licenseName',this.$route.query.name)
        
        // Add files
        this.uploadedImages.forEach(item => formData.append('img', item.file));
        this.uploadedVideos.forEach(item => formData.append('video', item.file));
        this.uploadedDocs.forEach(item => formData.append('file', item.file));
         console.log("formData:"+this.formData)
        console.log("uploadedImages:"+this.uploadedImages)
        console.log("uploadedVideos:"+this.uploadedVideos)
        console.log("uploadedDocs:"+this.uploadedDocs)
        // Submit form
        this.$axios.post('/api/user/addUser', formData).then(response => {
            console.log("response.data"+response.data)
            if(response.data==='success'){
                this.$message.success('提交成功');
                this.$router.push({
                  path:'/infoList'
                })
            }
        }).catch(error => {
            this.$message.error('提交失败');
            console.error(error);
        });
      })
    }
  }
}
</script>

<style scoped>
.content {
  height: 100%;
  width: 100%;
  padding: 20px;
  box-sizing: border-box;
  overflow-y: auto;
}

.infoAdd {
  width: 95%;
  margin: 0 auto;
  margin-top: 20px;
  border: 1px solid #EBEEF5;
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  padding: 20px;
  border-radius: 4px;
  background-color: #fff;
}

.title::before {
  content: '';
  display: inline-block;
  width: 4px;
  height: 14px;
  background-color: #409EFF;
  margin-right: 8px;
  vertical-align: middle;
}

.title {
  font-size: 16px;
  margin-bottom: 20px;
  font-weight: bold;
  color: #303133;
}

.items {
  margin-top: 12px;
  display: flex;
  justify-content: flex-start;
  flex-wrap: wrap;
  gap: 10px;
}

.itemAdd {
  width: 100px;
  height: 80px;
  background-color: #F5F7FA;
  border: 1px dashed #DCDFE6;
  border-radius: 4px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.3s;
  position: relative;
}

.itemAdd:hover {
  border-color: #409EFF;
}

.itemAdd img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 4px;
}

.upload-container {
  display: flex;
  justify-content: space-between;
  margin-top: 15px;
}

.upload-item {
  flex: 1;
  margin: 0 5px;
  height: 100px;
  background-color: #F5F7FA;
  border: 1px dashed #DCDFE6;
  border-radius: 4px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 0.3s;
}

.upload-item:hover {
  border-color: #409EFF;
}

.upload-icon {
  font-size: 24px;
  width: 40px;
  height: 40px;
  color: #909399;
  border: 1px solid #DCDFE6;
  box-sizing: border-box;
  border-radius: 50%;
  display: flex;
  justify-content: center;
  align-items: center;
  margin-bottom: 8px;
}

.upload-text {
  font-size: 14px;
  color: #606266;
}

.cancel {
  position: absolute;
  font-size: 16px;
  right: -8px;
  top: -8px;
  width: 20px;
  height: 20px;
  background: #f56c6c;
  color: white;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  z-index: 2;
}

.file-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.file-icon {
  margin-bottom: 5px;
}

.file-icon i {
  font-size: 30px;
  color: #409EFF;
}

.file-name {
  font-size: 12px;
  text-align: center;
  width: 90px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

video {
  background-color: #000;
  border-radius: 4px;
}

.sumBtn {
  width: 95%;
  margin: 30px auto;
  text-align: center;
}

.example {
  color: #909399;
  font-size: 14px;
}

.el-form-item {
  margin-bottom: 20px;
}

.el-cascader {
  width: 100%;
}

.el-date-editor {
  width: 100%;
}

.el-textarea__inner {
  min-height: 80px !important;
}
</style>

以下是基于SSM框架的后端实现代码,用于接收前端发送的表单数据和文件,并保存到数据库中的步骤:(在成功创建SSM项目并能启动成功之后)

1.在 pom.xml 中添加文件上传支持库:

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

2.在 spring-mvc.xml 中配置 MultipartResolver:

<bean id="multipartResolver" 
      class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <!-- 设置编码 -->
    <property name="defaultEncoding" value="UTF-8"/>
    <!-- 最大上传大小(单位:字节) -->
    <property name="maxUploadSize" value="10485760"/> <!-- 10MB -->
    <!-- 内存缓冲大小 -->
    <property name="maxInMemorySize" value="40960"/>
</bean>

还可能需要解决跨域问题:

<!--    配置CORS(跨域资源共享)的设定。它的作用是允许web应用处理来自不同源的跨域请求-->
    <mvc:cors>
        <mvc:mapping path="/**"
                     allowed-origins="*"
                     allowed-methods="GET, POST, PUT, DELETE, OPTIONS"
                     allowed-headers="Content-Type"/>
    </mvc:cors>

3.创建实体类,接收表单数据

@Data
public class User {
    private int id;
    private String proposerName;
    private String licenseName;
    private String telephone;
    private String cardId;
    private String charterName;
    private String location;
    private String address;
    private Date expDate;
    private String applyState;
}

4.mapper接口

public interface UserMapper {

   //添加申请人
   void addUser(User user);

   //更新申请人
   void updateUser(@Param("id") Integer id,@Param("type") String type,@Param("filePath") String fliePath);
}

5.mapper.xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yuankan.mapper.UserMapper">
    <resultMap id="userMap" type="com.yuankan.pojo.User">
        <!-- 主键字段使用id标签 -->
        <id property="id" column="id" />

        <!-- 普通字段使用result标签 -->
        <result property="proposerName" column="proposer_Name" />
        <result property="licenseName" column="licenseName"/>
        <result property="telephone" column="telephone" />
        <result property="cardId" column="cardId" />
        <result property="charterName" column="charterName" />
        <result property="location" column="location" />
        <result property="address" column="address" />
        <result property="expDate" column="expDate" />
        <result property="applyState" column="applyState" />
    </resultMap>

    <insert id="addUser" parameterType="user" useGeneratedKeys="true" keyProperty="id">
        insert into user  values(null,#{proposerName},#{licenseName},#{telephone},
        #{cardId},#{charterName},#{location},#{address},#{expDate}, #{applyState},null,null,null)
    </insert>
    <update id="updateUser">
        update user
        <set>
            <if test="type=='image'">
                img = #{filePath},
            </if>
            <if test="type=='video'">
                video = #{filePath},
            </if>
            <if test="type=='file'">
                file = #{filePath},
            </if>
        </set>
        WHERE id = #{id}
    </update>
</mapper>

6.service接口

public interface UserService {
    //添加申请人
    void addUser(User user, List<MultipartFile> images,
                 List<MultipartFile> videos,
                 List<MultipartFile> files);

}

7.service接口实现

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;
    /**
     * 添加申请人
     * @param user
     */
    @Override
    public void addUser(User user, List<MultipartFile> images,
                        List<MultipartFile> videos,
                        List<MultipartFile> files) {
        //保存申请人信息到数据库
        userMapper.addUser(user);
        // 保存上传的文件
        try {
            saveUploadedFiles(images, "image", user.getId());
            saveUploadedFiles(videos, "video", user.getId());
            saveUploadedFiles(files, "file", user.getId());
        } catch (IOException e) {
            throw new RuntimeException("文件保存失败", e);
        }
    }

    /**
     * 保存上传的文件
     * @param files 上传的文件
     * @param type 文件类型
     * @param id 申请人id
     * @throws IOException
     */
    private void saveUploadedFiles(List<MultipartFile> files, String type, Integer id) throws IOException {
       // 如果没有上传文件,则返回
        if (files == null || files.isEmpty()) return;
        // 遍历文件列表,保存文件到服务器
        for (MultipartFile file : files) {
            String filePath = FileUploadUtil.saveFile(file, type);
            // 更新数据库中的文件路径
            userMapper.updateUser(id,type,filePath);
        }
    }

}

FileUploadUtil工具类:

/**
 * 文件上传工具类
 */
public class FileUploadUtil {
    // 上传目录
    private static final String UPLOAD_DIR = "/uploads/";

    /**
     * 保存上传的文件
     * @param file 上传的文件
     * @param type 文件类型
     * @return
     * @throws IOException
     */
    public static String saveFile(MultipartFile file, String type) throws IOException {
        // 创建上传目录(如果不存在)
        File uploadDir = new File(UPLOAD_DIR + type);
        if (!uploadDir.exists()) {
            // 创建目录
            uploadDir.mkdirs();
        }
        
        // 生成唯一文件名
        String fileName = file.getOriginalFilename();
        //文件路径
        Path filePath = Paths.get(uploadDir.getAbsolutePath(), fileName);
        
        // 保存文件
        Files.copy(file.getInputStream(), filePath);
        // 返回文件路径
        return filePath.toString();
    }
}

8.Controller层(使用日志记录信息)

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    //获取Logger对象
    private static  final Logger logger= LogManager.getLogger(UserController.class);

    @PostMapping("/addUser")
    public String addUser(
            @RequestParam String proposer_Name,
            @RequestParam String telephone,
            @RequestParam String cardId,
            @RequestParam String charterName,
            @RequestParam String[] location, // 地区数组
            @RequestParam String address,
            @RequestParam Date expDate,
            @RequestParam String applyState,
            @RequestParam String licenseName,
            @RequestParam(required = false) MultipartFile[] img,
            @RequestParam(required = false) MultipartFile[] video,
            @RequestParam(required = false) MultipartFile[] file){

        // 创建申请人对象
        User user = new User();
        user.setProposerName(proposer_Name);
        logger.info("申请人姓名:"+proposer_Name);
        user.setTelephone(telephone);
        logger.info("申请人电话:"+telephone);
        user.setCardId(cardId);
        logger.info("申请人身份证号:"+cardId);
        user.setCharterName(charterName);
        logger.info("营业执照名称:"+charterName);
        user.setLocation(String.join("/", location));// 将数组转为字符串
        logger.info("申请人地址:"+String.join("/", location));
        user.setAddress(address);
        logger.info("申请人详细地址:"+address);
        user.setExpDate(expDate);
        logger.info("申请人有效期:"+expDate);
        user.setApplyState(applyState);
        logger.info("申请说明:"+applyState);
        user.setLicenseName(licenseName);
        logger.info("许可证名称:"+licenseName);
        logger.info("user:"+user);

        // 处理上传文件
        List<MultipartFile> images = img != null ? Arrays.asList(img) : null;
        List<MultipartFile> videos = video != null ? Arrays.asList(video) : null;
        List<MultipartFile> files = file != null ? Arrays.asList(file) : null;

        // 添加申请人
        userService.addUser(user, images, videos, files);
        return "success";
    }
}

日志使用:

1.导入依赖:

<!-- Log4j2 -->
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-core</artifactId>
      <version>2.17.2</version>
    </dependency>
    <dependency>
      <groupId>org.apache.logging.log4j</groupId>
      <artifactId>log4j-slf4j-impl</artifactId>
      <version>2.17.2</version>
    </dependency>

2.编写log4j2.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>

<!--        <RollingFile name="RollingFile" fileName="logs/ssm.log"-->
<!--                     filePattern="logs/ssm-%d{yyyy-MM-dd}.log">-->
<!--            <PatternLayout>-->
<!--                <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Pattern>-->
<!--            </PatternLayout>-->
<!--            <Policies>-->
<!--                <TimeBasedTriggeringPolicy interval="1" modulate="true"/>-->
<!--            </Policies>-->
<!--            <DefaultRolloverStrategy max="30"/>-->
<!--        </RollingFile>-->
    </Appenders>

    <Loggers>
        <Root level="info">
            <AppenderRef ref="Console"/>
<!--            <AppenderRef ref="RollingFile"/>-->
        </Root>

        <Logger name="com.it.dao" level="debug"/>
    </Loggers>
</Configuration>


网站公告

今日签到

点亮在社区的每一天
去签到