Commit 63b84398 by Ivan

feat: 时间范围过滤器

parent 82241f04
......@@ -13,6 +13,7 @@ class Admin::ImagesController < ApplicationController
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 ]),
filters: params[:q] || {},
tags: Tag.all.to_json,
pagination: {
current_page: @images.current_page,
total_pages: @images.total_pages,
......@@ -32,6 +33,7 @@ class Admin::ImagesController < ApplicationController
total_pages: @images.total_pages,
total_count: @images.total_count
},
tags: Tag.all.to_json,
pending_count: Image.pending.count,
approved_count: Image.approved.count,
rejected_count: Image.rejected.count
......@@ -74,7 +76,7 @@ class Admin::ImagesController < ApplicationController
if params[:image] && params[:image][:tag_ids].present?
@image.set_tags_by_ids(params[:image][:tag_ids])
end
# Add the tag if we're creating from a tag context
if params[:tag_id].present? && @tag
@image.tags << @tag unless @image.tags.include?(@tag)
......
......@@ -15,6 +15,7 @@ class ImagesController < ApplicationController
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 ]),
filters: params[:q] || {},
tags: Tag.all.to_json,
pagination: {
current_page: @images.current_page,
total_pages: @images.total_pages,
......@@ -29,6 +30,7 @@ class ImagesController < ApplicationController
render inertia: "images/Index", props: {
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]),
filters: params[:q] || {},
tags: Tag.all.to_json,
pagination: {
current_page: @images.current_page,
total_pages: @images.total_pages,
......
......@@ -10,7 +10,7 @@ export default function ImageUploadForm({
onSubmit,
submitButtonText = '上传图片',
processingButtonText = '上传中...',
showTagsField = false,
showTagsField = true,
isEdit = false,
image = null,
tags = [],
......
......@@ -12,34 +12,74 @@ export default function TagSearchInput({ value, onChange, allTags = {}, placehol
const [filteredTags, setFilteredTags] = useState({})
const [internalValue, setInternalValue] = useState(value || [])
// 只在组件初始化或外部value变化时更新selectedTags
// 处理标签数据和选中标签
useEffect(() => {
if (isEqual(value, internalValue)) {
setInternalValue(value || [])
if (value) {
const tagIds = value
// 处理标签数据,支持数组或已分组的对象格式
let processedTags = {}
let tagsData = allTags
// 如果是 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 = []
Object.values(allTags).forEach(tagGroup => {
const selected = [];
Object.values(processedTags).forEach(tagGroup => {
tagGroup.forEach(tag => {
if (tagIds.includes(tag.id)) {
selected.push(tag)
selected.push(tag);
}
})
})
});
});
setSelectedTags(selected)
setSelectedTags(selected);
} else {
setSelectedTags([])
// 如果标签 ID 为空,则清空选中标签
setSelectedTags([]);
}
}
}, [value, allTags])
// 使用传入的标签数据
useEffect(() => {
setAvailableTags(allTags || {})
}, [allTags])
}, [allTags, value, internalValue])
// 根据搜索词过滤标签
useEffect(() => {
......@@ -97,7 +137,7 @@ export default function TagSearchInput({ value, onChange, allTags = {}, placehol
}
return (
<div className="relative">
<div className="relative mt-1">
<Popover.Root open={isOpen} onOpenChange={setIsOpen} modal={false}>
<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">
......@@ -123,7 +163,7 @@ export default function TagSearchInput({ value, onChange, allTags = {}, placehol
))}
{selectedTags.length === 0 && (
<div className="text-gray-400 text-sm">{placeholder}</div>
<div className="text-gray-400 text-sm pl-1">{placeholder}</div>
)}
</div>
</Popover.Trigger>
......@@ -172,7 +212,7 @@ export default function TagSearchInput({ value, onChange, allTags = {}, placehol
{catalog || '未分类'}
</h3>
<div className="space-y-1">
{tags.map(tag => {
{Array.isArray(tags) ? tags.map(tag => {
const isSelected = selectedTags.some(t => t.id === tag.id)
return (
<button
......@@ -194,7 +234,7 @@ export default function TagSearchInput({ value, onChange, allTags = {}, placehol
)}
</button>
)
})}
}) : <div className="text-sm text-gray-500 py-1">无可用标签</div>}
</div>
</div>
))}
......
......@@ -13,27 +13,6 @@ export default function ComImageEdit({ image, tags, path, isAdmin = false }) {
title: image.title || '',
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) => {
e.preventDefault()
......@@ -85,7 +64,7 @@ export default function ComImageEdit({ image, tags, path, isAdmin = false }) {
<TagSearchInput
value={data.tag_ids}
onChange={(value) => setData('tag_ids', value)}
allTags={groupedTags}
allTags={tags}
placeholder="点击搜索图标添加标签"
/>
</Form.Control>
......
......@@ -2,7 +2,7 @@ import { Head } from '@inertiajs/react'
import Layout from '../../Layout'
import ComImageIndex from '../../../components/images/ComImageIndex'
export default function AdminImagesIndex({ auth, images, pagination, filters }) {
export default function AdminImagesIndex({ auth, images, pagination, filters, tags }) {
return (
<Layout user={auth}>
<Head title="图片管理" />
......@@ -15,6 +15,7 @@ export default function AdminImagesIndex({ auth, images, pagination, filters })
pagination={pagination}
filters={filters}
showUserName={true}
allTags={tags}
/>
</Layout>
)
......
......@@ -2,7 +2,7 @@ import { Head, Link } from '@inertiajs/react'
import Layout from '../../../Layout'
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 (
<Layout user={auth}>
<Head title={`${tag.name} - 标签图片`} />
......@@ -15,6 +15,7 @@ export default function TagsImagesIndex({ auth, tag, images, pagination, filters
pagination={pagination}
filters={filters}
showUserName={true}
allTags={tags}
backLink={{
url: '/admin/tags',
label: '返回标签列表'
......
......@@ -2,7 +2,7 @@ import Layout from '../Layout'
import ComImageIndex from '../../components/images/ComImageIndex'
import { Head } from '@inertiajs/react'
export default function Index({ auth, images, pagination, filters }) {
export default function Index({ auth, images, pagination, filters, tags }) {
return (
<Layout user={auth}>
<Head title="我的图片" />
......@@ -14,6 +14,7 @@ export default function Index({ auth, images, pagination, filters }) {
images={images}
pagination={pagination}
filters={filters}
allTags={tags}
/>
</Layout>
)
......
......@@ -2,7 +2,7 @@ import { Head, Link } from '@inertiajs/react'
import Layout from '../../Layout'
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 (
<Layout user={auth}>
<Head title={`标签: ${tag.name} - 图片列表`} />
......@@ -14,6 +14,7 @@ export default function Index({ tag, images, pagination, auth, filters = {} }) {
images={images}
pagination={pagination}
filters={filters}
allTags={tags}
backLink={{
url: '/tags',
label: '返回标签列表'
......
......@@ -16,6 +16,8 @@ class Image < ApplicationRecord
scope :approved, -> { where(status: :approved) }
scope :rejected, -> { where(status: :rejected) }
ransacker :status, formatter: ->(value) { statuses[value] }
def self.ransackable_attributes(auth_object = nil)
[ "created_at", "id", "id_value", "status", "title", "updated_at", "user_id" ]
end
......
......@@ -25,7 +25,9 @@
"@tailwindcss/vite": "^4.0.12",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"dayjs": "^1.11.13",
"lodash-es": "^4.17.21",
"radix-ui": "^1.1.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"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