Commit 82241f04 by Ivan

feat: 样式与页面补充

parent 5495beb5
......@@ -56,10 +56,13 @@ class Admin::ImagesController < ApplicationController
def new
if params[:tag_id].present?
render inertia: "admin/tags/images/New", props: {
tag: @tag.as_json(only: [ :id, :name, :catalog ], methods: [ :images_count ])
tag: @tag.as_json(only: [ :id, :name, :catalog ], methods: [ :images_count ]),
tags: Tag.all.as_json
}
else
render inertia: "admin/images/New"
render inertia: "admin/images/New", props: {
tags: Tag.all.as_json
}
end
end
......@@ -67,11 +70,11 @@ class Admin::ImagesController < ApplicationController
@image = Current.user.images.new(image_create_params)
if @image.save
if params[:tag_ids].present?
# 使用新的 set_tags_by_ids 方法来设置标签
@image.set_tags_by_ids(params[:tag_ids])
# 处理标签 - 从表单数据中获取tag_ids
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)
......@@ -98,10 +101,9 @@ class Admin::ImagesController < ApplicationController
def update
if @image.update(image_params)
# 处理标签
if params[:tag_ids].present?
# 使用新的 set_tags_by_ids 方法来设置标签
@image.set_tags_by_ids(params[:tag_ids])
# 处理标签 - 从表单数据中获取tag_ids
if params[:image] && params[:image][:tag_ids].present?
@image.set_tags_by_ids(params[:image][:tag_ids])
end
redirect_to admin_image_path(@image), notice: "Image was successfully updated."
......@@ -197,11 +199,11 @@ class Admin::ImagesController < ApplicationController
end
def image_params
params.require(:image).permit(:title)
params.require(:image).permit(:title, :status, tag_ids: [])
end
def image_create_params
params.require(:image).permit(:title, :file)
params.require(:image).permit(:title, :file, :status, tag_ids: [])
end
def authorize_admin
......
......@@ -3,20 +3,39 @@ class ImagesController < ApplicationController
# allow_unauthenticated_access only: [:index, :show, :search]
before_action :set_image, only: [ :show, :edit, :update, :destroy, :approve, :reject ]
before_action :set_tag, only: [ :index ], if: -> { params[:tag_id].present? }
def index
@q = Current.user.images.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, :thumbnail_url, :medium_url ]),
filters: params[:q] || {},
pagination: {
current_page: @images.current_page,
total_pages: @images.total_pages,
total_count: @images.total_count
if params[:tag_id].present?
# When accessed via /tags/:tag_id/images
@q = @tag.images.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: "tags/images/Index", props: {
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] || {},
pagination: {
current_page: @images.current_page,
total_pages: @images.total_pages,
total_count: @images.total_count
}
}
}
else
# Regular /images index
@q = Current.user.images.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, :thumbnail_url, :medium_url ]),
filters: params[:q] || {},
pagination: {
current_page: @images.current_page,
total_pages: @images.total_pages,
total_count: @images.total_count
}
}
end
end
def show
......@@ -30,15 +49,18 @@ class ImagesController < ApplicationController
def new
@image = Image.new
render inertia: "images/New"
render inertia: "images/New", props: {
tags: Tag.all.as_json
}
end
def create
@image = Current.user.images.new(image_params)
if @image.save
if params[:tag_ids].present?
@image.set_tags_by_ids(params[:tag_ids])
# 处理标签 - 从表单数据中获取tag_ids
if params[:image] && params[:image][:tag_ids].present?
@image.set_tags_by_ids(params[:image][:tag_ids])
end
redirect_to image_path(@image), notice: "Image was successfully uploaded and is pending review."
else
......@@ -69,8 +91,9 @@ class ImagesController < ApplicationController
end
if @image.update(image_params)
if params[:tag_ids].present?
@image.set_tags_by_ids(params[:tag_ids])
# 处理标签 - 从表单数据中获取tag_ids
if params[:image] && params[:image][:tag_ids].present?
@image.set_tags_by_ids(params[:image][:tag_ids])
end
redirect_to image_path(@image), notice: "Image was successfully updated."
else
......@@ -103,8 +126,12 @@ class ImagesController < ApplicationController
@image = Current.user.images.find(params[:id])
end
def set_tag
@tag = Tag.find(params[:tag_id])
end
def image_params
params.require(:image).permit(:title, :file)
params.require(:image).permit(:title, :file, tag_ids: [])
end
def authorize_user
......
import { useState, useEffect } from 'react'
import * as Form from '@radix-ui/react-form'
import TagSearchInput from './TagSearchInput'
export default function ImageUploadForm({
data,
setData,
processing,
errors = {},
onSubmit,
submitButtonText = '上传图片',
processingButtonText = '上传中...',
showTagsField = false,
isEdit = false,
image = null,
tags = [],
cancelUrl = null
}) {
const [preview, setPreview] = useState(isEdit && image?.file_url ? image.file_url : null)
const [groupedTags, setGroupedTags] = useState({})
// 使用控制器返回的所有标签数据并按catalog属性分组
useEffect(() => {
if (Array.isArray(tags) && tags.length > 0) {
// 按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()
onSubmit(e)
}
const handleFileChange = (e) => {
const file = e.target.files[0]
setData('file', file)
if (file) {
const reader = new FileReader()
reader.onloadend = () => {
setPreview(reader.result)
}
reader.readAsDataURL(file)
} else {
setPreview(null)
}
}
return (
<Form.Root className="space-y-6" onSubmit={handleSubmit}>
<Form.Field name="title" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700">
标题
</Form.Label>
<Form.Control asChild>
<input
type="text"
name="title"
value={data.title}
onChange={(e) => setData('title', e.target.value)}
className="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
placeholder="输入标题"
/>
</Form.Control>
{errors.title && (
<Form.Message className="text-sm text-red-600">
{errors.title}
</Form.Message>
)}
</Form.Field>
{!isEdit && (
<Form.Field name="file" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700">
图片
</Form.Label>
<div className="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
<div className="space-y-1 text-center">
{preview ? (
<div className="mb-4">
<img
src={preview}
alt="Preview"
className="mx-auto h-64 object-contain"
/>
</div>
) : (
<svg
className="mx-auto h-12 w-12 text-gray-400"
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
aria-hidden="true"
>
<path
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
<div className="flex text-sm text-gray-600">
<label
htmlFor="file-upload"
className="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500"
>
<span>上传图片</span>
<Form.Control asChild>
<input
id="file-upload"
name="file"
type="file"
className="sr-only"
onChange={handleFileChange}
/>
</Form.Control>
</label>
<p className="pl-1">或拖拽上传</p>
</div>
<p className="text-xs text-gray-500">
{/* PNG, JPG, GIF up to 10MB */}
</p>
</div>
</div>
{errors.file && (
<Form.Message className="text-sm text-red-600">
{errors.file}
</Form.Message>
)}
</Form.Field>
)}
{isEdit && image && (
<div className="mb-6">
<img
src={image.file_url}
alt={image.title}
className="max-h-64 mx-auto object-contain"
/>
</div>
)}
{showTagsField && (
<Form.Field name="tags" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700">
标签
</Form.Label>
<Form.Control asChild>
<TagSearchInput
value={data.tag_ids || []}
onChange={(value) => setData('tag_ids', value)}
allTags={groupedTags}
placeholder="点击搜索图标添加标签"
/>
</Form.Control>
<p className="text-xs text-gray-500">
点击右侧搜索图标可按分类浏览标签
</p>
</Form.Field>
)}
<div className="flex justify-end space-x-3">
{cancelUrl && (
<a
href={cancelUrl}
className="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
取消
</a>
)}
<Form.Submit asChild>
<button
type="submit"
disabled={processing}
className="inline-flex justify-center py-2 px-4 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 disabled:opacity-50"
>
{processing ? processingButtonText : submitButtonText}
</button>
</Form.Submit>
</div>
</Form.Root>
)
}
import React from 'react'
import { Link } from '@inertiajs/react'
export default function Pagination({ links }) {
if (!links || links.length <= 3) {
return null
}
return (
<nav className="border-t border-gray-200 px-4 flex items-center justify-between sm:px-0">
<div className="w-0 flex-1 flex">
{links[0].url ? (
<Link
href={links[0].url}
className="border-t-2 border-transparent pt-4 pr-1 inline-flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300"
>
<svg
className="mr-3 h-5 w-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z"
clipRule="evenodd"
/>
</svg>
Previous
</Link>
) : (
<span className="border-t-2 border-transparent pt-4 pr-1 inline-flex items-center text-sm font-medium text-gray-300">
<svg
className="mr-3 h-5 w-5 text-gray-300"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M7.707 14.707a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l2.293 2.293a1 1 0 010 1.414z"
clipRule="evenodd"
/>
</svg>
Previous
</span>
)}
</div>
<div className="hidden md:flex">
{links.slice(1, -1).map((link, i) => (
<React.Fragment key={i}>
{link.url ? (
<Link
href={link.url}
className={`border-t-2 pt-4 px-4 inline-flex items-center text-sm font-medium ${
link.active
? 'border-indigo-500 text-indigo-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}`}
dangerouslySetInnerHTML={{ __html: link.label }}
/>
) : (
<span
className="border-transparent text-gray-500 border-t-2 pt-4 px-4 inline-flex items-center text-sm font-medium"
dangerouslySetInnerHTML={{ __html: link.label }}
/>
)}
</React.Fragment>
))}
</div>
<div className="w-0 flex-1 flex justify-end">
{links[links.length - 1].url ? (
<Link
href={links[links.length - 1].url}
className="border-t-2 border-transparent pt-4 pl-1 inline-flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300"
>
Next
<svg
className="ml-3 h-5 w-5 text-gray-400"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</Link>
) : (
<span className="border-t-2 border-transparent pt-4 pl-1 inline-flex items-center text-sm font-medium text-gray-300">
Next
<svg
className="ml-3 h-5 w-5 text-gray-300"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</span>
)}
</div>
</nav>
)
}
import { Link } from "@inertiajs/react";
import ComImageStatusTag from "./ComImageStatusTag";
export function ComImageCard({ image, path, showActions = false }) {
export function ComImageCard({ image, path, showUserName = false}) {
return (
<div
className="bg-white overflow-hidden shadow rounded-lg flex flex-col"
......@@ -18,6 +18,11 @@ export function ComImageCard({ image, path, showActions = false }) {
</div>
</div>
<div className="px-4 py-4 flex flex-col justify-between flex-grow">
{showUserName && (
<div className="text-sm text-gray-500">
{image.user.name}
</div>
)}
<h3 className="text-lg font-medium text-gray-900 truncate">
{image.title}
</h3>
......
......@@ -5,7 +5,7 @@ import * as Collapsible from '@radix-ui/react-collapsible'
import { uniqBy } from 'lodash-es'
import { ComImageCard } from './ComImageCard'
export default function ComImageIndex({ title, description, path, images, pagination, filters, auth, backLink, actionButton }) {
export default function ComImageIndex({ title, description, path, images, pagination, filters, auth, backLink, actionButton, showUserName = false }) {
const [allImages, setAllImages] = useState(images || [])
const [page, setPage] = useState(pagination?.current_page || 1)
const [maxVisiblePage, setMaxVisiblePage] = useState(0)
......@@ -148,10 +148,10 @@ export default function ComImageIndex({ title, description, path, images, pagina
{description}
</p>
</div>
<div className="flex space-x-2">
<div className="flex flex-col space-y-0 sm:flex-row">
<button
onClick={() => setIsFilterOpen(!isFilterOpen)}
className={`inline-flex items-center px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 ${filterCount ? '!bg-indigo-700 !text-white' : ''}`}
className={`inline-flex items-center justify-center mb-2 ml-2 px-3 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 ${filterCount ? '!bg-indigo-700 !text-white' : ''}`}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
......@@ -163,7 +163,7 @@ export default function ComImageIndex({ title, description, path, images, pagina
) : auth && (
<Link
href={`${path}/new`}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
className="inline-flex items-center justify-center mb-2 ml-2 px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
上传新图片
</Link>
......@@ -292,6 +292,7 @@ export default function ComImageIndex({ title, description, path, images, pagina
ref={index === allImages.length - 1 ? lastImageElementRef : null}
image={image}
path={path}
showUserName={showUserName}
// showActions={showActions}
/>
))}
......
......@@ -7,16 +7,16 @@ export default function ComImageShow({ path, image, can_edit, can_approve, isAdm
const [isModalOpen, setIsModalOpen] = useState(false)
return (
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 sm:px-6 flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-gray-900">{image.title}</h1>
<div className="w-full bg-white shadow overflow-hidden sm:rounded-lg">
<div className="w-full px-4 py-5 sm:px-6 flex justify-between items-center">
<div className="flex-grow w-0">
<h1 className="w-full text-2xl truncate font-bold text-gray-900">{image.title}</h1>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
{image.user.name}{' '}
{new Date(image.created_at).toLocaleDateString()}{' '}上传
</p>
</div>
<div className="flex space-x-2">
<div className="flex-shrink-0 grid grid-cols-2 gap-2">
{can_edit && (
<Link
href={`${path}/edit`}
......@@ -33,7 +33,7 @@ export default function ComImageShow({ path, image, can_edit, can_approve, isAdm
as="button"
className="inline-flex items-center px-3 py-1.5 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
批准
通过
</Link>
<Link
href={`${path}/reject`}
......
......@@ -10,7 +10,7 @@ export default function ComImageStatusTag({ image }) {
}`}
>
{image.status === 'pending' ? '待审核' :
image.status === 'approved' ? '已批准' : '已拒绝'}
image.status === 'approved' ? '已通过' : '已拒绝'}
</span>
)
}
\ No newline at end of file
......@@ -144,7 +144,7 @@ export default function ComTagsIndex({
href={`${path}/${tag.id}/images`}
className="text-xs font-medium text-gray-700 bg-gray-200 hover:bg-gray-300 px-2 py-1 rounded-full transition-colors"
>
{tag.images_count}
{tag.images_count}
</Link>
</div>
......
import { useState } from 'react'
import { Link } from '@inertiajs/react'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { AcademicCapIcon } from '@heroicons/react/24/outline'
export default function Layout({ children, user, title }) {
const [isMenuOpen, setIsMenuOpen] = useState(false)
......@@ -13,7 +14,7 @@ export default function Layout({ children, user, title }) {
<div className="flex">
<div className="flex-shrink-0 flex items-center">
<Link href="/" className="text-xl font-bold text-indigo-600">
图片管理
<AcademicCapIcon className="h-6 w-6" />
</Link>
</div>
<nav className="hidden sm:ml-6 sm:flex sm:space-x-8">
......@@ -31,12 +32,28 @@ export default function Layout({ children, user, title }) {
上传
</Link>
)}
{user && (
<Link
href="/tags"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
标签
</Link>
)}
{user?.roles.includes('admin') && (
<Link
href="/admin/images"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
管理员
图片总管
</Link>
)}
{user?.roles.includes('admin') && (
<Link
href="/admin/tags"
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
>
标签总管
</Link>
)}
</nav>
......@@ -154,12 +171,28 @@ export default function Layout({ children, user, title }) {
上传
</Link>
)}
{user && (
<Link
href="/tags"
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"
>
标签
</Link>
)}
{user?.roles?.includes('admin') && (
<Link
href="/admin/images"
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"
>
管理员
图片总管
</Link>
)}
{user?.roles?.includes('admin') && (
<Link
href="/admin/tags"
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"
>
标签总管
</Link>
)}
</div>
......
......@@ -14,6 +14,7 @@ export default function AdminImagesIndex({ auth, images, pagination, filters })
images={images}
pagination={pagination}
filters={filters}
showUserName={true}
/>
</Layout>
)
......
import { Head, useForm, router, Link } from '@inertiajs/react'
import Layout from '../../Layout'
import ImageUploadForm from '../../../components/ImageUploadForm'
export default function New({ auth, errors = {}, tags }) {
const { data, setData, processing } = useForm({
title: '',
file: null,
tag_ids: [],
status: 'approved',
})
const handleSubmit = (e) => {
e.preventDefault()
router.post('/admin/images', { image: data })
}
return (
<Layout user={auth}>
<Head title="添加新图片" />
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 sm:px-6">
<div className="flex items-center mb-4">
<Link
href="/admin/images"
className="inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-500"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
返回图片列表
</Link>
</div>
<h1 className="text-2xl font-bold text-gray-900">添加新图片</h1>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
添加新图片(自动审核通过)
</p>
</div>
<div className="border-t border-gray-200 px-4 py-5 sm:px-6">
<ImageUploadForm
data={data}
setData={setData}
processing={processing}
errors={errors}
onSubmit={handleSubmit}
submitButtonText="添加图片"
processingButtonText="添加中..."
showTagsField={true}
tags={tags}
/>
</div>
</div>
</Layout>
)
}
......@@ -14,6 +14,7 @@ export default function TagsImagesIndex({ auth, tag, images, pagination, filters
images={images}
pagination={pagination}
filters={filters}
showUserName={true}
backLink={{
url: '/admin/tags',
label: '返回标签列表'
......@@ -21,7 +22,7 @@ export default function TagsImagesIndex({ auth, tag, images, pagination, filters
actionButton={(
<Link
href={`/admin/tags/${tag.id}/images/new`}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
className="inline-flex items-center mb-2 ml-2 px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
添加新图片到此标签
</Link>
......
import { useState } from 'react'
import { Head, useForm, router, Link } from '@inertiajs/react'
import Layout from '../../../Layout'
import * as Form from '@radix-ui/react-form'
import ImageUploadForm from '../../../../components/ImageUploadForm'
export default function New({ auth, tag, errors = {} }) {
const [preview, setPreview] = useState(null)
export default function New({ auth, tag, tags, errors = {} }) {
const { data, setData, processing } = useForm({
title: '',
file: null,
tag_ids: [tag.id], // Pre-select the current tag
status: 'approved'
})
const handleSubmit = (e) => {
......@@ -15,21 +15,6 @@ export default function New({ auth, tag, errors = {} }) {
router.post(`/admin/tags/${tag.id}/images`, { image: data })
}
const handleFileChange = (e) => {
const file = e.target.files[0]
setData('file', file)
if (file) {
const reader = new FileReader()
reader.onloadend = () => {
setPreview(reader.result)
}
reader.readAsDataURL(file)
} else {
setPreview(null)
}
}
return (
<Layout user={auth}>
<Head title={`添加图片到标签: ${tag.name}`} />
......@@ -52,100 +37,18 @@ export default function New({ auth, tag, errors = {} }) {
</p>
</div>
<div className="border-t border-gray-200 px-4 py-5 sm:px-6">
<Form.Root className="space-y-6" onSubmit={handleSubmit}>
<Form.Field name="title" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700">
标题
</Form.Label>
<Form.Control asChild>
<input
type="text"
name="title"
value={data.title}
onChange={(e) => setData('title', e.target.value)}
className="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
placeholder="输入标题"
/>
</Form.Control>
{errors.title && (
<Form.Message className="text-sm text-red-600">
{errors.title}
</Form.Message>
)}
</Form.Field>
<Form.Field name="file" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700">
图片
</Form.Label>
<div className="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
<div className="space-y-1 text-center">
{preview ? (
<div className="mb-4">
<img
src={preview}
alt="Preview"
className="mx-auto h-64 object-contain"
/>
</div>
) : (
<svg
className="mx-auto h-12 w-12 text-gray-400"
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
aria-hidden="true"
>
<path
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
<div className="flex text-sm text-gray-600">
<label
htmlFor="file-upload"
className="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500"
>
<span>上传图片</span>
<Form.Control asChild>
<input
id="file-upload"
name="file"
type="file"
className="sr-only"
onChange={handleFileChange}
/>
</Form.Control>
</label>
<p className="pl-1">或拖拽上传</p>
</div>
<p className="text-xs text-gray-500">
{/* PNG, JPG, GIF up to 10MB */}
</p>
</div>
</div>
{errors.file && (
<Form.Message className="text-sm text-red-600">
{errors.file}
</Form.Message>
)}
</Form.Field>
<div className="flex justify-end">
<Form.Submit asChild>
<button
type="submit"
disabled={processing}
className="inline-flex justify-center py-2 px-4 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 disabled:opacity-50"
>
{processing ? '上传中...' : '上传并添加到标签'}
</button>
</Form.Submit>
</div>
</Form.Root>
<ImageUploadForm
data={data}
setData={setData}
processing={processing}
errors={errors}
onSubmit={handleSubmit}
submitButtonText="上传并添加到标签"
processingButtonText="上传中..."
showTagsField={true}
tags={tags}
cancelUrl={`/admin/tags/${tag.id}/images`}
/>
</div>
</div>
</Layout>
......
import { Head } from '@inertiajs/react'
import { Head, useForm } from '@inertiajs/react'
import Layout from '../Layout'
import ComImageEdit from '../../components/images/ComImageEdit'
import ImageUploadForm from '../../components/ImageUploadForm'
export default function Edit({ image, tags, auth, errors = {} }) {
// 初始化已有标签的ID列表
const initialTagIds = image.tags
? image.tags.map(tag => tag.id)
: []
const { data, setData, patch, processing } = useForm({
title: image.title || '',
tag_ids: initialTagIds, // 使用tag_ids而不是tags
})
const handleSubmit = (e) => {
e.preventDefault()
patch(`/images/${image.id}`)
}
return (
<Layout user={auth}>
<Head title={`Edit ${image.title}`} />
<ComImageEdit
image={image}
tags={tags}
path={`/images/${image.id}`}
isAdmin={false}
/>
<Head title={`编辑 ${image.title}`} />
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 sm:px-6">
<h1 className="text-2xl font-bold text-gray-900">编辑图片</h1>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
更新图片信息
</p>
</div>
<div className="border-t border-gray-200 px-4 py-5 sm:px-6">
<ImageUploadForm
data={data}
setData={setData}
processing={processing}
errors={errors}
onSubmit={handleSubmit}
submitButtonText="保存"
processingButtonText="保存中..."
showTagsField={true}
isEdit={true}
image={image}
tags={tags}
cancelUrl={`/images/${image.id}`}
/>
</div>
</div>
</Layout>
)
}
import { useState } from 'react'
import { Head, useForm, router } from '@inertiajs/react'
import Layout from '../Layout'
import * as Form from '@radix-ui/react-form'
import ImageUploadForm from '../../components/ImageUploadForm'
export default function New({ auth, errors = {} }) {
const [preview, setPreview] = useState(null)
export default function New({ auth, tags = [], errors = {} }) {
const { data, setData, processing } = useForm({
title: '',
file: null,
tags: '',
tag_ids: [],
})
const handleSubmit = (e) => {
......@@ -16,21 +14,6 @@ export default function New({ auth, errors = {} }) {
router.post('/images', { image: data })
}
const handleFileChange = (e) => {
const file = e.target.files[0]
setData('file', file)
if (file) {
const reader = new FileReader()
reader.onloadend = () => {
setPreview(reader.result)
}
reader.readAsDataURL(file)
} else {
setPreview(null)
}
}
return (
<Layout user={auth}>
<Head title="上传新图片" />
......@@ -42,119 +25,17 @@ export default function New({ auth, errors = {} }) {
</p>
</div>
<div className="border-t border-gray-200 px-4 py-5 sm:px-6">
<Form.Root className="space-y-6" onSubmit={handleSubmit}>
<Form.Field name="title" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700">
标题
</Form.Label>
<Form.Control asChild>
<input
type="text"
name="title"
value={data.title}
onChange={(e) => setData('title', e.target.value)}
className="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
placeholder="输入标题"
/>
</Form.Control>
{errors.title && (
<Form.Message className="text-sm text-red-600">
{errors.title}
</Form.Message>
)}
</Form.Field>
<Form.Field name="file" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700">
图片
</Form.Label>
<div className="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
<div className="space-y-1 text-center">
{preview ? (
<div className="mb-4">
<img
src={preview}
alt="Preview"
className="mx-auto h-64 object-contain"
/>
</div>
) : (
<svg
className="mx-auto h-12 w-12 text-gray-400"
stroke="currentColor"
fill="none"
viewBox="0 0 48 48"
aria-hidden="true"
>
<path
d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
<div className="flex text-sm text-gray-600">
<label
htmlFor="file-upload"
className="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500"
>
<span>上传图片</span>
<Form.Control asChild>
<input
id="file-upload"
name="file"
type="file"
className="sr-only"
onChange={handleFileChange}
/>
</Form.Control>
</label>
<p className="pl-1">或拖拽上传</p>
</div>
<p className="text-xs text-gray-500">
{/* PNG, JPG, GIF up to 10MB */}
</p>
</div>
</div>
{errors.file && (
<Form.Message className="text-sm text-red-600">
{errors.file}
</Form.Message>
)}
</Form.Field>
{/* <Form.Field name="tags" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700">
标签
</Form.Label>
<Form.Control asChild>
<input
type="text"
name="tags"
value={data.tags}
onChange={(e) => setData('tags', e.target.value)}
className="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
placeholder="Enter tags separated by commas (e.g. nature, landscape, mountains)"
/>
</Form.Control>
<p className="text-xs text-gray-500">
Enter tags separated by commas
</p>
</Form.Field> */}
<div className="flex justify-end">
<Form.Submit asChild>
<button
type="submit"
disabled={processing}
className="inline-flex justify-center py-2 px-4 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 disabled:opacity-50"
>
{processing ? '上传中...' : '上传图片'}
</button>
</Form.Submit>
</div>
</Form.Root>
<ImageUploadForm
data={data}
setData={setData}
processing={processing}
errors={errors}
onSubmit={handleSubmit}
submitButtonText="上传图片"
processingButtonText="上传中..."
showTagsField={false}
cancelUrl="/images"
/>
</div>
</div>
</Layout>
......
......@@ -5,10 +5,10 @@ import ComTagsIndex from '../../components/tags/ComTagsIndex'
export default function TagsIndex({ tags, auth }) {
return (
<Layout user={auth}>
<Head title="Tags" />
<Head title="标签" />
<ComTagsIndex
title="Tags"
description="标签管理"
title="标签"
description="标签总览"
tags={tags}
auth={auth}
isAdmin={false}
......
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 = {} }) {
return (
<Layout user={auth}>
<Head title={`标签: ${tag.name} - 图片列表`} />
<ComImageIndex
auth={auth}
title={tag.name}
description={`查看关联图片`}
path={`/tags/${tag.id}/images`}
images={images}
pagination={pagination}
filters={filters}
backLink={{
url: '/tags',
label: '返回标签列表'
}}
actionButton={(
<Link
href={`/tags/${tag.id}/images/new`}
className="inline-flex items-center mb-2 ml-2 px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
添加新图片到此标签
</Link>
)}
/>
</Layout>
)
}
import { Head, useForm, router, Link } from '@inertiajs/react'
import Layout from '../../Layout'
import ImageUploadForm from '../../../components/ImageUploadForm'
export default function New({ auth, tag, tags, errors = {} }) {
const { data, setData, processing } = useForm({
title: '',
file: null,
tag_ids: [tag.id], // Pre-select the current tag
status: 'approved'
})
const handleSubmit = (e) => {
e.preventDefault()
router.post(`/tags/${tag.id}/images`, { image: data })
}
return (
<Layout user={auth}>
<Head title={`添加图片到标签: ${tag.name}`} />
<div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 sm:px-6">
<div className="flex items-center mb-4">
<Link
href={`/tags/${tag.id}/images`}
className="inline-flex items-center text-sm font-medium text-indigo-600 hover:text-indigo-500"
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
返回标签图片列表
</Link>
</div>
<h1 className="text-2xl font-bold text-gray-900">添加图片到标签: {tag.name}</h1>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
上传新图片并自动添加到标签 "{tag.name}"
</p>
</div>
<div className="border-t border-gray-200 px-4 py-5 sm:px-6">
<ImageUploadForm
data={data}
setData={setData}
processing={processing}
errors={errors}
onSubmit={handleSubmit}
submitButtonText="上传并添加到标签"
processingButtonText="上传中..."
// showTagsField={true}
// tags={tags}
cancelUrl={`/tags/${tag.id}/images`}
/>
</div>
</div>
</Layout>
)
}
......@@ -45,14 +45,6 @@ class Image < ApplicationRecord
# Rails.application.routes.url_helpers.rails_blob_path(variant, only_path: true)
end
# Method to add tags to an image by names
def add_tags(tag_names)
tag_names.each do |name|
tag = Tag.find_or_create_by(name: name.downcase.strip)
tags << tag unless tags.include?(tag)
end
end
# Method to set tags by IDs
def set_tags_by_ids(tag_ids)
return if tag_ids.nil?
......
......@@ -45,7 +45,9 @@ Rails.application.routes.draw do
end
# Tag management routes
resources :tags, except: [ :show ]
resources :tags, except: [ :show ] do
resources :images, only: [ :index, :new, :create ]
end
# Admin namespace for admin-only actions
namespace :admin do
......
......@@ -4,11 +4,13 @@
"watchAdditionalPaths": []
},
"development": {
"host": "127.0.0.1",
"autoBuild": true,
"publicOutputDir": "vite-dev",
"port": 3036
},
"test": {
"host": "127.0.0.1",
"autoBuild": true,
"publicOutputDir": "vite-test",
"port": 3037
......
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