如何在 Laravel 中从多个浏览器上传多个图像?

How to Upload Multiple Images from Multiple Browsing in Laravel?

这是我在 Stack Overflow 中的第一个问题。非常感谢任何建议。

我正在创建一个网站,它的主要功能是允许用户上传任意数量的图片。它可以来自不同的文件夹,用户可以根据需要多次浏览图像。此外,用户可以预览他们选择的图像。如果用户不想首先选择其中一张照片。用户可以通过单击预览图像附近的按钮将其删除。

在 Laravel 的查看部分,您将看到用于浏览多个文件的按钮以及告诉用户当前选择了多少张照片的文本。当我使用 View Part 时,它似乎工作得很好。但是,存储表格后的结果。显示只存储了上次浏览时选择的照片。

为了看得更清楚,这里是例子。

存储此来源的预期和正确结果是将 A、B、C、E 和 F 作为从用户选择的照片。但是发现数据库只存了第二次浏览的照片E和F

我想修复它以便它可以保存正确的照片;在这种情况下,A、B、C、E 和 F。

我使用javascript添加或删除预览图像并添加复选框以存储所选图像的名称。稍后我将在控制器中使用它来检查它是否是用户要存储的文件。这是代码:

var totalFiles = [];

    function handleFileSelect(evt) {

        // FileList object
        var files = evt.target.files; 

        // Loop through the FileList and render image files as thumbnails.
        for (var i = 0, f; f = files[i]; i++) {

            // Only process image files.
            if (!f.type.match('image.*')) {
                continue;
            }

            var reader = new FileReader();

            // Closure to capture the file information.
            reader.onload = (function(theFile) {
                return function(e) {
                // Render thumbnail.
                var span = document.createElement('span');
                span.innerHTML = ['<input type="checkbox" name="selected[]" value="' , theFile.name, '" style="display: none;" checked /><img width=50% height="auto" class="thumb p-3" src="', e.target.result,
                                    '" title="', theFile.name, '"/>', "<button onclick='deleteImage()'>" + "<i class='fas fa-trash-alt' aria-hidden='true'></i>" + " ลบรูปภาพ</button><br/>"].join('');

                document.getElementById('preview_photo').insertBefore(span, null);
                };
            })(f);

            totalFiles.push(f);

            // Read in the image file as a data URL.
            reader.readAsDataURL(f);
        }

        if(Array.from(totalFiles).length > 0){
            document.getElementById('count_selected_photo').innerHTML = "Image Selected:  " + Array.from(totalFiles).length + " Photo(s)";
        }
        else{
            document.getElementById('count_selected_photo').innerHTML = "No Image Selected";
        }
    }


    function deleteImage() { 
        var index = Array.from(document.getElementById('preview_photo').children).indexOf(event.target.parentNode)
        document.querySelector("#preview_photo").removeChild( document.querySelectorAll('#preview_photo span')[index]);

        totalFiles.splice(index, 1); 

        if(Array.from(totalFiles).length > 0){
            document.getElementById('count_selected_photo').innerHTML = "Image Selected:  " + Array.from(totalFiles).length + " Photo(s)";
        }
        else{
            document.getElementById('count_selected_photo').innerHTML = "No Image Selected";
        }
    }

    document.getElementById('before_photo').addEventListener('change', handleFileSelect, false);

在 Controller 中,我使用 request->file('before_photo') 获取删除前选择的文件,使用 request->input('selected') 获取所选照片的​​名称并将这两者放在一起比较。如果 request->file('before_photo') 中的文件名与 request->input('selected') 中的文件名相同,则将存储此照片。如果没有,这意味着用户在点击提交之前删除了它,它将不会存储。

这是我在 Controller 中的部分代码:

// Store Photo If Existed
                    if (request()->hasFile('before_photo')) {
                        // File Chosen at First Time
                        $before_photos = $request->file('before_photo');

                        // File Chosen After Delete Image
                        $selected_photos = $request->input('selected');

                        // Collect Path of Each Photo
                        $paths  = [];

                        // Collect File Name of Each Photo
                        $filenames = [];

                        foreach($selected_photos as $selected_photo){
                            foreach ($before_photos as $before_photo) {
                                // If File Name of Chosen Photo at First Time = File Name of Photo After Deleting
                                if($before_photo->getClientOriginalName() == $selected_photo){

                                    // Transform The File Name to be 'before-photo-TIME-name-originalFileName'
                                    $filename = "before-photo-" . time() . "-name-" . $before_photo->getClientOriginalName();
                                    $paths[] = $before_photo->storeAs('', $filename, 'irepair_photo');
                                    array_push($filenames, $filename);
                                }
                            }
                        }

                        // Transform List of Photo Name into Array
                        $repair_ticket->before_photo = $filenames;
                    }

如果您对此有任何建议或意见,请在下方留言。 感谢您的关注。

这可能不是您要查找的内容,但应该能为您指明正确的方向。我看到您使用的是 vanilla JavaScript,这可能会使事情变得更加棘手。但是因为你也在使用 Laravel 并且 Laravel 附带了 Vue 支持(至少它曾经是),我将向你展示我使用 Vue 的实现。如果你知道 JavaScript,应该不难理解,因为 Vue 非常简单。

多文件上传的诀窍是使用另一个数组来存储您的文件,因为由文件输入 return 编辑的 FileList object 是不可变的。看看下面的代码片段(我试图添加注释来解释我在做什么。)

您可以在此 pen 中看到一个工作示例。

<!-- @change is the Vue equivalent of onchange -->
<input @change="handleFileSelect" type="file" multiple>
<ul>
  <li v-for="(photo, index) in photos" :key="`thumb-${index}`">
    <img :src="photo.preview" :alt="photo.file.name">
    <button @click="removePhoto(index)" type="button">Remove Photo</button>
  </li>
</ul>

在 HTML 部分,我们所要做的就是监听文件输入的变化,并设置一个可以显示所选图像缩略图的位置。 Vue 通过使用 v-for 指令遍历数组使这变得非常容易。迭代中的当前元素可用于 li 及其 child 元素,我们可以使用它来填充属性,例如 imgsrc.

export default {
  data() {
    return {
      photos: []
    }
  },
  methods: {
    handleFileSelect(e) {
      Array.from(e.target.files).forEach(file => {
        // perform check here such as making sure its an image

        const reader = new FileReader()
        
        reader.onload = () => {
          // push the preview result and also the file to photos array
          this.photos.push({
            preview: reader.result,
            file
          })
        }
        
        reader.readAsDataURL(file)
      })
    },
    removePhoto(index) {
      this.photos.splice(index, 1)
    },
    upload() {
      const fd = new FormData()

      this.photos.forEach((photo, index) => {
        // we only want to append the actual file, excluding the preview
        // so we only append the `file` attribute
        fd.append(`photo-${index}`, photo.file)
      })

      // send the data
      // for example, using axios
      const uploadEndpoint = ''
      axios.post(uploadEndpoint, fd, {
        headers: {
          'Content-Type': 'multipart/form-data' // important
        }
      })
    }
  }
}

JavaScript 部分非常简单。我们在这里做的是设置一个 photos “本地状态”来存储我们的照片。我们使用此变量迭代并在 HTML 中显示缩略图。当用户选择图像时,我们获取文件并使用 FileReader object 为它们生成缩略图,并将其与实际文件一起推送到 photos 变量。

{
  preview: 'data:image/....', // the file preview generated by the FileReader
  file: File object // the actual file the user selected
}

当用户想要取消选择图片时,我们只需从 photos 数组中 splice 它,这样 photos 数组将始终包含用户想要上传的照片。当我们开始上传时,如果我们使用 AJAX 发送请求,我们可以创建一个 FormData object 并通过遍历 photos 变量将每张照片附加到它.在 Laravel 后端,然后可以使用 request()->file() 访问这些照片。在发送您的请求之前,您必须设置正确的 headers,主要是 Content-Type: multipart/form-data.

我希望这能让您对如何处理这个问题有一些大概的了解。如果您有任何问题,请随时将其留在评论部分,我会尽力解释更多。


更新:有关在 Laravel 后端

上处理照片的更多信息

从前端成功发送请求后,您可以使用 Laravel 的 request()->file() 辅助函数访问 photos/files。这将return请求中的所有文件放在一个数组中。

dd(request()->file());

输出类似于:

array:3 [
  "photo-0" => Illuminate\Http\UploadedFile {#221
    -test: false
    -originalName: "The Dark Reef Fugitive.jpg"
    -mimeType: "image/jpeg"
    -error: 0
    #hashName: null
    path: "/tmp"
    filename: "phpjr6Cx5"
    basename: "phpjr6Cx5"
    pathname: "/tmp/phpjr6Cx5"
    extension: ""
    realPath: "/tmp/phpjr6Cx5"
    aTime: 2020-03-03 08:29:19
    mTime: 2020-03-03 08:29:19
    cTime: 2020-03-03 08:29:19
    inode: 12976169
    size: 298634
    perms: 0100600
    owner: 1000
    group: 1000
    type: "file"
    writable: true
    readable: true
    executable: false
    file: true
    dir: false
    link: false
  }
  "photo-1" => Illuminate\Http\UploadedFile {#243
    -test: false
    -originalName: "Nevermore.jpg"
    -mimeType: "image/jpeg"
    -error: 0
    #hashName: null
    path: "/tmp"
    filename: "php3LNGW5"
    basename: "php3LNGW5"
    pathname: "/tmp/php3LNGW5"
    extension: ""
    realPath: "/tmp/php3LNGW5"
    aTime: 2020-03-03 08:29:19
    mTime: 2020-03-03 08:29:19
    cTime: 2020-03-03 08:29:19
    inode: 12976170
    size: 322173
    perms: 0100600
    owner: 1000
    group: 1000
    type: "file"
    writable: true
    readable: true
    executable: false
    file: true
    dir: false
    link: false
  }
  "photo-2" => Illuminate\Http\UploadedFile {#250
    -test: false
    -originalName: "Magnanumus.png"
    -mimeType: "image/png"
    -error: 0
    #hashName: null
    path: "/tmp"
    filename: "phpJZ9El6"
    basename: "phpJZ9El6"
    pathname: "/tmp/phpJZ9El6"
    extension: ""
    realPath: "/tmp/phpJZ9El6"
    aTime: 2020-03-03 08:29:19
    mTime: 2020-03-03 08:29:19
    cTime: 2020-03-03 08:29:19
    inode: 12976172
    size: 1068594
    perms: 0100600
    owner: 1000
    group: 1000
    type: "file"
    writable: true
    readable: true
    executable: false
    file: true
    dir: false
    link: false
  }
]

如您所见,每个文件都是 Illuminate\Http\UploadedFile 的一个实例。要存储此文件,您有几个选择。一种是在文件上使用 storestoreAs 函数,或者只使用 Storage facade 的 putputFileAs 方法。这是一个例子:

$filePaths = collect(request()->file())->values()->map(function ($photo) {
  return Storage::put('photos', $photo);
  // OR
  // return Storage::putFileAs('photos', $photo, $photo->getClientOriginalName());
  // OR
  // return $photo->store('photos');
  // OR
  // return $photo->storeAs('photos', $photo->getClientOriginalName());
});

阅读 Laravel docuemntation on file storage 以了解其他选项。

$filePaths 将是存储照片的路径数组。