如何在Ruby on Rails 7的轮播图单个图片上实现收藏图标
实现轮播单内容的独立收藏功能
需求背景
我基于Ruby on Rails开发了一款照片分享应用,支持用户对内容进行点赞、评论和收藏。当用户批量上传多张图片/视频时,这些内容会通过Bootstrap 5.2的轮播组件展示。目前用户收藏时会保存整个轮播下的所有内容,我需要调整为允许用户仅收藏轮播中单个的图片或视频。
现有代码片段
路由配置
resources :events do resources :attachments, only: [:new, :create, :index, :destroy] end resources :attachments, except: [:index, :new, :create, :destroy] do resources :comments, only: [:create, :edit, :update] resources :likes, only: :create resources :favorites, only: :create end resources :comments, only: [:destroy] resources :likes, only: :destroy resources :favorites, only: :destroy
Attachment模型
class Attachment < ApplicationRecord belongs_to :event belongs_to :user has_many :comments, dependent: :destroy has_many :likes, dependent: :destroy has_many_attached :posts has_many :favorites, dependent: :destroy validates :posts, presence: true end
Favorite模型
class Favorite < ApplicationRecord belongs_to :attachment belongs_to :user validates :user_id, uniqueness: { scope: :attachment_id} end
Attachment详情页视图
<div class="attachment-post"> <% if @attachment.posts.attached? %> <% if @attachment.posts.length > 1 %> <div id="carouselExampleIndicators" class="carousel slide" data-bs-ride="true"> <div class="carousel-indicators"> <% @attachment.posts.each_with_index do |post, index| %> <button type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide-to="<%="#{index}" %>" class="<%= index.zero? ? "active" : nil %>", aria-current="<%= index.zero? ? "true" : nil %>" aria-label="Slide <%="#{index}" %>"></button> <% end %> </div> <div class="carousel-inner"> <% @attachment.posts.each_with_index do |post, index| %> <div class="carousel-item <%= index.zero? ? "active" : nil %>"> <% if post.content_type.include?("image") %> <%= cl_image_tag post.key, class:"img-event-gallery d-block w-100", crop: :fill %> <% else %> <%= cl_video_tag post.key, controls: true, class:"img-event-gallery d-block w-100", crop: :fill %> <% end %> </div> <button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="visually-hidden">Previous</span> </button> <button class="carousel-control-next" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="next"> <span class="carousel-control-next-icon" aria-hidden="true"></span> <span class="visually-hidden">Next</span> </button> <% end %> </div> </div> <% else %> <% if @attachment.posts[0].content_type.include?("image") %> <%= cl_image_tag @attachment.posts[0].key, class:"img-event-gallery", crop: :fill %> <% else %> <%= cl_video_tag @attachment.posts[0].key, controls: true, class:"img-event-gallery", crop: :fill%> <% end %> <% end %> <% else %> <h3>Upload your images by clicking on the plus sign</h3> <% end %> </div> <div class="post-content"> <div class="like-and-delete-and-favorite d-flex justify-content-between"> <div class = "likes-icon d-flex justify-content-start" > <div class="likes-icon-number"> <p class="likes"><%= pluralize(@attachment.likes.count, "Like") %> <% like = current_user.likes.find_by(attachment: @attachment) %> <% if current_user %> <% if like.nil? %> <%= link_to attachment_likes_path(@attachment), data: {turbo_method: :post}, class:"likes-icons" do %> <i class="fa-regular fa-heart"></i> <% end %> <% else %> <%= link_to like_path(like), data: {turbo_method: :delete}, class:"likes-icons" do %> <i class="fa-solid fa-heart"></i> <% end %> <% end %> <% else %> <%# signup %> <%= link_to attachment_likes_path(@attachment), data: {turbo_method: :post}, class:"likes-icons" do %> <i class="fa-regular fa-heart"></i> <% end %> <% end %> </div> <div class="favorite-icon"> <% favorite = current_user.favorites.find_by(attachment: @attachment) %> <% if current_user %> <% if favorite.nil? %> <%= link_to attachment_favorites_path(@attachment), data: {turbo_method: :post}, class:"favorites-icons" do %> <i class="fa-regular fa-bookmark"></i> <% end %> <% else %> <%= link_to favorite_path(favorite), data: {turbo_method: :delete}, class:"favorites-icons" do %> <i class="fa-solid fa-bookmark"></i> <% end %> <% end %> <% else %> <%# signup %> <%= link_to attachment_favorites_path(@attachment), data: {turbo_method: :post}, class:"favorites-icons" do %> <i class="fa-regular fa-bookmark"></i> <% end %> <% end %> </div> </div> </div>
未收藏时的HTML代码
<a data-turbo-method="post" class="favorites-icons" href="/attachments/7/favorites"> <i class="fa-regular fa-bookmark"></i> </a>
已收藏时的HTML代码
<a data-turbo-method="delete" class="favorites-icons" href="/favorites/59"> <i class="fa-solid fa-bookmark"></i> </a>
实现方案
1. 重构收藏关联模型
当前Favorite关联的是整个Attachment,需要改成支持关联单个的上传内容(即ActiveStorage::Attachment,对应posts里的每一项)。这里用多态关联保证灵活性,后续也可扩展到其他可收藏内容:
修改Favorite模型
class Favorite < ApplicationRecord belongs_to :user belongs_to :favoritable, polymorphic: true validates :user_id, uniqueness: { scope: [:favoritable_type, :favoritable_id] } end
给ActiveStorage::Attachment添加可收藏能力
创建app/models/concerns/favoritable.rb:
module Favoritable extend ActiveSupport::Concern included do has_many :favorites, as: :favoritable, dependent: :destroy end end
在config/initializers/active_storage.rb中引入该模块:
ActiveStorage::Attachment.include(Favoritable)
更新Attachment模型(可选)
若需保留收藏整个Attachment的功能,修改Attachment模型:
class Attachment < ApplicationRecord # ... 原有代码 has_many :favorites, as: :favoritable, dependent: :destroy end
2. 调整路由配置
添加针对单个上传内容的收藏路由:
resources :events do resources :attachments, only: [:new, :create, :index, :destroy] end resources :attachments, except: [:index, :new, :create, :destroy] do resources :comments, only: [:create, :edit, :update] resources :likes, only: :create resources :favorites, only: :create # 保留原收藏整个Attachment的路由(可选) # 添加单个post的收藏路由 resources :posts, only: [] do resources :favorites, only: [:create, :destroy] end end resources :comments, only: [:destroy] resources :likes, only: :destroy resources :favorites, only: :destroy
3. 更新Favorites控制器
修改控制器以支持处理单个上传内容的收藏请求:
class FavoritesController < ApplicationController before_action :set_favoritable def create @favorite = current_user.favorites.create(favoritable: @favoritable) respond_to do |format| format.turbo_stream # 支持Turbo无刷新更新 format.html { redirect_back fallback_location: root_path } end end def destroy @favorite = current_user.favorites.find(params[:id]) @favorite.destroy respond_to do |format| format.turbo_stream format.html { redirect_back fallback_location: root_path } end end private def set_favoritable if params[:attachment_id] && params[:post_id] # 单个上传内容的收藏 @favoritable = ActiveStorage::Attachment.find(params[:post_id]) elsif params[:attachment_id] # 整个Attachment的收藏(保留的话) @favoritable = Attachment.find(params[:attachment_id]) end end end
4. 修改视图,在轮播项中添加独立收藏按钮
把收藏按钮从全局位置移到每个轮播项内部,针对单个上传内容生成收藏链接:
<div class="carousel-inner"> <% @attachment.posts.each_with_index do |post, index| %> <div class="carousel-item <%= index.zero? ? "active" : nil %>"> <% if post.content_type.include?("image") %> <%= cl_image_tag post.key, class:"img-event-gallery d-block w-100", crop: :fill %> <% else %> <%= cl_video_tag post.key, controls: true, class:"img-event-gallery d-block w-100", crop: :fill %> <% end %> <!-- 单个内容的收藏按钮 --> <div class="carousel-favorite-btn"> <% if current_user %> <% favorite = current_user.favorites.find_by(favoritable: post) %> <% if favorite.nil? %> <%= link_to attachment_post_favorites_path(@attachment, post), data: {turbo_method: :post}, class:"favorites-icons", id: dom_id(post, :favorite) do %> <i class="fa-regular fa-bookmark"></i> <% end %> <% else %> <%= link_to favorite_path(favorite), data: {turbo_method: :delete}, class:"favorites-icons", id: dom_id(post, :favorite) do %> <i class="fa-solid fa-bookmark"></i> <% end %> <% end %> <% else %> <%= link_to attachment_post_favorites_path(@attachment, post), data: {turbo_method: :post}, class:"favorites-icons", id: dom_id(post, :favorite) do %> <i class="fa-regular fa-bookmark"></i> <% end %> <% end %> </div> </div> <% end %> <!-- 轮播控制按钮移到循环外面,避免重复生成 --> <button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="prev"> <span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="visually-hidden">Previous</span> </button> <button class="carousel-control-next" type="button" data-bs-target="#carouselExampleIndicators" data-bs-slide="next"> <span class="carousel-control-next-icon" aria-hidden="true"></span> <span class="visually-hidden">Next</span> </button> </div>
添加CSS样式调整按钮位置
给收藏按钮添加定位样式,让它显示在轮播内容的右上角:
.carousel-item { position: relative; } .carousel-favorite-btn { position: absolute; top: 1rem; right: 1rem; z-index: 10; } .favorites-icons { color: #fff; font-size: 1.5rem; text-shadow: 0 0 4px rgba(0,0,0,0.6); transition: color 0.2s; } .favorites-icons:hover { color: #ffc107; }
5. 实现Turbo无刷新更新(可选)
创建Turbo流视图文件,实现点击收藏按钮后无刷新切换图标状态:
app/views/favorites/create.turbo_stream.erb
<%= turbo_stream.replace dom_id(@favorite.favoritable, :favorite) do %> <%= link_to favorite_path(@favorite), data: {turbo_method: :delete}, class:"favorites-icons", id: dom_id(@favorite.favoritable, :favorite) do %> <i class="fa-solid fa-bookmark"></i> <% end %> <% end %>
app/views/favorites/destroy.turbo_stream.erb
<%= turbo_stream.replace dom_id(@favorite.favoritable, :favorite) do %> <%= link_to attachment_post_favorites_path(@favorite.favoritable.record, @favorite.favoritable), data: {turbo_method: :post}, class:"favorites-icons", id: dom_id(@favorite.favoritable, :favorite) do %> <i class="fa-regular fa-bookmark"></i> <% end %> <% end %>
这样用户点击收藏按钮时,页面会动态更新图标状态,不需要刷新整个页面。
内容的提问来源于stack exchange,提问作者Philippe Paoli
相关产品推荐
相关产品推荐

