Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
I
img-manager
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Ivan Lan
img-manager
Commits
68348823
Commit
68348823
authored
Mar 08, 2025
by
Ivan
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: 修复滚动加载 page 初始化大于 1 的情况
parent
24f5149a
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
407 additions
and
88 deletions
+407
-88
application_controller.rb
app/controllers/application_controller.rb
+2
-2
images_controller.rb
app/controllers/images_controller.rb
+60
-40
Layout.jsx
app/frontend/pages/Layout.jsx
+1
-1
Index.jsx
app/frontend/pages/images/Index.jsx
+192
-33
New.jsx
app/frontend/pages/images/New.jsx
+3
-3
image.rb
app/models/image.rb
+13
-0
tag.rb
app/models/tag.rb
+4
-0
user.rb
app/models/user.rb
+34
-8
20250308072606_create_active_storage_tables.active_storage.rb
...0308072606_create_active_storage_tables.active_storage.rb
+57
-0
20250308073302_add_roles_to_users.rb
db/migrate/20250308073302_add_roles_to_users.rb
+9
-0
schema.rb
db/schema.rb
+32
-1
No files found.
app/controllers/application_controller.rb
View file @
68348823
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
app/controllers/images_controller.rb
View file @
68348823
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
app/frontend/pages/Layout.jsx
View file @
68348823
...
...
@@ -166,7 +166,7 @@ export default function Layout({ children, user }) {
Upload
</
Link
>
)
}
{
user
?.
role
===
'admin'
&&
(
{
user
?.
role
s
?.
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"
...
...
app/frontend/pages/images/Index.jsx
View file @
68348823
import
{
useState
}
from
'react'
import
{
Head
,
Link
}
from
'@inertiajs/react'
import
{
useState
,
useEffect
,
useRef
,
useCallback
}
from
'react'
import
{
Head
,
Link
,
router
}
from
'@inertiajs/react'
import
Layout
from
'../Layout'
import
*
as
Tabs
from
'@radix-ui/react-tabs'
import
*
as
Form
from
'@radix-ui/react-form'
export
default
function
Index
({
images
,
filters
,
auth
})
{
export
default
function
Index
({
images
,
filters
,
pagination
,
auth
})
{
const
[
allImages
,
setAllImages
]
=
useState
(
images
||
[])
const
[
page
,
setPage
]
=
useState
(
pagination
?.
current_page
||
1
)
const
[
maxVisiblePage
,
setMaxVisiblePage
]
=
useState
(
0
)
console
.
log
(
'auth'
,
auth
)
const
[
loading
,
setLoading
]
=
useState
(
false
)
const
[
hasMore
,
setHasMore
]
=
useState
(
pagination
?.
current_page
<
pagination
?.
total_pages
)
const
[
loadedPageMap
,
setLoadedPageMap
]
=
useState
({})
const
bottomObserver
=
useRef
()
const
[
searchParams
,
setSearchParams
]
=
useState
({
title_cont
:
filters
.
title_cont
||
''
,
tags_name_cont
:
filters
.
tags_name_cont
||
''
,
...
...
@@ -19,8 +25,131 @@ console.log('auth', auth)
setSearchParams
({
...
searchParams
,
[
name
]:
value
})
}
// const loadMoreImages = useCallback(() => {
// if (loading || !hasMore) return
// try {
// const nextPage = page + 1
// if (loadedPageMap[nextPage]) return
// setLoading(true)
// // Use Inertia's router.visit with replace option to avoid changing URL
// router.visit(
// '/images',
// {
// method: 'get',
// data: { page: nextPage },
// preserveState: true,
// preserveScroll: true,
// replace: true, // Replace current URL instead of adding to history
// only: ['images', 'pagination'],
// onSuccess: (response) => {
// // Append new images to existing ones
// setPage(response.props.pagination.current_page)
// setHasMore(response.props.pagination.current_page < response.props.pagination.total_pages)
// setLoading(false)
// setLoadedPageMap({ ...loadedPageMap, [response.props.pagination.current_page]: response.props.images })
// },
// onError: () => {
// console.error('Error loading more images')
// setLoading(false)
// }
// }
// )
// } catch (error) {
// console.error('Error loading more images:', error)
// setLoading(false)
// }
// }, [page, loading, hasMore])
const
loadPage
=
((
currentPage
)
=>
{
if
(
loadedPageMap
[
currentPage
])
{
return
loadedPageMap
[
currentPage
];
}
try
{
setLoading
(
true
)
router
.
visit
(
'/images'
,
{
method
:
'get'
,
data
:
{
page
:
currentPage
},
preserveState
:
true
,
preserveScroll
:
true
,
replace
:
true
,
only
:
[
'images'
,
'pagination'
],
onSuccess
:
(
response
)
=>
{
// Prepend new images to existing ones
setPage
(
response
.
props
.
pagination
.
current_page
)
setLoading
(
false
)
setHasMore
(
maxVisiblePage
<
response
.
props
.
pagination
.
total_pages
)
setLoadedPageMap
({...
loadedPageMap
,
[
response
.
props
.
pagination
.
current_page
]:
response
.
props
.
images
})
},
onError
:
()
=>
{
console
.
error
(
'Error loading images'
)
setLoading
(
false
)
}
})
}
catch
(
error
)
{
console
.
error
(
'Error loading images:'
,
error
)
setLoading
(
false
)
}
})
const
setVisibleImages
=
async
(
expectPage
)
=>
{
if
(
!
loadedPageMap
[
expectPage
]
&&
expectPage
>
maxVisiblePage
)
{
await
loadPage
(
expectPage
)
}
setAllImages
([...
allImages
,
...
loadedPageMap
[
expectPage
]])
setMaxVisiblePage
(
expectPage
)
}
useEffect
(()
=>
{
if
(
page
>=
1
&&
!
loadedPageMap
[
1
])
{
loadPage
(
1
)
setMaxVisiblePage
(
1
)
}
else
{
loadPage
(
page
)
setVisibleImages
(
page
)
}
},
[
page
,
loadedPageMap
,
maxVisiblePage
])
const
loadMoreImages
=
useCallback
(()
=>
{
loadPage
(
page
+
1
)
},
[
page
,
loadPage
])
// Observer for the last image (bottom of the page)
const
lastImageElementRef
=
useCallback
(
node
=>
{
if
(
loading
)
return
if
(
bottomObserver
.
current
)
bottomObserver
.
current
.
disconnect
()
bottomObserver
.
current
=
new
IntersectionObserver
(
entries
=>
{
if
(
entries
[
0
].
isIntersecting
&&
hasMore
)
{
loadMoreImages
()
}
},
{
threshold
:
0.1
,
rootMargin
:
'100px'
})
if
(
node
)
bottomObserver
.
current
.
observe
(
node
)
},
[
loading
,
hasMore
,
loadMoreImages
])
// Observer for the first image (top of the page)
// const firstImageElementRef = useCallback(node => {
// if (loading) return
// if (topObserver.current) topObserver.current.disconnect()
// topObserver.current = new IntersectionObserver(entries => {
// if (entries[0].isIntersecting && hasPrevious && page > 1) {
// loadPreviousImages()
// }
// }, { threshold: 0.5, rootMargin: '200px' })
// if (node) topObserver.current.observe(node)
// }, [loading, hasPrevious, page, loadPreviousImages])
return
(
<
Layout
user=
{
auth
.
user
}
>
<
Layout
user=
{
auth
}
>
<
Head
title=
"My Images"
/>
<
div
className=
"bg-white shadow overflow-hidden sm:rounded-lg"
>
<
div
className=
"px-4 py-5 sm:px-6 flex justify-between items-center"
>
...
...
@@ -57,15 +186,26 @@ console.log('auth', auth)
</
Tabs
.
List
>
<
Tabs
.
Content
value=
"all"
className=
"focus:outline-none"
>
{
i
mages
.
length
>
0
?
(
{
allI
mages
.
length
>
0
?
(
<
div
className=
"grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"
>
{
images
.
map
((
image
)
=>
(
{
loading
&&
(
<
div
className=
"col-span-full text-center py-4"
>
<
div
className=
"inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-r-transparent align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite]"
role=
"status"
>
<
span
className=
"!absolute !-m-px !h-px !w-px !overflow-hidden !whitespace-nowrap !border-0 !p-0 ![clip:rect(0,0,0,0)]"
>
Loading...
</
span
>
</
div
>
</
div
>
)
}
{
allImages
.
map
((
image
,
index
)
=>
(
<
div
key=
{
image
.
id
}
ref=
{
index
===
allImages
.
length
-
1
?
lastImageElementRef
:
null
}
className=
"bg-white overflow-hidden shadow rounded-lg"
>
<
div
className=
"relative pb-[75%]"
>
<
img
loading=
"lazy"
src=
{
image
.
file_url
}
alt=
{
image
.
title
}
className=
"absolute h-full w-full object-cover"
...
...
@@ -115,34 +255,53 @@ console.log('auth', auth)
</
div
>
)
:
(
<
div
className=
"text-center py-12"
>
<
svg
className=
"mx-auto h-12 w-12 text-gray-400"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
aria
-
hidden=
"true"
>
<
path
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
{
2
}
d=
"M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</
svg
>
<
h3
className=
"mt-2 text-sm font-medium text-gray-900"
>
No images
</
h3
>
<
p
className=
"mt-1 text-sm text-gray-500"
>
Get started by uploading a new image.
</
p
>
<
div
className=
"mt-6"
>
<
Link
href=
"/images/new"
className=
"inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
{
loading
&&
allImages
.
length
===
0
?
(
<
div
className=
"flex justify-center items-center"
>
<
svg
className=
"animate-spin h-8 w-8 text-indigo-600"
xmlns=
"http://www.w3.org/2000/svg"
fill=
"none"
viewBox=
"0 0 24 24"
>
<
circle
className=
"opacity-25"
cx=
"12"
cy=
"12"
r=
"10"
stroke=
"currentColor"
strokeWidth=
"4"
></
circle
>
<
path
className=
"opacity-75"
fill=
"currentColor"
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></
path
>
</
svg
>
</
div
>
)
:
(
<
div
>
<
svg
className=
"mx-auto h-12 w-12 text-gray-400"
fill=
"none"
viewBox=
"0 0 24 24"
stroke=
"currentColor"
aria
-
hidden=
"true"
>
Upload New Image
</
Link
>
<
path
strokeLinecap=
"round"
strokeLinejoin=
"round"
strokeWidth=
{
2
}
d=
"M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</
svg
>
<
h3
className=
"mt-2 text-sm font-medium text-gray-900"
>
No images
</
h3
>
<
p
className=
"mt-1 text-sm text-gray-500"
>
Get started by uploading a new image.
</
p
>
<
div
className=
"mt-6"
>
<
Link
href=
"/images/new"
className=
"inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Upload New Image
</
Link
>
</
div
>
</
div
>
)
}
</
div
>
)
}
{
loading
&&
allImages
.
length
>
0
&&
(
<
div
className=
"flex justify-center mt-6 pb-6"
>
<
svg
className=
"animate-spin h-8 w-8 text-indigo-600"
xmlns=
"http://www.w3.org/2000/svg"
fill=
"none"
viewBox=
"0 0 24 24"
>
<
circle
className=
"opacity-25"
cx=
"12"
cy=
"12"
r=
"10"
stroke=
"currentColor"
strokeWidth=
"4"
></
circle
>
<
path
className=
"opacity-75"
fill=
"currentColor"
d=
"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></
path
>
</
svg
>
</
div
>
)
}
</
Tabs
.
Content
>
...
...
app/frontend/pages/images/New.jsx
View file @
68348823
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
,
p
ost
,
p
rocessing
}
=
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
)
=>
{
...
...
app/models/image.rb
View file @
68348823
...
...
@@ -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
|
...
...
app/models/tag.rb
View file @
68348823
...
...
@@ -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
...
...
app/models/user.rb
View file @
68348823
...
...
@@ -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
...
...
db/migrate/20250308072606_create_active_storage_tables.active_storage.rb
0 → 100644
View file @
68348823
# 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
db/migrate/20250308073302_add_roles_to_users.rb
0 → 100644
View file @
68348823
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
db/schema.rb
View file @
68348823
...
...
@@ -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"
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment