Commit 68348823 by Ivan

fix: 修复滚动加载 page 初始化大于 1 的情况

parent 24f5149a
class ApplicationController < ActionController::Base
include Authentication
# Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has.
allow_browser versions: :modern
# allow_browser versions: :modern
inertia_share do
{
auth: authenticated?.user&.as_json(only: [ :id, :name, :email_address, :admin ])
auth: authenticated?.user&.as_json(only: [ :id, :name, :email_address, :roles ])
}
end
end
class ImagesController < ApplicationController
# Allow unauthenticated access to index and show actions
# allow_unauthenticated_access only: [:index, :show, :search]
before_action :set_image, only: [:show, :edit, :update, :destroy, :approve, :reject]
before_action :authorize_admin, only: [:approve, :reject, :destroy]
before_action :set_image, only: [ :show, :edit, :update, :destroy, :approve, :reject ]
before_action :authorize_admin, only: [ :approve, :reject, :destroy ]
def index
@q = Image.includes(:user, :tags).ransack(params[:q])
@images = @q.result(distinct: true).with_attached_file.order(created_at: :desc).page(params[:page]).per(12)
render inertia: 'images/Index', props: {
images: @images.as_json(include: [:user, :tags], methods: [:file_url]),
filters: params[:q] || {}
}
respond_to do |format|
format.html do
render inertia: "images/Index", props: {
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url ]),
filters: params[:q] || {},
pagination: {
current_page: @images.current_page,
total_pages: @images.total_pages,
total_count: @images.total_count
}
}
end
format.json do
render json: {
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url ]),
pagination: {
current_page: @images.current_page,
total_pages: @images.total_pages,
total_count: @images.total_count
}
}
end
end
end
def show
render inertia: 'images/Show', props: {
image: @image.as_json(include: [:user, :tags], methods: [:file_url]),
render inertia: "images/Show", props: {
image: @image.as_json(include: [ :user, :tags ], methods: [ :file_url ]),
can_edit: Current.user == @image.user,
can_approve: Current.user&.admin?
}
......@@ -25,20 +45,20 @@ class ImagesController < ApplicationController
def new
@image = Image.new
render inertia: 'images/New'
render inertia: "images/New"
end
def create
@image = Current.user.images.new(image_params)
if @image.save
if params[:tags].present?
@image.add_tags(params[:tags].split(','))
@image.add_tags(params[:tags].split(","))
end
redirect_to image_path(@image), notice: 'Image was successfully uploaded and is pending review.'
redirect_to image_path(@image), notice: "Image was successfully uploaded and is pending review."
else
render inertia: 'images/New', props: {
image: @image.as_json(methods: [:errors]),
render inertia: "images/New", props: {
image: @image.as_json(methods: [ :errors ]),
errors: @image.errors
}, status: :unprocessable_entity
end
......@@ -46,24 +66,24 @@ class ImagesController < ApplicationController
def edit
authorize_user
render inertia: 'images/Edit', props: {
image: @image.as_json(include: [:tags], methods: [:file_url]),
tags: @image.tags.pluck(:name).join(', ')
render inertia: "images/Edit", props: {
image: @image.as_json(include: [ :tags ], methods: [ :file_url ]),
tags: @image.tags.pluck(:name).join(", ")
}
end
def update
authorize_user
if @image.update(image_params)
if params[:tags].present?
@image.tags.clear
@image.add_tags(params[:tags].split(','))
@image.add_tags(params[:tags].split(","))
end
redirect_to image_path(@image), notice: 'Image was successfully updated.'
redirect_to image_path(@image), notice: "Image was successfully updated."
else
render inertia: 'images/Edit', props: {
image: @image.as_json(include: [:tags], methods: [:file_url, :errors]),
render inertia: "images/Edit", props: {
image: @image.as_json(include: [ :tags ], methods: [ :file_url, :errors ]),
tags: params[:tags],
errors: @image.errors
}, status: :unprocessable_entity
......@@ -72,49 +92,49 @@ class ImagesController < ApplicationController
def destroy
@image.destroy
redirect_to images_path, notice: 'Image was successfully deleted.'
redirect_to images_path, notice: "Image was successfully deleted."
end
def search
@q = Image.approved.includes(:user, :tags).ransack(params[:q])
@images = @q.result(distinct: true).with_attached_file.order(created_at: :desc).page(params[:page]).per(12)
render inertia: 'images/Search', props: {
images: @images.as_json(include: [:user, :tags], methods: [:file_url]),
render inertia: "images/Search", props: {
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url ]),
filters: params[:q] || {},
tags: Tag.all.pluck(:name)
}
end
def approve
@image.approved!
redirect_to image_path(@image), notice: 'Image was successfully approved.'
redirect_to image_path(@image), notice: "Image was successfully approved."
end
def reject
@image.rejected!
redirect_to image_path(@image), notice: 'Image was rejected.'
redirect_to image_path(@image), notice: "Image was rejected."
end
private
def set_image
@image = Image.find(params[:id])
end
def image_params
params.require(:image).permit(:title, :file)
end
def authorize_user
unless Current.user == @image.user
redirect_to images_path, alert: 'You are not authorized to perform this action.'
redirect_to images_path, alert: "You are not authorized to perform this action."
end
end
def authorize_admin
unless Current.user&.admin?
redirect_to images_path, alert: 'You are not authorized to perform this action.'
redirect_to images_path, alert: "You are not authorized to perform this action."
end
end
end
......@@ -166,7 +166,7 @@ export default function Layout({ children, user }) {
Upload
</Link>
)}
{user?.role === 'admin' && (
{user?.roles?.includes('admin') && (
<Link
href="/admin"
className="border-transparent text-gray-500 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-700 block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
......
import { useState } from 'react'
import { Head, useForm } from '@inertiajs/react'
import { Head, useForm, router } from '@inertiajs/react'
import Layout from '../Layout'
import * as Form from '@radix-ui/react-form'
export default function New({ auth, errors = {} }) {
const [preview, setPreview] = useState(null)
const { data, setData, post, processing } = useForm({
const { data, setData, processing } = useForm({
title: '',
file: null,
tags: '',
......@@ -13,7 +13,7 @@ export default function New({ auth, errors = {} }) {
const handleSubmit = (e) => {
e.preventDefault()
post('/images')
router.post('/images', { image: data })
}
const handleFileChange = (e) => {
......
......@@ -16,6 +16,19 @@ class Image < ApplicationRecord
scope :approved, -> { where(status: :approved) }
scope :rejected, -> { where(status: :rejected) }
def self.ransackable_attributes(auth_object = nil)
[ "created_at", "id", "id_value", "status", "title", "updated_at", "user_id" ]
end
def self.ransackable_associations(auth_object = nil)
["image_tags", "tags", "user"]
end
# Returns the URL for the attached file
def file_url
file.attached? ? Rails.application.routes.url_helpers.rails_blob_path(file, only_path: true) : nil
end
# Method to add tags to an image
def add_tags(tag_names)
tag_names.each do |name|
......
......@@ -6,6 +6,10 @@ class Tag < ApplicationRecord
before_save :downcase_name
def self.ransackable_attributes(auth_object = nil)
["created_at", "id", "id_value", "status", "name", "updated_at", "user_id"]
end
private
def downcase_name
......
......@@ -14,13 +14,39 @@ class User < ApplicationRecord
attribute :notify_on_new_login, :boolean, default: true
attribute :max_sessions, :integer, default: 5
# Admin role
# enum role: { user: 0, admin: 1 }, _default: :user
# 确保 roles 字段始终是数组
# attribute :roles, :json, default: -> { [] }
serialize :roles, coder: JSON, yaml: true
# 定义可用的角色
AVAILABLE_ROLES = %w[admin editor viewer]
# 添加角色
def add_role(role)
return unless AVAILABLE_ROLES.include?(role.to_s)
current_roles = (roles || []).map(&:to_s)
current_roles << role.to_s unless current_roles.include?(role.to_s)
update(roles: current_roles)
end
# 移除角色
def remove_role(role)
current_roles = (roles || []).map(&:to_s)
current_roles.delete(role.to_s)
update(roles: current_roles)
end
# 检查是否有指定角色
def has_role?(role)
Rails.logger.info("roles#{roles.class}")
(roles || []).map(&:to_s).include?(role.to_s)
end
# 向后兼容的 admin? 方法
def admin?
role == "admin"
has_role?("admin")
end
def security_settings
{
require_two_factor: require_two_factor,
......@@ -29,20 +55,20 @@ class User < ApplicationRecord
max_sessions: max_sessions
}
end
def enforce_max_sessions
return unless max_sessions > 0
# Get all sessions except the current one, ordered by last activity
other_sessions = sessions.where.not(id: Current.session&.id).order(updated_at: :desc)
# If we have more sessions than allowed, destroy the oldest ones
if other_sessions.count >= max_sessions
sessions_to_remove = other_sessions.offset(max_sessions - 1)
sessions_to_remove.destroy_all
end
end
def notify_on_new_login?
notify_on_new_login
end
......
# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
def change
# Use Active Record's configured type for primary and foreign keys
primary_key_type, foreign_key_type = primary_and_foreign_key_types
create_table :active_storage_blobs, id: primary_key_type do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :key ], unique: true
end
create_table :active_storage_attachments, id: primary_key_type do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
t.references :blob, null: false, type: foreign_key_type
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
create_table :active_storage_variant_records, id: primary_key_type do |t|
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
t.string :variation_digest, null: false
t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end
private
def primary_and_foreign_key_types
config = Rails.configuration.generators
setting = config.options[config.orm][:primary_key_type]
primary_key_type = setting || :primary_key
foreign_key_type = setting || :bigint
[ primary_key_type, foreign_key_type ]
end
end
class AddRolesToUsers < ActiveRecord::Migration[8.0]
def change
add_column :users, :roles, :string, default: '[]'
# 如果之前有 role 字段,我们需要将其移除
# 注意:这里假设之前没有 role 字段,如果有的话会自动处理
remove_column :users, :role if column_exists?(:users, :role)
end
end
......@@ -10,7 +10,35 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_03_08_064523) do
ActiveRecord::Schema[8.0].define(version: 2025_03_08_073302) do
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.bigint "record_id", null: false
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
create_table "active_storage_blobs", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.string "service_name", null: false
t.bigint "byte_size", null: false
t.string "checksum"
t.datetime "created_at", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
create_table "active_storage_variant_records", force: :cascade do |t|
t.bigint "blob_id", null: false
t.string "variation_digest", null: false
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end
create_table "image_tags", force: :cascade do |t|
t.integer "image_id", null: false
t.integer "tag_id", null: false
......@@ -50,9 +78,12 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_08_064523) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name"
t.string "roles", default: "[]"
t.index ["email_address"], name: "index_users_on_email_address", unique: true
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "image_tags", "images"
add_foreign_key "image_tags", "tags"
add_foreign_key "images", "users"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment