Link Search Menu Expand Document

21. 多檔案上傳

21-1 上傳單張圖片

這一章介紹上傳文檔,示範的情境有:

  1. Event 活動可以上傳一張 Logo 圖片
  2. Event 活動可以上傳多張 Images 圖片
  3. Events 活動可以上傳多個附加文檔,每個文檔有各自的描述說明

在 Rails 中上傳和存儲文檔,最常見使用的 gem 是 carrierwavepaperclip,這裡我們使用 carrierwave。

接下來我來實作為 Event 活動上傳一張 Logo 圖片:

編輯 Gemfile

+  gem 'carrierwave'
+  gem "mini_magick"

執行 bundle,重啟伺服器

執行 rails g migration add_logo_to_events logo:string,這會產生一個 migration 內容是為 events 增加一個 logo 字段。

執行 rake db:migrate

執行 rails g uploader EventLogo

編輯 app/uploaders/event_logo_uploader.rb

  class EventLogoUploader < CarrierWave::Uploader::Base

+   include CarrierWave::MiniMagick

+   version :thumb do
+     process resize_to_fit: [50, 50]
+   end

  end

編輯 app/models/event.rb,把 carrierwave 的 Uploader 掛載上去。

  class Event < ApplicationRecord
+   mount_uploader :logo, EventLogoUploader

編輯 app/views/admin/events/_form.html.erb 加上文檔上傳的輸入框。如果已經有上傳過了,則多顯示刪除的 checkbox。


+  <div class="form-group">
+    <%= f.label :logo %>
+    <%= f.file_field :logo, :class => "form-control" %>
+    <% if f.object.logo.present? %>
+      <label>
+        <%= f.check_box :remove_logo %> 刪除圖檔
+      </label>
+      <%= link_to f.object.logo.filename, f.object.logo.url, :target => "_blank" %>
+    <% end %>
+  </div>

   <div class="form-group">
     <%= f.label :description %>

編輯 app/controllers/admin/events_controller.rb 加上 :logo:remove_logo 到 event_params

  def event_params
-    params.require(:event).permit(:name, :description, :friendly_id, :status, :category_id, :tickets_attributes => [:id, :name, :description, :price, :_destroy])
+    params.require(:event).permit(:name, :logo, :remove_logo, :description, :friendly_id, :status, :category_id, :tickets_attributes => [:id, :name, :description, :price, :_destroy])
  end

image

image

編輯前臺的活動頁面 app/views/events/show.html.erb,把圖片顯示出來

+ <% if @event.logo.present? %>
+   <%= link_to image_tag(@event.logo.url(:thumb)), @event.logo.url, :target => "_blank" %>
+ <% end %>

  <h1><%= @event.name %></h1>

最後,上傳的文檔不需要 git commit,所以請編輯 .gitignore 忽略掉 public/uploads 這個目錄。

  # Ignore Byebug command history file.
  .byebug_history
+ /public/uploads/*

這樣就完成了,請在後臺上傳圖片,然後到前臺活動頁就可看到了。

21-2 上傳多檔

情境:Event 活動可以上傳多張 Images 圖片

執行 rails g migration add_images_to_events images:string

執行 rake db:migrate

執行 rails g uploader EventImage

編輯 app/uploaders/event_image_uploader.rb

  class EventLogoUploader < CarrierWave::Uploader::Base

+   include CarrierWave::MiniMagick

+   version :small do
+     process resize_to_fit: [250, 250]
+   end

  end

編輯 app/models/event.rb,把 carrierwave 的 Uploader 掛載上去:

  class Event < ApplicationRecord
    mount_uploader :logo, EventLogoUploader
+   mount_uploaders :images, EventImageUploader
+   serialize :images, JSON

編輯 app/views/admin/events/_form.html.erb 加上文檔上傳的輸入框,注意到因為要支援多檔上傳,多了一個 :multiple => true 參數。


+  <div class="form-group">
+    <%= f.label :images %>
+    <%= f.file_field :images, :multiple => true, :class => "form-control" %>
+    <% if f.object.images.present? %>
+      <label>
+        <%= f.check_box :remove_images %> 刪除圖檔
+      </label>
+      <% f.object.images.each do |i| %>
+        <%= link_to i.filename, i.url, :target => "_blank" %>
+      <% end %>
+    <% end %>
+  </div>

   <div class="form-group">
     <%= f.label :description %>

編輯 app/controllers/admin/events_controller.rb 加上 :remove_images:images => [] 到 event_params

  def event_params
-    params.require(:event).permit(:name, :logo, :remove_logo, :description, :friendly_id, :status, :category_id, :tickets_attributes => [:id, :name, :description, :price, :_destroy])
+    params.require(:event).permit(:name, :logo, :remove_logo, :remove_images, :description, :friendly_id, :status, :category_id, :images => [], :tickets_attributes => [:id, :name, :description, :price, :_destroy])
  end

小心 :images => [] 要放在最後,因為預設的 Hash 雜湊參數都是放在參數最後

image

請用 command 點選文檔進行多選

編輯前臺的活動頁面 app/views/events/show.html.erb,把圖片顯示出來。注意到因為是多檔了,所以相比顯示 Logo 多了 .each 循環。

   <h2><%= @event.category.try(:name) %></h2>

+  <% if @event.images.present? %>
+    <% @event.images.each do |i| %>
+      <%= link_to image_tag(i.url(:small)), i.url %>
+    <% end %>
+  <% end %>

這樣就完成了。

21-3 上傳多檔案(使用額外 Model)

上一節的多檔上傳實作,我們用了 serialize :images, JSON 這會將多個文檔的檔名陣列,轉成 JSON 後存儲在 images 字段之中,Rails 在讀出來的時候,會自動轉回陣列。這一招讓 carrierwave 很簡單就可以支援多檔上傳,它把所有檔名的數據都塞進同一個字段 images

不過這種方法有幾個缺點:

  1. 無法增加每個文檔編輯額外的描述或其他資訊
  2. 無法為個別的文檔進行更新:重傳的時候,它會全部砍掉然後全部重新上傳
  3. 上傳的 UI 需要用戶知道用 Command 按鍵才能多選文檔。因此如果是後台管理員還可以教育,前臺一般用戶可能會不知道如何使用。

因此,接下來我們示範如何讓 Events 活動可以上傳多個附加文檔,每個文檔有各自的描述說明。

作法是新增一個 EventAttachment model,然後讓 Event has_many EventAttachment,其中每一筆 EventAttachment 就是一個附加文檔。表單 UI 的部分則使用實戰應用教過的 「嵌套表單(1-to-many」

執行 rails g model event_attachment,我們將讓 Event has_many 這個 EventAttachment model,然後將 carrierwave 也掛載到這個 EventAttachment model 身上。

編輯 201XXXXX025046_create_event_attachments.rb

  class CreateEventAttachments < ActiveRecord::Migration[5.0]
    def change
      create_table :event_attachments do |t|
+       t.integer :event_id, :index => true
+       t.string :attachment
+       t.string :description
        t.timestamps
      end
    end
  end

執行 rake db:migrate

執行 rails g uploader EventAttachment

編輯 app/models/event.rb

   has_many :tickets, :dependent => :destroy
   accepts_nested_attributes_for :tickets, :allow_destroy => true, :reject_if => :all_blank

+  has_many :attachments, :class_name => "EventAttachment", :dependent => :destroy
+  accepts_nested_attributes_for :attachments, :allow_destroy => true, :reject_if => :all_blank

編輯 app/models/event_attachment.rb

  class EventAttachment < ApplicationRecord

+   mount_uploader :attachment, EventAttachmentUploader
+   belongs_to :event

  end

這樣 models 的部分就好了。接著編輯後台表單 app/views/admin/events/_form.html.erb

+ <%= f.nested_fields_for :attachments do |ff| %>
+   <fieldset style="border-left: 5px solid #bbb; margin-bottom: 10px; padding: 10px;">
+     <legend>Attachment</legend>
+     <div class="form-group">
+       <%= ff.label :attachment %>
+       <%= ff.file_field :attachment, :class => "form-control" %>
+       <% if ff.object.attachment.present? %>
+         已上傳檔案 <%= link_to ff.object.description, ff.object.attachment.url, :target => "_blank" %>
+       <% end %>
+     </div>
+
+     <div class="form-group">
+       <%= ff.label :description %>
+       <%= ff.text_field :description, :class => "form-control" %>
+     </div>
+
+     <%= ff.remove_nested_fields_link "移除這個檔案", :class => "btn btn-danger" %>
+   </fieldset>
+ <% end %>
+ <p class="text-right">
+   <%= f.add_nested_fields_link :attachments, "新增檔案", :class => "btn btn-default" %>
+ </p>
+

 <%= f.nested_fields_for :tickets do |ff| %>

編輯 app/controllers/admin/events_controller.rb,如果該 @event 沒有 attachments 的話,new 出來一筆好讓表單可以顯示一筆進行編輯,以及把 attachments_attributes 加進 event_params

   def new
     @event = Event.new
     @event.tickets.build
+    @event.attachments.build
   end

   # (略)

   def edit
     @event = Event.find_by_friendly_id!(params[:id])
     @event.tickets.build if @event.tickets.empty?
+    @event.attachments.build if @event.attachments.empty?
   end

   # (略)

   protected

   def event_params
-    params.require(:event).permit(:name, :logo, :remove_logo, :remove_images, :description, :friendly_id, :status, :category_id, :images => [], :tickets_attributes => [:id, :name, :description, :price, :_destroy])
+    params.require(:event).permit(:name, :logo, :remove_logo, :remove_images, :description, :friendly_id, :status, :category_id, :images => [], :tickets_attributes => [:id, :name, :description, :price, :_destroy], :attachments_attributes => [:id, :attachment, :description, :_destroy])
   end

image

編輯前臺 app/views/events/show.html.erb

+  <ul>
+  <% @event.attachments.each do |a| %>
+    <li><%= link_to a.description, a.attachment.url %></li>
+  <% end %>
+  </ul>

  <%= sanitize @event.description %>

這樣就完成了。請在後臺上傳一些文檔和描述,然後到前臺瀏覽看看。


Copyright © 2010-2022 Wen-Tien Chang All Rights Reserved.