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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 08:50:38