From f044ad705d9fed90e61b97f781c005c7abf6b481 Mon Sep 17 00:00:00 2001 From: xxq250 Date: Thu, 16 Oct 2025 09:26:31 +0800 Subject: [PATCH 1/8] =?UTF-8?q?fixed=20pr=E6=B5=81=E6=B0=B4=E7=BA=BF?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/api/v1/projects/pipelines/build_pipeline.yaml.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/api/v1/projects/pipelines/build_pipeline.yaml.erb b/app/views/api/v1/projects/pipelines/build_pipeline.yaml.erb index 41908c8f5..02b40f71d 100644 --- a/app/views/api/v1/projects/pipelines/build_pipeline.yaml.erb +++ b/app/views/api/v1/projects/pipelines/build_pipeline.yaml.erb @@ -19,6 +19,7 @@ on: <% end %> <%if node.name.to_s.include?("on-pull_request") %> pull_request: + types: [opened, reopened] <% end %> <%if node.name.to_s.include?("on-fork") %> fork: From 2bd58d2ad70bc60852ee054ccb8bf514215528e5 Mon Sep 17 00:00:00 2001 From: yystopf Date: Fri, 17 Oct 2025 14:01:51 +0800 Subject: [PATCH 2/8] =?UTF-8?q?fix:=20action=20=E6=95=B0=E6=8D=AE=E5=9F=8B?= =?UTF-8?q?=E7=82=B9=E9=95=BF=E4=BF=A1=E6=81=AF=E7=A7=BB=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/project_actions/fork_event.rb | 2 +- app/models/project_actions/issue_comment_event.rb | 2 +- app/models/project_actions/issues_event.rb | 2 +- app/models/project_actions/pull_request_event.rb | 6 +++--- .../project_actions/pull_request_review_comment_event.rb | 2 +- app/models/project_actions/release_event.rb | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/models/project_actions/fork_event.rb b/app/models/project_actions/fork_event.rb index 0d9bb99b0..c6815533a 100644 --- a/app/models/project_actions/fork_event.rb +++ b/app/models/project_actions/fork_event.rb @@ -24,7 +24,7 @@ class ProjectActions::ForkEvent < ProjectAction forkee_name: fork_project.identifier, forkee_owner_id: fork_project.owner.id, forkee_owner_login: fork_project.owner.login - } + }.as_json project_action.save! end end diff --git a/app/models/project_actions/issue_comment_event.rb b/app/models/project_actions/issue_comment_event.rb index 2a5a73fdd..46e2bbe63 100644 --- a/app/models/project_actions/issue_comment_event.rb +++ b/app/models/project_actions/issue_comment_event.rb @@ -23,7 +23,7 @@ class ProjectActions::IssueCommentEvent < ProjectAction issue_id: issue.id, issue_number: issue.project_issues_index, body: journal.notes - } + }.as_json project_action.save! end end diff --git a/app/models/project_actions/issues_event.rb b/app/models/project_actions/issues_event.rb index a51536a35..fba70e798 100644 --- a/app/models/project_actions/issues_event.rb +++ b/app/models/project_actions/issues_event.rb @@ -27,7 +27,7 @@ class ProjectActions::IssuesEvent < ProjectAction title: issue.subject, body: issue.description, action: action - } + }.as_json project_action.save! end end diff --git a/app/models/project_actions/pull_request_event.rb b/app/models/project_actions/pull_request_event.rb index b27e43c41..b7846e723 100644 --- a/app/models/project_actions/pull_request_event.rb +++ b/app/models/project_actions/pull_request_event.rb @@ -25,8 +25,8 @@ class ProjectActions::PullRequestEvent < ProjectAction pull_id: pull_request.id, pull_number: pull_request.gitea_number, title: pull_request.title, - body: pull_request.body, - git_diff: gitea_pull_request, + # body: pull_request.body, + # git_diff: gitea_pull_request, additions: gitea_pull_request['TotalAddition'], deletions: gitea_pull_request['TotalDeletion'], changed_files: gitea_pull_request['NumFiles'], @@ -35,7 +35,7 @@ class ProjectActions::PullRequestEvent < ProjectAction head_repo_id: pull_request.is_original ? pull_request.fork_project_id : pull_request.project_id, head_repo_name: pull_request.project&.identifier, head_ref: pull_request.head - } + }.as_json project_action.save! end end diff --git a/app/models/project_actions/pull_request_review_comment_event.rb b/app/models/project_actions/pull_request_review_comment_event.rb index a1a2fa0f6..22a6677a4 100644 --- a/app/models/project_actions/pull_request_review_comment_event.rb +++ b/app/models/project_actions/pull_request_review_comment_event.rb @@ -23,7 +23,7 @@ class ProjectActions::PullRequestReviewCommentEvent < ProjectAction pull_id: pull_request.id, pull_number: pull_request.gitea_number, body: journal.notes - } + }.as_json project_action.save! end end diff --git a/app/models/project_actions/release_event.rb b/app/models/project_actions/release_event.rb index 2505d1603..9a46f94c3 100644 --- a/app/models/project_actions/release_event.rb +++ b/app/models/project_actions/release_event.rb @@ -23,7 +23,7 @@ class ProjectActions::ReleaseEvent < ProjectAction tag_name: version_release.tag_name, name: version_release.name, body: version_release.body - } + }.as_json project_action.save! end end From 343e48890302bdcdd514318fb4949efd91883be5 Mon Sep 17 00:00:00 2001 From: yystopf Date: Wed, 22 Oct 2025 08:59:51 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E6=8A=95?= =?UTF-8?q?=E7=A5=A8=E5=8A=9F=E8=83=BD=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/platform_communicates_controller.rb | 104 +++++++++++++++++- app/models/platform_communicate.rb | 2 + app/models/user.rb | 3 + app/models/user_vote_option.rb | 19 ++++ app/models/vote.rb | 52 +++++++++ app/models/vote_option.rb | 11 ++ .../platform_communicates/_vote.json.jbuilder | 4 + .../vote_results.json.jbuilder | 7 ++ config/environments/development.rb | 3 +- config/routes.rb | 7 +- db/migrate/20251021015122_create_votes.rb | 13 +++ .../20251021055402_create_vote_options.rb | 12 ++ ...20251021060115_create_user_vote_options.rb | 10 ++ 13 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 app/models/user_vote_option.rb create mode 100644 app/models/vote.rb create mode 100644 app/models/vote_option.rb create mode 100644 app/views/home/platform_communicates/_vote.json.jbuilder create mode 100644 app/views/home/platform_communicates/vote_results.json.jbuilder create mode 100644 db/migrate/20251021015122_create_votes.rb create mode 100644 db/migrate/20251021055402_create_vote_options.rb create mode 100644 db/migrate/20251021060115_create_user_vote_options.rb diff --git a/app/controllers/home/platform_communicates_controller.rb b/app/controllers/home/platform_communicates_controller.rb index a47fb3729..70f991718 100644 --- a/app/controllers/home/platform_communicates_controller.rb +++ b/app/controllers/home/platform_communicates_controller.rb @@ -1,8 +1,110 @@ class Home::PlatformCommunicatesController < ApplicationController + before_action :require_login, only: [:submit_vote, :vote_results] + before_action :load_vote, only: [:submit_vote, :vote_results] def index location = params[:location] || "pc" scope = PlatformCommunicate.where(status:true).where(location: location).order(order_index: :desc) - @communicates = kaminari_paginate(scope) + @communicates = kaminari_paginate(scope.includes(:vote)) end + + def submit_vote + return render_error("投票已结束!") if @vote.expired? + + # 处理投票选项 + selected_option_ids = vote_params[:vote_option_ids] || [] + # 验证选择数量 + validation_result = validate_vote_selections(selected_option_ids) + return render_error(validation_result[:message]) unless validation_result[:valid] + # 保存投票 + result = save_user_votes(selected_option_ids) + if result[:success] + render_ok + else + render_error(result[:errors].join(', ')) + end + end + + def vote_results + @vote_results = @vote.results_with_cache + @selected_option_ids = current_user.user_vote_options.where(vote_option: @vote.vote_options).pluck(:vote_option_id) + end + + private + def load_vote + @communicate = PlatformCommunicate.find_by_id(params[:id]) + @vote = @communicate.vote + return render_error("投票不存在!") unless @vote.present? + end + + def vote_params + params.require(:vote).permit(vote_option_ids: []) + end + + def validate_vote_selections(selected_option_ids) + selected_option_ids = Array(selected_option_ids).map(&:to_i).uniq + + # 检查是否选择了选项 + if selected_option_ids.empty? + return { valid: false, message: '请至少选择一个投票选项' } + end + + # 检查选项是否属于该投票 + valid_option_ids = @vote.vote_options.pluck(:id) + invalid_options = selected_option_ids - valid_option_ids + + unless invalid_options.empty? + return { valid: false, message: '包含无效的投票选项' } + end + + # 检查选择数量限制 + if @vote.is_multiple_choice + max_choices = @vote.max_choice_vote_options_count || selected_option_ids.length + if selected_option_ids.length > max_choices + return { + valid: false, + message: "最多只能选择 #{max_choices} 个选项" + } + end + else + if selected_option_ids.length > 1 + return { valid: false, message: '只能选择一个选项' } + end + end + + { valid: true } + end + + def save_user_votes(selected_option_ids) + selected_option_ids = Array(selected_option_ids).map(&:to_i) + saved_selections = [] + errors = [] + + ActiveRecord::Base.transaction do + # 如果是多选,先删除用户之前的投票记录(重新投票) + if @vote.is_multiple_choice + @vote.user_vote_options.where(user_id: current_user.id).destroy_all + end + + selected_option_ids.each do |option_id| + vote_option = @vote.vote_options.find(option_id) + user_vote_option = current_user.user_vote_options.new( + vote_option: vote_option + ) + + if user_vote_option.save + saved_selections << user_vote_option + else + errors << user_vote_option.errors.full_messages + raise ActiveRecord::Rollback + end + end + end + + if errors.empty? + { success: true, selections: saved_selections } + else + { success: false, errors: errors.flatten } + end + end end \ No newline at end of file diff --git a/app/models/platform_communicate.rb b/app/models/platform_communicate.rb index 8c7cb089f..d327e926e 100644 --- a/app/models/platform_communicate.rb +++ b/app/models/platform_communicate.rb @@ -21,6 +21,8 @@ class PlatformCommunicate < ApplicationRecord before_save :add_fake_id, on: [:create, :update] has_many :watchers, as: :watchable, dependent: :destroy + has_one :vote, dependent: :destroy + has_many :vote_options, through: :vote def watched_ext_info(user_id) watcher = self.watchers.find_by(user_id: user_id) diff --git a/app/models/user.rb b/app/models/user.rb index 214179993..aa1747ff3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -192,6 +192,9 @@ class User < Owner has_one :page, :dependent => :destroy has_many :home_top_settings, dependent: :destroy + + has_many :user_vote_options, dependent: :destroy + has_many :vote_options, through: :user_vote_options # Groups and active users scope :active, lambda { where(status: [STATUS_ACTIVE, STATUS_EDIT_INFO]) } diff --git a/app/models/user_vote_option.rb b/app/models/user_vote_option.rb new file mode 100644 index 000000000..11f11c03c --- /dev/null +++ b/app/models/user_vote_option.rb @@ -0,0 +1,19 @@ +class UserVoteOption < ApplicationRecord + belongs_to :user + belongs_to :vote_option, counter_cache: true + + validates :user_id, uniqueness: { scope: :vote_option_id, message: "您已经投过票了" } + + validate :user_can_vote_only_once_per_vote + + private + def user_can_vote_only_once_per_vote + return unless user && vote_option + + existing_vote = UserVoteOption.joins(:vote_option) + .where(user_id: user_id, vote_options: { vote_id: vote_option.vote_id }) + .where.not(id: id) # 排除自身(更新时) + .exists? + errors.add(:base, '您已经投过票了') if existing_vote + end +end diff --git a/app/models/vote.rb b/app/models/vote.rb new file mode 100644 index 000000000..e415871c6 --- /dev/null +++ b/app/models/vote.rb @@ -0,0 +1,52 @@ +class Vote < ApplicationRecord + belongs_to :platform_communicate + has_many :vote_options, dependent: :destroy + has_many :user_vote_options, through: :vote_options + + validates :max_choice_vote_options_count, numericality: { greater_than: 0 } + + def expired? + due_at.present? && due_at < Time.current + end + + def user_voted?(user) + return false unless user + + user_vote_options.where(user_id: user.id).exists? + end + + def user_selections(user) + return UserVoteOption.none unless user + + user_vote_options.where(user_id: user.id) + end + + def total_votes_count + # 计算参与投票的总人数(去重) + user_vote_options.select(:user_id).distinct.count + end + + def total_votes_options_count + vote_options.sum(:user_vote_options_count) + end + + # 获取投票结果(使用缓存字段,性能更好) + def results_with_cache + vote_options.select(:id, :title, :user_vote_options_count).map do |option| + { + id: option.id, + title: option.title, + vote_count: option.user_vote_options_count, + percentage: calculate_percentage_for_option(option.user_vote_options_count) + } + end + end + + private + + def calculate_percentage_for_option(option_votes) + total = total_votes_options_count + return 0 if total.zero? + (option_votes.to_f / total * 100).round(2) + end +end diff --git a/app/models/vote_option.rb b/app/models/vote_option.rb new file mode 100644 index 000000000..a9dafd991 --- /dev/null +++ b/app/models/vote_option.rb @@ -0,0 +1,11 @@ +class VoteOption < ApplicationRecord + + belongs_to :vote + has_many :user_vote_options, dependent: :destroy + has_many :users, through: :user_vote_options + validates :title, presence: true + + def self.selected?(option, user) + option.user_vote_options.exists?(user_id: user.id) + end +end diff --git a/app/views/home/platform_communicates/_vote.json.jbuilder b/app/views/home/platform_communicates/_vote.json.jbuilder new file mode 100644 index 000000000..baaedb5a7 --- /dev/null +++ b/app/views/home/platform_communicates/_vote.json.jbuilder @@ -0,0 +1,4 @@ +json.(vote, :id, :description, :is_multiple_choice, :max_choice_vote_options_count) +json.created_at vote.created_at.to_i +json.due_at vote.due_at.to_i +json.total_votes_count vote.total_votes_count # 计算参与投票的总人数(去重) \ No newline at end of file diff --git a/app/views/home/platform_communicates/vote_results.json.jbuilder b/app/views/home/platform_communicates/vote_results.json.jbuilder new file mode 100644 index 000000000..22f3741c4 --- /dev/null +++ b/app/views/home/platform_communicates/vote_results.json.jbuilder @@ -0,0 +1,7 @@ +json.vote do + json.partial! "home/platform_communicates/vote", locals: {vote: @vote} +end +json.selected_option_ids @selected_option_ids +json.vote_results @vote_results do |result| + json.(result, :id, :title, :vote_count, :percentage) +end \ No newline at end of file diff --git a/config/environments/development.rb b/config/environments/development.rb index 199481259..2591ab8c1 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -15,7 +15,8 @@ Rails.application.configure do # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. - config.cache_store = :file_store, "#{Rails.root }/files/cache_store/" + # config.cache_store = :file_store, "#{Rails.root }/files/cache_store/" + config.cache_store = :redis_store, { url: 'redis://127.0.0.1:6379/1', namespace: 'cache_store' } # if Rails.root.join('tmp', 'caching-dev.txt').exist? # config.action_controller.perform_caching = true diff --git a/config/routes.rb b/config/routes.rb index f68039d24..4f0e6cb36 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -145,7 +145,12 @@ Rails.application.routes.draw do namespace :home do resources :platform_statistics, only: [:index] resources :competitions, only:[:index] - resources :platform_communicates, only: [:index] + resources :platform_communicates, only: [:index] do + member do + post :submit_vote + get :vote_results + end + end resources :platform_people, only: [:index] end get 'home/index' diff --git a/db/migrate/20251021015122_create_votes.rb b/db/migrate/20251021015122_create_votes.rb new file mode 100644 index 000000000..9445d0316 --- /dev/null +++ b/db/migrate/20251021015122_create_votes.rb @@ -0,0 +1,13 @@ +class CreateVotes < ActiveRecord::Migration[5.2] + def change + create_table :votes do |t| + t.references :platform_communicate + t.text :description + t.boolean :is_multiple_choice, default: false + t.integer :max_choice_vote_options_count, default: 1 + t.datetime :due_at + + t.timestamps + end + end +end diff --git a/db/migrate/20251021055402_create_vote_options.rb b/db/migrate/20251021055402_create_vote_options.rb new file mode 100644 index 000000000..0bff1e4dc --- /dev/null +++ b/db/migrate/20251021055402_create_vote_options.rb @@ -0,0 +1,12 @@ +class CreateVoteOptions < ActiveRecord::Migration[5.2] + def change + create_table :vote_options do |t| + t.references :vote + t.string :title + t.integer :order_index, default: 0 + t.integer :user_vote_options_count, default: 0 + + t.timestamps + end + end +end diff --git a/db/migrate/20251021060115_create_user_vote_options.rb b/db/migrate/20251021060115_create_user_vote_options.rb new file mode 100644 index 000000000..31aa04c1b --- /dev/null +++ b/db/migrate/20251021060115_create_user_vote_options.rb @@ -0,0 +1,10 @@ +class CreateUserVoteOptions < ActiveRecord::Migration[5.2] + def change + create_table :user_vote_options do |t| + t.references :user, index: true + t.references :vote_option, index: true + + t.timestamps + end + end +end From 6d112ffdf5df4d57a269cf1c900b6f7b8c2c16de Mon Sep 17 00:00:00 2001 From: yystopf Date: Wed, 22 Oct 2025 09:11:54 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/vote.rb | 2 ++ db/migrate/20251021015122_create_votes.rb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/vote.rb b/app/models/vote.rb index e415871c6..9fce41dc2 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -1,4 +1,6 @@ class Vote < ApplicationRecord + self.table_name = 'new_votes' + belongs_to :platform_communicate has_many :vote_options, dependent: :destroy has_many :user_vote_options, through: :vote_options diff --git a/db/migrate/20251021015122_create_votes.rb b/db/migrate/20251021015122_create_votes.rb index 9445d0316..e135946c4 100644 --- a/db/migrate/20251021015122_create_votes.rb +++ b/db/migrate/20251021015122_create_votes.rb @@ -1,6 +1,6 @@ class CreateVotes < ActiveRecord::Migration[5.2] def change - create_table :votes do |t| + create_table :new_votes do |t| t.references :platform_communicate t.text :description t.boolean :is_multiple_choice, default: false From c8c08e143c2f7947cecf55e05dc7d14e5347ff33 Mon Sep 17 00:00:00 2001 From: yystopf Date: Wed, 22 Oct 2025 09:48:15 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/user_vote_option.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/models/user_vote_option.rb b/app/models/user_vote_option.rb index 11f11c03c..c1e4f3f10 100644 --- a/app/models/user_vote_option.rb +++ b/app/models/user_vote_option.rb @@ -4,8 +4,6 @@ class UserVoteOption < ApplicationRecord validates :user_id, uniqueness: { scope: :vote_option_id, message: "您已经投过票了" } - validate :user_can_vote_only_once_per_vote - private def user_can_vote_only_once_per_vote return unless user && vote_option From c50dee7b018065887164d3e0a4fa9fee67538953 Mon Sep 17 00:00:00 2001 From: yystopf Date: Wed, 22 Oct 2025 09:51:23 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E6=98=AF=E5=90=A6=E5=90=AB=E6=8A=95=E7=A5=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/home/platform_communicates/index.json.jbuilder | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/home/platform_communicates/index.json.jbuilder b/app/views/home/platform_communicates/index.json.jbuilder index 8a2d3d4d1..f88f5e0d1 100644 --- a/app/views/home/platform_communicates/index.json.jbuilder +++ b/app/views/home/platform_communicates/index.json.jbuilder @@ -9,5 +9,6 @@ json.communicates do json.enroll_field communicate.enroll_field.to_s.split(",") json.enroll_info current_user.watched?(communicate) ? communicate.watched_ext_info(current_user.id) : [] json.url communicate.url_or_uuid + json.has_vote communicate.vote.present? end end \ No newline at end of file From 362dd1a0335a2717ce2dff5ba641a1481c8c9005 Mon Sep 17 00:00:00 2001 From: yystopf Date: Wed, 22 Oct 2025 17:10:05 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E6=96=B0=E5=A2=9E=EF=BC=9A=E6=8A=95?= =?UTF-8?q?=E7=A5=A8=E5=90=8E=E5=8F=B0=E7=AE=A1=E7=90=86=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform_communicates_controller.rb | 53 ++- app/models/vote.rb | 2 + .../platform_communicates/_list.html.erb | 5 + .../platform_communicates/_vote_form.html.erb | 312 ++++++++++++++++++ .../platform_communicates/edit_vote.html.erb | 15 + .../platform_communicates/new_vote.html.erb | 15 + config/routes.rb | 4 + 7 files changed, 405 insertions(+), 1 deletion(-) create mode 100644 app/views/admins/platform_communicates/_vote_form.html.erb create mode 100644 app/views/admins/platform_communicates/edit_vote.html.erb create mode 100644 app/views/admins/platform_communicates/new_vote.html.erb diff --git a/app/controllers/admins/platform_communicates_controller.rb b/app/controllers/admins/platform_communicates_controller.rb index 15d2c66c3..8bd84059c 100644 --- a/app/controllers/admins/platform_communicates_controller.rb +++ b/app/controllers/admins/platform_communicates_controller.rb @@ -1,5 +1,5 @@ class Admins::PlatformCommunicatesController < Admins::BaseController - before_action :get_communicate, only: [:edit, :update, :destroy, :show, :online_switch] + before_action :get_communicate, only: [:edit, :update, :destroy, :show, :online_switch, :new_vote, :create_vote, :edit_vote, :update_vote] def index params[:location] = "pc" @@ -85,6 +85,42 @@ class Admins::PlatformCommunicatesController < Admins::BaseController # render_ok end + def new_vote + @vote = Vote.new(platform_communicate_id: @communicate.id) + 2.times { @vote.vote_options.build } + end + + def create_vote + @vote = Vote.new(platform_communicate_id: @communicate.id) + @vote.attributes = vote_params + + if @vote.save + redirect_to applet_admins_platform_communicates_path, notice: '投票创建成功' + else + # 确保至少有2个选项显示在表单中 + @vote.vote_options.build if @vote.vote_options.empty? + @vote.vote_options.build if @vote.vote_options.size == 1 + + render :new_vote + end + end + + def edit_vote + @vote = @communicate.vote + # 确保至少有2个选项显示在表单中 + @vote.vote_options.build if @vote.vote_options.empty? + @vote.vote_options.build if @vote.vote_options.size == 1 + end + + def update_vote + @vote = @communicate.vote + if @vote.update(vote_params) + redirect_to applet_admins_platform_communicates_path, notice: '投票更新成功' + else + render :edit_vote + end + end + private def get_communicate @@ -94,4 +130,19 @@ class Admins::PlatformCommunicatesController < Admins::BaseController def communicate_params params.require(:platform_communicate).permit! end + + def vote_params + params.require(:vote).permit( + :description, + :is_multiple_choice, + :max_choice_vote_options_count, + :due_at, + vote_options_attributes: [ + :id, + :title, + :order_index, + :_destroy + ] + ) + end end \ No newline at end of file diff --git a/app/models/vote.rb b/app/models/vote.rb index 9fce41dc2..96f987930 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -5,6 +5,8 @@ class Vote < ApplicationRecord has_many :vote_options, dependent: :destroy has_many :user_vote_options, through: :vote_options + accepts_nested_attributes_for :vote_options, allow_destroy: true + validates :max_choice_vote_options_count, numericality: { greater_than: 0 } def expired? diff --git a/app/views/admins/platform_communicates/_list.html.erb b/app/views/admins/platform_communicates/_list.html.erb index 3ca5c674a..c35c0b674 100644 --- a/app/views/admins/platform_communicates/_list.html.erb +++ b/app/views/admins/platform_communicates/_list.html.erb @@ -21,6 +21,11 @@ <%= c.order_index %> <%= c.tag_field.to_s.include?("活动") ? link_to(c.watchers_count, "/admins/platform_communicates/#{c.id}") : "--"%> + <% if c.vote.present? %> + <%= link_to "编辑投票", edit_vote_admins_platform_communicate_path(c), class: "action" %> + <% else %> + <%= link_to "新增投票", new_vote_admins_platform_communicate_path(c), class: "action" %> + <% end %> <%= link_to c.status ? "下架" : "上架", "/admins/platform_communicates/#{c.id}/online_switch.js", style: "#{c.status? ? 'color:#FF6800;' : ''}", method: :post, remote: true%> <%= link_to "编辑", edit_admins_platform_communicate_path(c), remote: true, class: "action" %> <%= link_to "删除", admins_platform_communicate_path(c), method: :delete, data:{confirm: "确认删除的吗?"}, class: "action" %> diff --git a/app/views/admins/platform_communicates/_vote_form.html.erb b/app/views/admins/platform_communicates/_vote_form.html.erb new file mode 100644 index 000000000..60a1793ac --- /dev/null +++ b/app/views/admins/platform_communicates/_vote_form.html.erb @@ -0,0 +1,312 @@ +<%= form_for @vote, url: {controller: "platform_communicates", action: "#{type}"} do |form| %> + <% if vote.errors.any? %> +
+
<%= pluralize(vote.errors.count, "error") %> 阻止了此投票的保存:
+
    + <% vote.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> + + +
+
+
基本信息
+
+
+ +
+ <%= form.label :description, "投票描述", class: "font-weight-bold" %> + <%= form.text_area :description, class: "form-control", rows: 3, placeholder: "输入投票描述(可选)" %> + 详细描述投票的目的和背景 +
+
+
+ + +
+
+
投票设置
+
+
+
+
+ <%= form.check_box :is_multiple_choice, class: "form-check-input", id: "is_multiple_choice" %> + <%= form.label :is_multiple_choice, "允许多选", class: "form-check-label font-weight-bold" %> +
+ 启用后用户可以选择多个选项 +
+ +
+ <%= form.label :max_choice_vote_options_count, "最多选择数", class: "font-weight-bold" %> + <%= form.number_field :max_choice_vote_options_count, class: "form-control", min: 2, max: 20, placeholder: "输入最多可选择的数量" %> + 设置用户最多可以选择的选项数量(2-20) +
+ +
+ <%= form.label :due_at, "结束时间", class: "font-weight-bold" %> +
+ <%= form.datetime_local_field :due_at, class: "form-control" %> +
+ +
+
+ 不设置则投票永久有效 +
+
+
+ + +
+
+
投票选项
+ +
+
+
+ <%= form.fields_for :vote_options do |vote_option_fields| %> +
+
+
+
+
+ <%= vote_option_fields.label :title, "选项名称", class: "font-weight-bold" %> + <%= vote_option_fields.text_field :title, class: "form-control", placeholder: "输入选项名称", required: true %> +
+
+
+
+ <%= vote_option_fields.label :order_index, "排序", class: "font-weight-bold" %> + <%= vote_option_fields.number_field :order_index, class: "form-control", min: 1, value: vote_option_fields.object.order_index || 1 %> +
+
+
+ <% unless vote_option_fields.object.persisted? %> + + <% end %> +
+
+ <% if vote_option_fields.object.persisted? %> +
+
+ <%= vote_option_fields.check_box :_destroy, class: "form-check-input" %> + <%= vote_option_fields.label :_destroy, "删除此选项", class: "form-check-label text-danger" %> +
+
+ <% end %> +
+
+ <% end %> +
+ +
+ 提示: 至少需要添加2个投票选项,最多支持50个选项。 +
+
+
+ + +
+ <%= form.submit vote.persisted? ? "更新投票" : "创建投票", class: "btn btn-primary btn-lg mr-2" %> + <%= link_to "取消", applet_admins_platform_communicates_path, class: "btn btn-secondary btn-lg" %> +
+<% end %> + + + + +<%= javascript_tag nonce: true do %> + // 简化的投票表单交互功能 +document.addEventListener('DOMContentLoaded', function() { + initVoteForm(); +}); + +// 如果使用 Turbolinks/Turbo +document.addEventListener('turbolinks:load', initVoteForm); +document.addEventListener('turbo:load', initVoteForm); + +function initVoteForm() { + console.log('初始化投票表单...'); + + // 防止重复初始化 + if (window.voteFormInitialized) { + console.log('投票表单已经初始化过,跳过'); + return; + } + window.voteFormInitialized = true; + + // 多选切换 + const multipleChoiceCheckbox = document.getElementById('is_multiple_choice'); + const maxChoicesField = document.getElementById('max_choices_field'); + + if (multipleChoiceCheckbox && maxChoicesField) { + function toggleMaxChoices() { + maxChoicesField.style.display = multipleChoiceCheckbox.checked ? 'block' : 'none'; + if (!multipleChoiceCheckbox.checked) { + const maxChoiceInput = document.getElementById('vote_max_choice_vote_options_count'); + if (maxChoiceInput) maxChoiceInput.value = ''; + } + } + + multipleChoiceCheckbox.addEventListener('change', toggleMaxChoices); + toggleMaxChoices(); // 初始状态 + } + + // 清除结束时间 + const clearEndsAtBtn = document.getElementById('clear_ends_at'); + if (clearEndsAtBtn) { + clearEndsAtBtn.addEventListener('click', function() { + const endsAtInput = document.getElementById('vote_ends_at'); + if (endsAtInput) endsAtInput.value = ''; + }); + } + + // 添加选项 - 关键修复:确保只绑定一次 + const addOptionButton = document.getElementById('add_vote_option'); + if (addOptionButton && !addOptionButton.hasListener) { + addOptionButton.hasListener = true; // 标记已绑定 + + addOptionButton.addEventListener('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + addNewOption(); + }); + } + + // 删除选项 - 使用事件委托 + const optionsContainer = document.getElementById('vote_options'); + if (optionsContainer) { + optionsContainer.addEventListener('click', function(event) { + if (event.target.closest('.remove-option')) { + event.preventDefault(); + event.target.closest('.vote-option-item').remove(); + } + }); + } + + // 表单验证 + const form = document.querySelector('.vote-form'); + if (form) { + form.addEventListener('submit', validateForm); + } +} + +// 添加新选项 +function addNewOption() { + console.log('执行添加选项函数'); + + const optionsContainer = document.getElementById('vote_options'); + const optionTemplate = document.getElementById('vote_option_template'); + + if (!optionsContainer || !optionTemplate) { + console.error('找不到选项容器或模板'); + return; + } + + // 计算新的索引和位置 + const currentOptions = optionsContainer.querySelectorAll('.vote-option-item'); + const newIndex = currentOptions.length; + const newPosition = getNextPosition(); + + const newOption = optionTemplate.content.cloneNode(true); + const html = newOption.querySelector('.vote-option-item').outerHTML; + let newHtml = html.replace(/INDEX/g, newIndex); + newHtml = newHtml.replace(/POSITION_VALUE/g, newPosition); + + optionsContainer.insertAdjacentHTML('beforeend', newHtml); + + console.log(`成功添加选项,索引: ${newIndex}, 位置: ${newPosition}`); +} + +// 获取下一个位置值 +function getNextPosition() { + const positionInputs = document.querySelectorAll('input[name*="position"]'); + let maxPosition = 0; + + positionInputs.forEach(input => { + const value = parseInt(input.value) || 0; + if (value > maxPosition) maxPosition = value; + }); + + return maxPosition + 1; +} + +// 表单验证 +function validateForm(event) { + const optionsContainer = document.getElementById('vote_options'); + const optionItems = optionsContainer ? optionsContainer.querySelectorAll('.vote-option-item') : []; + let validOptionCount = 0; + + // 计算有效的选项数量 + optionItems.forEach(item => { + const nameInput = item.querySelector('input[type="text"][name*="name"]'); + if (nameInput && nameInput.value.trim()) { + validOptionCount++; + } + }); + + if (validOptionCount < 2) { + event.preventDefault(); + alert('请至少填写2个有效的投票选项!'); + return false; + } + + // 验证选项名称是否重复 + const optionNames = []; + let hasDuplicate = false; + + optionItems.forEach(item => { + const nameInput = item.querySelector('input[type="text"][name*="name"]'); + if (nameInput && nameInput.value.trim()) { + const name = nameInput.value.trim().toLowerCase(); + if (optionNames.includes(name)) { + hasDuplicate = true; + nameInput.classList.add('is-invalid'); + } else { + optionNames.push(name); + nameInput.classList.remove('is-invalid'); + } + } + }); + + if (hasDuplicate) { + event.preventDefault(); + alert('存在重复的选项名称,请修改后重新提交!'); + return false; + } + + return true; +} +<% end %> \ No newline at end of file diff --git a/app/views/admins/platform_communicates/edit_vote.html.erb b/app/views/admins/platform_communicates/edit_vote.html.erb new file mode 100644 index 000000000..592e985b2 --- /dev/null +++ b/app/views/admins/platform_communicates/edit_vote.html.erb @@ -0,0 +1,15 @@ +<% content_for :page_title, "编辑投票" %> + +
+
+
+
+
编辑投票
+
+
+ <%= render 'vote_form', vote: @vote, type: "update_vote" %> +
+
+
+
+ diff --git a/app/views/admins/platform_communicates/new_vote.html.erb b/app/views/admins/platform_communicates/new_vote.html.erb new file mode 100644 index 000000000..a8dbe6962 --- /dev/null +++ b/app/views/admins/platform_communicates/new_vote.html.erb @@ -0,0 +1,15 @@ +<% content_for :page_title, "创建新投票" %> + +
+
+
+
+
创建新投票
+
+
+ <%= render 'vote_form', vote: @vote, type: "create_vote" %> +
+
+
+
+ diff --git a/config/routes.rb b/config/routes.rb index 4f0e6cb36..5c76c7482 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1226,6 +1226,10 @@ Rails.application.routes.draw do get :applet, on: :collection member do post :online_switch + get :new_vote + post :create_vote + get :edit_vote + patch :update_vote end end resources :platform_people From 869c874e21157b727cf7e113835767548f7db55c Mon Sep 17 00:00:00 2001 From: yystopf Date: Wed, 22 Oct 2025 17:16:01 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E6=9B=B4=E6=94=B9=EF=BC=9Amax=5Fchoice=5Fv?= =?UTF-8?q?ote=5Foptions=5Fcount=E5=8F=AF=E4=BB=A5=E4=B8=BAnil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/vote.rb | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/models/vote.rb b/app/models/vote.rb index 96f987930..82a639993 100644 --- a/app/models/vote.rb +++ b/app/models/vote.rb @@ -7,7 +7,16 @@ class Vote < ApplicationRecord accepts_nested_attributes_for :vote_options, allow_destroy: true - validates :max_choice_vote_options_count, numericality: { greater_than: 0 } + validates :max_choice_vote_options_count, + numericality: { + greater_than: 1, + less_than_or_equal_to: 20, + allow_nil: true + }, + if: :is_multiple_choice? + + validate :validate_multiple_choice_settings + def expired? due_at.present? && due_at < Time.current @@ -53,4 +62,14 @@ class Vote < ApplicationRecord return 0 if total.zero? (option_votes.to_f / total * 100).round(2) end + + def validate_multiple_choice_settings + if is_multiple_choice? && max_choice_vote_options_count.nil? + errors.add(:max_choice_vote_options_count, '多选投票必须设置最大选择数') + end + + if !is_multiple_choice? && max_choice_vote_options_count.present? + errors.add(:max_choice_vote_options_count, '单选投票不需要设置最大选择数') + end + end end