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
|
||||
}
|
||||
|
||||
has_many :votes, as: :votable, dependent: :destroy
|
||||
|
||||
def to_s
|
||||
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
|
||||
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
|
||||
|
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 :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id
|
||||
has_many :votes, as: :votable, dependent: :destroy
|
||||
|
||||
validates :weather_date, presence: true
|
||||
validates :city_id, presence: true
|
||||
@ -55,4 +56,16 @@ class WeatherArt < ApplicationRecord
|
||||
def image_url
|
||||
image.attached? ? image.blob : nil
|
||||
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
|
||||
|
@ -143,6 +143,9 @@
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= render 'votes/vote_buttons', votable: @city %>
|
||||
|
||||
</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
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= render 'votes/vote_buttons', votable: @weather_art %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -39,4 +39,6 @@ Rails.application.routes.draw do
|
||||
|
||||
# Defines the root path route ("/")
|
||||
# root "posts#index"
|
||||
|
||||
resources :votes, only: [:create]
|
||||
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.
|
||||
|
||||
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|
|
||||
t.string "namespace"
|
||||
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
|
||||
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|
|
||||
t.integer "city_id", null: false
|
||||
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