Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
e5c1461e8a |
49
app/controllers/votes_controller.rb
Normal file
49
app/controllers/votes_controller.rb
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
class VotesController < ApplicationController
|
||||||
|
def create
|
||||||
|
@votable = find_votable
|
||||||
|
user_session = session.id
|
||||||
|
|
||||||
|
# 检查是否已经存在投票
|
||||||
|
existing_vote = @votable.votes.find_by(user_session: user_session)
|
||||||
|
|
||||||
|
if existing_vote
|
||||||
|
# 如果点击相同类型的投票,则取消
|
||||||
|
if existing_vote.vote_type == params[:vote_type]
|
||||||
|
existing_vote.destroy
|
||||||
|
else
|
||||||
|
# 否则更新投票类型
|
||||||
|
existing_vote.update(vote_type: params[:vote_type])
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# 创建新投票
|
||||||
|
@votable.votes.create!(
|
||||||
|
user_session: user_session,
|
||||||
|
vote_type: params[:vote_type]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.turbo_stream do
|
||||||
|
render turbo_stream: [
|
||||||
|
turbo_stream.replace(
|
||||||
|
"#{@votable.class.name.downcase}_votes_#{@votable.id}",
|
||||||
|
partial: 'votes/vote_buttons',
|
||||||
|
locals: { votable: @votable }
|
||||||
|
)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def find_votable
|
||||||
|
if params[:weather_art_id]
|
||||||
|
WeatherArt.find(params[:weather_art_id])
|
||||||
|
elsif params[:city_id]
|
||||||
|
City.find(params[:city_id])
|
||||||
|
else
|
||||||
|
raise ActiveRecord::RecordNotFound
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
50
app/javascript/controllers/vote_controller.js
Normal file
50
app/javascript/controllers/vote_controller.js
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
connect() {
|
||||||
|
this.setupVoteButtons()
|
||||||
|
}
|
||||||
|
|
||||||
|
setupVoteButtons() {
|
||||||
|
document.querySelectorAll('.vote-btn').forEach(button => {
|
||||||
|
button.addEventListener('click', this.handleVote.bind(this))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleVote(event) {
|
||||||
|
const button = event.currentTarget
|
||||||
|
const votableType = button.dataset.votableType
|
||||||
|
const votableId = button.dataset.votableId
|
||||||
|
const action = button.dataset.action
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/votes', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
votable_type: votableType,
|
||||||
|
votable_id: votableId,
|
||||||
|
vote_type: action
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
this.updateVoteCounts(data)
|
||||||
|
} else {
|
||||||
|
alert(data.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Vote error:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVoteCounts(data) {
|
||||||
|
document.getElementById('upvotes-count').textContent = data.upvotes
|
||||||
|
document.getElementById('downvotes-count').textContent = data.downvotes
|
||||||
|
}
|
||||||
|
}
|
@ -71,6 +71,7 @@ class City < ApplicationRecord
|
|||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
has_many :votes, as: :votable, dependent: :destroy
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
name
|
name
|
||||||
@ -128,4 +129,16 @@ class City < ApplicationRecord
|
|||||||
Ahoy::Event.where("properties::jsonb->>'event_type' = 'city_view' AND (properties::jsonb->>'city_id')::integer = ?", self.id).count
|
Ahoy::Event.where("properties::jsonb->>'event_type' = 'city_view' AND (properties::jsonb->>'city_id')::integer = ?", self.id).count
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def vote_counts
|
||||||
|
{
|
||||||
|
upvotes: votes.upvote.count,
|
||||||
|
downvotes: votes.downvote.count
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_vote(user_session)
|
||||||
|
votes.find_by(user_session: user_session)&.vote_type
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
12
app/models/vote.rb
Normal file
12
app/models/vote.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
class Vote < ApplicationRecord
|
||||||
|
belongs_to :votable, polymorphic: true
|
||||||
|
|
||||||
|
enum :vote_type, { upvote: 0, downvote: 1 }
|
||||||
|
|
||||||
|
validates :user_session, presence: true
|
||||||
|
validates :votable_type, presence: true
|
||||||
|
validates :votable_id, presence: true
|
||||||
|
validates :vote_type, presence: true
|
||||||
|
|
||||||
|
scope :for_object, ->(obj) { where(votable: obj) }
|
||||||
|
end
|
@ -8,6 +8,7 @@ class WeatherArt < ApplicationRecord
|
|||||||
|
|
||||||
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :weather_art_id
|
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :weather_art_id
|
||||||
has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id
|
has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id
|
||||||
|
has_many :votes, as: :votable, dependent: :destroy
|
||||||
|
|
||||||
validates :weather_date, presence: true
|
validates :weather_date, presence: true
|
||||||
validates :city_id, presence: true
|
validates :city_id, presence: true
|
||||||
@ -55,4 +56,16 @@ class WeatherArt < ApplicationRecord
|
|||||||
def image_url
|
def image_url
|
||||||
image.attached? ? image.blob : nil
|
image.attached? ? image.blob : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def vote_counts
|
||||||
|
{
|
||||||
|
upvotes: votes.upvote.count,
|
||||||
|
downvotes: votes.downvote.count
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def user_vote(user_session)
|
||||||
|
votes.find_by(user_session: user_session)&.vote_type
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -143,6 +143,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<%= render 'votes/vote_buttons', votable: @city %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
30
app/views/votes/_vote_buttons.html.erb
Normal file
30
app/views/votes/_vote_buttons.html.erb
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# app/views/votes/_vote_buttons.html.erb
|
||||||
|
<%
|
||||||
|
Rails.logger.debug "Votable object: #{votable.inspect}"
|
||||||
|
Rails.logger.debug "Votable class: #{votable.class}"
|
||||||
|
Rails.logger.debug "Votable present?: #{votable.present?}"
|
||||||
|
%>
|
||||||
|
|
||||||
|
<div id="<%= "#{votable.class.name.downcase}_votes_#{votable.id}" %>">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<%= button_to votes_path,
|
||||||
|
params: {
|
||||||
|
"#{votable.class.name.downcase}_id": votable.id,
|
||||||
|
vote_type: :upvote
|
||||||
|
},
|
||||||
|
method: :post,
|
||||||
|
class: "btn btn-sm #{votable.user_vote(session.id) == 'upvote' ? 'btn-primary' : ''}" do %>
|
||||||
|
👍 <%= votable.vote_counts[:upvotes] %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<%= button_to votes_path,
|
||||||
|
params: {
|
||||||
|
"#{votable.class.name.downcase}_id": votable.id,
|
||||||
|
vote_type: :downvote
|
||||||
|
},
|
||||||
|
method: :post,
|
||||||
|
class: "btn btn-sm #{votable.user_vote(session.id) == 'downvote' ? 'btn-error' : ''}" do %>
|
||||||
|
👎 <%= votable.vote_counts[:downvotes] %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -115,6 +115,8 @@
|
|||||||
No more Weather Arts available
|
No more Weather Arts available
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<%= render 'votes/vote_buttons', votable: @weather_art %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
@ -39,4 +39,6 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
# Defines the root path route ("/")
|
# Defines the root path route ("/")
|
||||||
# root "posts#index"
|
# root "posts#index"
|
||||||
|
|
||||||
|
resources :votes, only: [:create]
|
||||||
end
|
end
|
||||||
|
14
db/migrate/20250205091907_create_votes.rb
Normal file
14
db/migrate/20250205091907_create_votes.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
class CreateVotes < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
create_table :votes do |t|
|
||||||
|
t.references :votable, polymorphic: true, null: false
|
||||||
|
t.string :user_session, null: false
|
||||||
|
t.integer :vote_type, default: 0
|
||||||
|
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
|
||||||
|
add_index :votes, [:votable_id, :votable_type, :user_session], unique: true
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
13
db/schema.rb
generated
13
db/schema.rb
generated
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_01_26_155239) do
|
ActiveRecord::Schema[8.0].define(version: 2025_02_05_091907) do
|
||||||
create_table "active_admin_comments", force: :cascade do |t|
|
create_table "active_admin_comments", force: :cascade do |t|
|
||||||
t.string "namespace"
|
t.string "namespace"
|
||||||
t.text "body"
|
t.text "body"
|
||||||
@ -155,6 +155,17 @@ ActiveRecord::Schema[8.0].define(version: 2025_01_26_155239) do
|
|||||||
t.index ["slug"], name: "index_regions_on_slug", unique: true
|
t.index ["slug"], name: "index_regions_on_slug", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "votes", force: :cascade do |t|
|
||||||
|
t.string "votable_type"
|
||||||
|
t.integer "votable_id"
|
||||||
|
t.string "session_id"
|
||||||
|
t.integer "vote_type", default: 0
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["votable_id", "votable_type", "session_id", "vote_type"], name: "index_votes_on_votable_and_session_id_and_vote_type", unique: true
|
||||||
|
t.index ["votable_type", "votable_id"], name: "index_votes_on_votable"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "weather_arts", force: :cascade do |t|
|
create_table "weather_arts", force: :cascade do |t|
|
||||||
t.integer "city_id", null: false
|
t.integer "city_id", null: false
|
||||||
t.date "weather_date"
|
t.date "weather_date"
|
||||||
|
11
test/fixtures/votes.yml
vendored
Normal file
11
test/fixtures/votes.yml
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
|
||||||
|
|
||||||
|
# This model initially had no columns defined. If you add columns to the
|
||||||
|
# model remove the "{}" from the fixture names and add the columns immediately
|
||||||
|
# below each fixture, per the syntax in the comments below
|
||||||
|
#
|
||||||
|
# one: {}
|
||||||
|
# column: value
|
||||||
|
#
|
||||||
|
# two: {}
|
||||||
|
# column: value
|
7
test/models/vote_test.rb
Normal file
7
test/models/vote_test.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class VoteTest < ActiveSupport::TestCase
|
||||||
|
# test "the truth" do
|
||||||
|
# assert true
|
||||||
|
# end
|
||||||
|
end
|
Loading…
Reference in New Issue
Block a user