Commit 63b84398 by Ivan

feat: 时间范围过滤器

parent 82241f04
...@@ -13,6 +13,7 @@ class Admin::ImagesController < ApplicationController ...@@ -13,6 +13,7 @@ class Admin::ImagesController < ApplicationController
tag: @tag.as_json(only: [ :id, :name, :catalog ], methods: [ :images_count ]), tag: @tag.as_json(only: [ :id, :name, :catalog ], methods: [ :images_count ]),
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]), images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]),
filters: params[:q] || {}, filters: params[:q] || {},
tags: Tag.all.to_json,
pagination: { pagination: {
current_page: @images.current_page, current_page: @images.current_page,
total_pages: @images.total_pages, total_pages: @images.total_pages,
...@@ -32,6 +33,7 @@ class Admin::ImagesController < ApplicationController ...@@ -32,6 +33,7 @@ class Admin::ImagesController < ApplicationController
total_pages: @images.total_pages, total_pages: @images.total_pages,
total_count: @images.total_count total_count: @images.total_count
}, },
tags: Tag.all.to_json,
pending_count: Image.pending.count, pending_count: Image.pending.count,
approved_count: Image.approved.count, approved_count: Image.approved.count,
rejected_count: Image.rejected.count rejected_count: Image.rejected.count
...@@ -74,7 +76,7 @@ class Admin::ImagesController < ApplicationController ...@@ -74,7 +76,7 @@ class Admin::ImagesController < ApplicationController
if params[:image] && params[:image][:tag_ids].present? if params[:image] && params[:image][:tag_ids].present?
@image.set_tags_by_ids(params[:image][:tag_ids]) @image.set_tags_by_ids(params[:image][:tag_ids])
end end
# Add the tag if we're creating from a tag context # Add the tag if we're creating from a tag context
if params[:tag_id].present? && @tag if params[:tag_id].present? && @tag
@image.tags << @tag unless @image.tags.include?(@tag) @image.tags << @tag unless @image.tags.include?(@tag)
......
...@@ -15,6 +15,7 @@ class ImagesController < ApplicationController ...@@ -15,6 +15,7 @@ class ImagesController < ApplicationController
tag: @tag.as_json(only: [ :id, :name, :catalog ], methods: [ :images_count ]), tag: @tag.as_json(only: [ :id, :name, :catalog ], methods: [ :images_count ]),
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]), images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]),
filters: params[:q] || {}, filters: params[:q] || {},
tags: Tag.all.to_json,
pagination: { pagination: {
current_page: @images.current_page, current_page: @images.current_page,
total_pages: @images.total_pages, total_pages: @images.total_pages,
...@@ -29,6 +30,7 @@ class ImagesController < ApplicationController ...@@ -29,6 +30,7 @@ class ImagesController < ApplicationController
render inertia: "images/Index", props: { render inertia: "images/Index", props: {
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]), images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]),
filters: params[:q] || {}, filters: params[:q] || {},
tags: Tag.all.to_json,
pagination: { pagination: {
current_page: @images.current_page, current_page: @images.current_page,
total_pages: @images.total_pages, total_pages: @images.total_pages,
......
...@@ -10,7 +10,7 @@ export default function ImageUploadForm({ ...@@ -10,7 +10,7 @@ export default function ImageUploadForm({
onSubmit, onSubmit,
submitButtonText = '上传图片', submitButtonText = '上传图片',
processingButtonText = '上传中...', processingButtonText = '上传中...',
showTagsField = false, showTagsField = true,
isEdit = false, isEdit = false,
image = null, image = null,
tags = [], tags = [],
......
...@@ -12,34 +12,74 @@ export default function TagSearchInput({ value, onChange, allTags = {}, placehol ...@@ -12,34 +12,74 @@ export default function TagSearchInput({ value, onChange, allTags = {}, placehol
const [filteredTags, setFilteredTags] = useState({}) const [filteredTags, setFilteredTags] = useState({})
const [internalValue, setInternalValue] = useState(value || []) const [internalValue, setInternalValue] = useState(value || [])
// 只在组件初始化或外部value变化时更新selectedTags // 处理标签数据和选中标签
useEffect(() => { useEffect(() => {
if (isEqual(value, internalValue)) { // 处理标签数据,支持数组或已分组的对象格式
setInternalValue(value || []) let processedTags = {}
if (value) { let tagsData = allTags
const tagIds = value
// 如果是 JSON 字符串,先解析
if (typeof allTags === 'string') {
try {
tagsData = JSON.parse(allTags)
} catch (e) {
console.error('Failed to parse tags JSON:', e)
tagsData = []
}
}
// 如果传入的是数组,需要按catalog属性分组
if (Array.isArray(tagsData)) {
// 按catalog属性分组标签
const groupedByCategory = {}
tagsData.forEach(tag => {
const catalog = tag.catalog || ''
if (!groupedByCategory[catalog]) {
groupedByCategory[catalog] = []
}
groupedByCategory[catalog].push(tag)
})
processedTags = groupedByCategory
} else {
// 如果已经是分组好的对象,直接使用
processedTags = tagsData || {}
}
// 更新分组后的标签
setAvailableTags(processedTags)
// 处理选中的标签
// 使用 JSON.stringify 比较数组,因于 isEqual 可能不总是可靠的用于空数组
const valueChanged = JSON.stringify(value) !== JSON.stringify(internalValue);
// 如果值发生变化或强制更新,则更新内部值
if (valueChanged) {
const newValue = value || [];
setInternalValue(newValue);
// 如果有标签 ID,则找出对应的标签对象
if (newValue && newValue.length > 0) {
const tagIds = newValue;
// 从所有可用标签中找出已选标签 // 从所有可用标签中找出已选标签
const selected = [] const selected = [];
Object.values(allTags).forEach(tagGroup => { Object.values(processedTags).forEach(tagGroup => {
tagGroup.forEach(tag => { tagGroup.forEach(tag => {
if (tagIds.includes(tag.id)) { if (tagIds.includes(tag.id)) {
selected.push(tag) selected.push(tag);
} }
}) });
}) });
setSelectedTags(selected) setSelectedTags(selected);
} else { } else {
setSelectedTags([]) // 如果标签 ID 为空,则清空选中标签
setSelectedTags([]);
} }
} }
}, [value, allTags]) }, [allTags, value, internalValue])
// 使用传入的标签数据
useEffect(() => {
setAvailableTags(allTags || {})
}, [allTags])
// 根据搜索词过滤标签 // 根据搜索词过滤标签
useEffect(() => { useEffect(() => {
...@@ -97,7 +137,7 @@ export default function TagSearchInput({ value, onChange, allTags = {}, placehol ...@@ -97,7 +137,7 @@ export default function TagSearchInput({ value, onChange, allTags = {}, placehol
} }
return ( return (
<div className="relative"> <div className="relative mt-1">
<Popover.Root open={isOpen} onOpenChange={setIsOpen} modal={false}> <Popover.Root open={isOpen} onOpenChange={setIsOpen} modal={false}>
<Popover.Trigger asChild> <Popover.Trigger asChild>
<div className="flex flex-wrap gap-2 min-h-[2.5rem] p-2 border border-gray-300 rounded-md bg-white cursor-pointer"> <div className="flex flex-wrap gap-2 min-h-[2.5rem] p-2 border border-gray-300 rounded-md bg-white cursor-pointer">
...@@ -123,7 +163,7 @@ export default function TagSearchInput({ value, onChange, allTags = {}, placehol ...@@ -123,7 +163,7 @@ export default function TagSearchInput({ value, onChange, allTags = {}, placehol
))} ))}
{selectedTags.length === 0 && ( {selectedTags.length === 0 && (
<div className="text-gray-400 text-sm">{placeholder}</div> <div className="text-gray-400 text-sm pl-1">{placeholder}</div>
)} )}
</div> </div>
</Popover.Trigger> </Popover.Trigger>
...@@ -172,7 +212,7 @@ export default function TagSearchInput({ value, onChange, allTags = {}, placehol ...@@ -172,7 +212,7 @@ export default function TagSearchInput({ value, onChange, allTags = {}, placehol
{catalog || '未分类'} {catalog || '未分类'}
</h3> </h3>
<div className="space-y-1"> <div className="space-y-1">
{tags.map(tag => { {Array.isArray(tags) ? tags.map(tag => {
const isSelected = selectedTags.some(t => t.id === tag.id) const isSelected = selectedTags.some(t => t.id === tag.id)
return ( return (
<button <button
...@@ -194,7 +234,7 @@ export default function TagSearchInput({ value, onChange, allTags = {}, placehol ...@@ -194,7 +234,7 @@ export default function TagSearchInput({ value, onChange, allTags = {}, placehol
)} )}
</button> </button>
) )
})} }) : <div className="text-sm text-gray-500 py-1">无可用标签</div>}
</div> </div>
</div> </div>
))} ))}
......
...@@ -13,27 +13,6 @@ export default function ComImageEdit({ image, tags, path, isAdmin = false }) { ...@@ -13,27 +13,6 @@ export default function ComImageEdit({ image, tags, path, isAdmin = false }) {
title: image.title || '', title: image.title || '',
tag_ids: initialTagIds, // 使用tag_ids而不是tags tag_ids: initialTagIds, // 使用tag_ids而不是tags
}) })
// 获取所有标签数据并按目录分组
const [groupedTags, setGroupedTags] = useState({})
// 使用控制器返回的所有标签数据并按catalog属性分组
useEffect(() => {
if (Array.isArray(tags)) {
// 按catalog属性分组标签
const groupedByCategory = {}
tags.forEach(tag => {
const catalog = tag.catalog || ''
if (!groupedByCategory[catalog]) {
groupedByCategory[catalog] = []
}
groupedByCategory[catalog].push(tag)
})
setGroupedTags(groupedByCategory)
}
}, [tags])
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault() e.preventDefault()
...@@ -85,7 +64,7 @@ export default function ComImageEdit({ image, tags, path, isAdmin = false }) { ...@@ -85,7 +64,7 @@ export default function ComImageEdit({ image, tags, path, isAdmin = false }) {
<TagSearchInput <TagSearchInput
value={data.tag_ids} value={data.tag_ids}
onChange={(value) => setData('tag_ids', value)} onChange={(value) => setData('tag_ids', value)}
allTags={groupedTags} allTags={tags}
placeholder="点击搜索图标添加标签" placeholder="点击搜索图标添加标签"
/> />
</Form.Control> </Form.Control>
......
...@@ -2,7 +2,7 @@ import { Head } from '@inertiajs/react' ...@@ -2,7 +2,7 @@ import { Head } from '@inertiajs/react'
import Layout from '../../Layout' import Layout from '../../Layout'
import ComImageIndex from '../../../components/images/ComImageIndex' import ComImageIndex from '../../../components/images/ComImageIndex'
export default function AdminImagesIndex({ auth, images, pagination, filters }) { export default function AdminImagesIndex({ auth, images, pagination, filters, tags }) {
return ( return (
<Layout user={auth}> <Layout user={auth}>
<Head title="图片管理" /> <Head title="图片管理" />
...@@ -15,6 +15,7 @@ export default function AdminImagesIndex({ auth, images, pagination, filters }) ...@@ -15,6 +15,7 @@ export default function AdminImagesIndex({ auth, images, pagination, filters })
pagination={pagination} pagination={pagination}
filters={filters} filters={filters}
showUserName={true} showUserName={true}
allTags={tags}
/> />
</Layout> </Layout>
) )
......
...@@ -2,7 +2,7 @@ import { Head, Link } from '@inertiajs/react' ...@@ -2,7 +2,7 @@ import { Head, Link } from '@inertiajs/react'
import Layout from '../../../Layout' import Layout from '../../../Layout'
import ComImageIndex from '../../../../components/images/ComImageIndex' import ComImageIndex from '../../../../components/images/ComImageIndex'
export default function TagsImagesIndex({ auth, tag, images, pagination, filters }) { export default function TagsImagesIndex({ auth, tag, images, pagination, filters, tags }) {
return ( return (
<Layout user={auth}> <Layout user={auth}>
<Head title={`${tag.name} - 标签图片`} /> <Head title={`${tag.name} - 标签图片`} />
...@@ -15,6 +15,7 @@ export default function TagsImagesIndex({ auth, tag, images, pagination, filters ...@@ -15,6 +15,7 @@ export default function TagsImagesIndex({ auth, tag, images, pagination, filters
pagination={pagination} pagination={pagination}
filters={filters} filters={filters}
showUserName={true} showUserName={true}
allTags={tags}
backLink={{ backLink={{
url: '/admin/tags', url: '/admin/tags',
label: '返回标签列表' label: '返回标签列表'
......
...@@ -2,7 +2,7 @@ import Layout from '../Layout' ...@@ -2,7 +2,7 @@ import Layout from '../Layout'
import ComImageIndex from '../../components/images/ComImageIndex' import ComImageIndex from '../../components/images/ComImageIndex'
import { Head } from '@inertiajs/react' import { Head } from '@inertiajs/react'
export default function Index({ auth, images, pagination, filters }) { export default function Index({ auth, images, pagination, filters, tags }) {
return ( return (
<Layout user={auth}> <Layout user={auth}>
<Head title="我的图片" /> <Head title="我的图片" />
...@@ -14,6 +14,7 @@ export default function Index({ auth, images, pagination, filters }) { ...@@ -14,6 +14,7 @@ export default function Index({ auth, images, pagination, filters }) {
images={images} images={images}
pagination={pagination} pagination={pagination}
filters={filters} filters={filters}
allTags={tags}
/> />
</Layout> </Layout>
) )
......
...@@ -2,7 +2,7 @@ import { Head, Link } from '@inertiajs/react' ...@@ -2,7 +2,7 @@ import { Head, Link } from '@inertiajs/react'
import Layout from '../../Layout' import Layout from '../../Layout'
import ComImageIndex from '../../../components/images/ComImageIndex' import ComImageIndex from '../../../components/images/ComImageIndex'
export default function Index({ tag, images, pagination, auth, filters = {} }) { export default function Index({ tag, images, pagination, auth, filters = {}, tags }) {
return ( return (
<Layout user={auth}> <Layout user={auth}>
<Head title={`标签: ${tag.name} - 图片列表`} /> <Head title={`标签: ${tag.name} - 图片列表`} />
...@@ -14,6 +14,7 @@ export default function Index({ tag, images, pagination, auth, filters = {} }) { ...@@ -14,6 +14,7 @@ export default function Index({ tag, images, pagination, auth, filters = {} }) {
images={images} images={images}
pagination={pagination} pagination={pagination}
filters={filters} filters={filters}
allTags={tags}
backLink={{ backLink={{
url: '/tags', url: '/tags',
label: '返回标签列表' label: '返回标签列表'
......
...@@ -16,6 +16,8 @@ class Image < ApplicationRecord ...@@ -16,6 +16,8 @@ class Image < ApplicationRecord
scope :approved, -> { where(status: :approved) } scope :approved, -> { where(status: :approved) }
scope :rejected, -> { where(status: :rejected) } scope :rejected, -> { where(status: :rejected) }
ransacker :status, formatter: ->(value) { statuses[value] }
def self.ransackable_attributes(auth_object = nil) def self.ransackable_attributes(auth_object = nil)
[ "created_at", "id", "id_value", "status", "title", "updated_at", "user_id" ] [ "created_at", "id", "id_value", "status", "title", "updated_at", "user_id" ]
end end
......
...@@ -25,7 +25,9 @@ ...@@ -25,7 +25,9 @@
"@tailwindcss/vite": "^4.0.12", "@tailwindcss/vite": "^4.0.12",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20", "autoprefixer": "^10.4.20",
"dayjs": "^1.11.13",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"radix-ui": "^1.1.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"tailwindcss": "^4.0.12" "tailwindcss": "^4.0.12"
......
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