Commit c139b140 by Ivan

feat: update images

parent 75d4a205
...@@ -97,7 +97,7 @@ class Admin::ImagesController < ApplicationController ...@@ -97,7 +97,7 @@ class Admin::ImagesController < ApplicationController
end end
def destroy def destroy
@image.destroy @image.destroy!
redirect_to admin_images_path, notice: "Image was successfully deleted." redirect_to admin_images_path, notice: "Image was successfully deleted."
end end
......
...@@ -3,10 +3,9 @@ class ImagesController < ApplicationController ...@@ -3,10 +3,9 @@ class ImagesController < ApplicationController
# allow_unauthenticated_access only: [:index, :show, :search] # allow_unauthenticated_access only: [:index, :show, :search]
before_action :set_image, only: [ :show, :edit, :update, :destroy, :approve, :reject ] before_action :set_image, only: [ :show, :edit, :update, :destroy, :approve, :reject ]
before_action :authorize_admin, only: [ :approve, :reject, :destroy ]
def index def index
@q = current_user.images.includes(:user, :tags).ransack(params[:q]) @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) @images = @q.result(distinct: true).with_attached_file.order(created_at: :desc).page(params[:page]).per(12)
render inertia: "images/Index", props: { render inertia: "images/Index", props: {
...@@ -23,8 +22,9 @@ class ImagesController < ApplicationController ...@@ -23,8 +22,9 @@ class ImagesController < ApplicationController
def show def show
render inertia: "images/Show", props: { render inertia: "images/Show", props: {
image: @image.as_json(include: [ :user, :tags ], methods: [ :file_url ]), image: @image.as_json(include: [ :user, :tags ], methods: [ :file_url ]),
can_edit: Current.user == @image.user, can_edit: Current.user == @image.user && @image.status == "pending",
can_approve: Current.user&.admin? can_approve: Current.user&.admin?,
can_delete: Current.user == @image.user && @image.status == "pending" || Current.user&.admin?
} }
end end
...@@ -60,6 +60,10 @@ class ImagesController < ApplicationController ...@@ -60,6 +60,10 @@ class ImagesController < ApplicationController
def update def update
authorize_user authorize_user
unless @image.status == "pending" || Current.user&.admin?
return redirect_to image_path(@image), notice: "Image was successfully updated."
end
if @image.update(image_params) if @image.update(image_params)
if params[:tags].present? if params[:tags].present?
@image.tags.clear @image.tags.clear
...@@ -80,17 +84,6 @@ class ImagesController < ApplicationController ...@@ -80,17 +84,6 @@ class ImagesController < ApplicationController
redirect_to images_path, notice: "Image was successfully deleted." redirect_to images_path, notice: "Image was successfully deleted."
end 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 ]),
filters: params[:q] || {},
tags: Tag.all.pluck(:name)
}
end
def approve def approve
@image.approved! @image.approved!
redirect_to image_path(@image), notice: "Image was successfully approved." redirect_to image_path(@image), notice: "Image was successfully approved."
...@@ -104,7 +97,7 @@ class ImagesController < ApplicationController ...@@ -104,7 +97,7 @@ class ImagesController < ApplicationController
private private
def set_image def set_image
@image = Image.find(params[:id]) @image = Current.user.images.find(params[:id])
end end
def image_params def image_params
......
import React from 'react'
import { Link } from '@inertiajs/react'
export default function ImageCard({ image, showActions = true }) {
const statusColors = {
pending: 'bg-yellow-100 text-yellow-800',
approved: 'bg-green-100 text-green-800',
rejected: 'bg-red-100 text-red-800',
}
return (
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="relative pb-[75%]">
<img
src={image.file_url}
alt={image.title}
className="absolute h-full w-full object-cover"
/>
</div>
<div className="px-4 py-4">
<h3 className="text-lg font-medium text-gray-900 truncate" title={image.title}>
{image.title}
</h3>
<p className="mt-1 text-sm text-gray-500">
Uploaded by {image.user?.name || 'Unknown'} on{' '}
{new Date(image.created_at).toLocaleDateString()}
</p>
{image.status && (
<span className={`inline-flex mt-2 items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusColors[image.status] || 'bg-gray-100 text-gray-800'}`}>
{image.status.charAt(0).toUpperCase() + image.status.slice(1)}
</span>
)}
{image.tags && image.tags.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1">
{image.tags.slice(0, 3).map((tag) => (
<span
key={tag.id}
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-indigo-100 text-indigo-800"
>
{tag.name}
</span>
))}
{image.tags.length > 3 && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800">
+{image.tags.length - 3} more
</span>
)}
</div>
)}
{showActions && (
<div className="mt-4 flex space-x-2">
<Link
href={`/images/${image.id}`}
className="inline-flex items-center px-2.5 py-1.5 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
View
</Link>
<Link
href={`/images/${image.id}/edit`}
className="inline-flex items-center px-2.5 py-1.5 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Edit
</Link>
</div>
)}
</div>
</div>
)
}
import { Link } from "@inertiajs/react";
import ComImageStatusTag from "./ComImageStatusTag";
export function ComImageCard({ image, path, showActions = false }) {
return (
<div
className="bg-white overflow-hidden shadow rounded-lg flex flex-col"
>
<div className="relative pb-[75%]">
<img
loading="lazy"
src={image.file_url}
alt={image.title}
className="absolute h-full w-full object-cover"
/>
<div className="absolute top-2 right-2">
<ComImageStatusTag image={image} />
</div>
</div>
<div className="px-4 py-4 flex flex-col justify-between flex-grow">
<h3 className="text-lg font-medium text-gray-900 truncate">
{image.title}
</h3>
<div className="mt-2 flex flex-wrap gap-1">
{image.tags.map((tag) => (
<span
key={`tag-${tag.id}`}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800"
>
{tag.name}
</span>
))}
</div>
<div className="mt-4 flex justify-between">
<Link
href={`${path}/${image.id}`}
className="text-sm text-indigo-600 hover:text-indigo-900"
>
查看详情
</Link>
<span className="text-sm text-gray-500">
{new Date(image.created_at).toLocaleDateString()}
</span>
</div>
</div>
</div>
)
}
\ No newline at end of file
...@@ -3,6 +3,7 @@ import { Head, Link, router } from '@inertiajs/react' ...@@ -3,6 +3,7 @@ import { Head, Link, router } from '@inertiajs/react'
import * as Form from '@radix-ui/react-form' import * as Form from '@radix-ui/react-form'
import * as Collapsible from '@radix-ui/react-collapsible' import * as Collapsible from '@radix-ui/react-collapsible'
import { uniqBy } from 'lodash-es' 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 }) {
const [allImages, setAllImages] = useState(images || []) const [allImages, setAllImages] = useState(images || [])
...@@ -164,7 +165,7 @@ export default function ComImageIndex({ title, description, path, images, pagina ...@@ -164,7 +165,7 @@ export default function ComImageIndex({ title, description, path, images, pagina
href={`${path}/new`} 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 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> </Link>
)} )}
</div> </div>
...@@ -286,59 +287,13 @@ export default function ComImageIndex({ title, description, path, images, pagina ...@@ -286,59 +287,13 @@ export default function ComImageIndex({ title, description, path, images, pagina
</div> </div>
)} )}
{allImages.map((image, index) => ( {allImages.map((image, index) => (
<div <ComImageCard
key={image.id} key={image.id}
ref={index === allImages.length - 1 ? lastImageElementRef : null} ref={index === allImages.length - 1 ? lastImageElementRef : null}
className="bg-white overflow-hidden shadow rounded-lg" image={image}
> path={path}
<div className="relative pb-[75%]"> // showActions={showActions}
<img
loading="lazy"
src={image.file_url}
alt={image.title}
className="absolute h-full w-full object-cover"
/> />
<div className="absolute top-2 right-2">
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
image.status === 'pending'
? 'bg-yellow-100 text-yellow-800'
: image.status === 'approved'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}
>
{image.status}
</span>
</div>
</div>
<div className="px-4 py-4">
<h3 className="text-lg font-medium text-gray-900 truncate">
{image.title}
</h3>
<div className="mt-2 flex flex-wrap gap-1">
{image.tags.map((tag) => (
<span
key={`tag-${tag.id}`}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800"
>
{tag.name}
</span>
))}
</div>
<div className="mt-4 flex justify-between">
<Link
href={`${path}/${image.id}`}
className="text-sm text-indigo-600 hover:text-indigo-900"
>
查看详情
</Link>
<span className="text-sm text-gray-500">
{new Date(image.created_at).toLocaleDateString()}
</span>
</div>
</div>
</div>
))} ))}
</div> </div>
) : ( ) : (
...@@ -367,17 +322,17 @@ export default function ComImageIndex({ title, description, path, images, pagina ...@@ -367,17 +322,17 @@ export default function ComImageIndex({ title, description, path, images, pagina
/> />
</svg> </svg>
<h3 className="mt-2 text-sm font-medium text-gray-900"> <h3 className="mt-2 text-sm font-medium text-gray-900">
暂无文件 暂无图片
</h3> </h3>
<p className="mt-1 text-sm text-gray-500"> <p className="mt-1 text-sm text-gray-500">
开始上传文件 开始上传图片
</p> </p>
<div className="mt-6"> <div className="mt-6">
<Link <Link
href={`${path}/new`} href={`${path}/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" 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"
> >
上传新文件 上传新图片
</Link> </Link>
</div> </div>
</div> </div>
......
import { useState } from 'react'
import { Link } from '@inertiajs/react'
import * as Dialog from '@radix-ui/react-dialog'
import ComImageStatusTag from './ComImageStatusTag'
export default function ComImageShow({ path, image, can_edit, can_approve, isAdmin = false }) {
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>
<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">
{can_edit && (
<Link
href={`${path}/edit`}
className="inline-flex items-center px-3 py-1.5 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>
)}
{can_approve && image.status === 'pending' && (
<>
<Link
href={`${path}/approve`}
method="patch"
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`}
method="patch"
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-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
拒绝
</Link>
</>
)}
{can_approve && (
<Link
href={`${path}`}
method="delete"
as="button"
className="inline-flex items-center px-3 py-1.5 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={(e) => {
if (!confirm('确定要删除这张图片吗?')) {
e.preventDefault()
}
}}
>
删除
</Link>
)}
</div>
</div>
<div className="border-t border-gray-200">
<div className="flex flex-col md:flex-row">
<div className="md:w-2/3 p-4">
<div className="relative pb-[75%]">
<img
src={image.file_url}
alt={image.title}
className="absolute h-full w-full object-contain cursor-pointer"
onClick={() => setIsModalOpen(true)}
/>
</div>
</div>
<div className="md:w-1/3 p-4 border-t md:border-t-0 md:border-l border-gray-200">
<div className="mb-4">
<h3 className="text-lg font-medium text-gray-900">审核状态</h3>
<ComImageStatusTag image={image} />
</div>
<div className="mb-4">
<h3 className="text-lg font-medium text-gray-900">标签</h3>
<div className="mt-2 flex flex-wrap gap-1">
{image.tags.length > 0 ? (
image.tags.map((tag) => (
<span
key={tag.id}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800"
>
{tag.name}
</span>
))
) : (
<p className="text-sm text-gray-500">无标签</p>
)}
</div>
</div>
<div className="mb-4">
<h3 className="text-lg font-medium text-gray-900">上传者</h3>
<p className="text-sm text-gray-500">{image.user.name}</p>
<p className="text-sm text-gray-500">
{image.user.email_address}
</p>
</div>
<div className="mb-4">
<h3 className="text-lg font-medium text-gray-900">
上传日期
</h3>
<p className="text-sm text-gray-500">
{new Date(image.created_at).toLocaleDateString()}
</p>
</div>
</div>
</div>
</div>
{/* 全屏图片模态框 */}
<Dialog.Root open={isModalOpen} onOpenChange={setIsModalOpen}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
<Dialog.Content className="fixed inset-0 flex items-center justify-center">
<div className="relative w-full h-full max-w-screen-xl max-h-screen p-4">
<img
src={image.file_url}
alt={image.title}
className="w-full h-full object-contain"
/>
<Dialog.Close asChild>
<button
className="absolute top-4 right-4 p-2 rounded-full bg-white/80 hover:bg-white text-gray-800"
aria-label="关闭"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</Dialog.Close>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</div>
)
}
export default function ComImageStatusTag({ image }) {
return (
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
image.status === 'pending'
? 'bg-yellow-100 text-yellow-800'
: image.status === 'approved'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}
>
{image.status === 'pending' ? '待审核' :
image.status === 'approved' ? '已批准' : '已拒绝'}
</span>
)
}
\ No newline at end of file
...@@ -159,7 +159,7 @@ export default function ComTagsIndex({ ...@@ -159,7 +159,7 @@ export default function ComTagsIndex({
href={`${path}/${tag.id}`} href={`${path}/${tag.id}`}
method="delete" method="delete"
as="button" as="button"
data={{ confirm: "确定要删除这个标签吗?这将从所有关联的文件中移除该标签。" }} data={{ confirm: "确定要删除这个标签吗?这将从所有关联的图片中移除该标签。" }}
className="text-xs text-red-600 hover:text-red-900" className="text-xs text-red-600 hover:text-red-900"
> >
删除 删除
...@@ -206,7 +206,7 @@ export default function ComTagsIndex({ ...@@ -206,7 +206,7 @@ export default function ComTagsIndex({
创建新标签 创建新标签
</Dialog.Title> </Dialog.Title>
<Dialog.Description className="mt-2 text-sm text-gray-500"> <Dialog.Description className="mt-2 text-sm text-gray-500">
添加一个新标签用于文件分类。 添加一个新标签用于图片分类。
</Dialog.Description> </Dialog.Description>
<Form.Root className="mt-4" onSubmit={handleCreateSubmit}> <Form.Root className="mt-4" onSubmit={handleCreateSubmit}>
......
...@@ -13,7 +13,7 @@ export default function Layout({ children, user, title }) { ...@@ -13,7 +13,7 @@ export default function Layout({ children, user, title }) {
<div className="flex"> <div className="flex">
<div className="flex-shrink-0 flex items-center"> <div className="flex-shrink-0 flex items-center">
<Link href="/" className="text-xl font-bold text-indigo-600"> <Link href="/" className="text-xl font-bold text-indigo-600">
文件管理 图片管理
</Link> </Link>
</div> </div>
<nav className="hidden sm:ml-6 sm:flex sm:space-x-8"> <nav className="hidden sm:ml-6 sm:flex sm:space-x-8">
...@@ -62,7 +62,7 @@ export default function Layout({ children, user, title }) { ...@@ -62,7 +62,7 @@ export default function Layout({ children, user, title }) {
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item className="text-sm text-gray-700 px-4 py-2 rounded hover:bg-gray-100"> <DropdownMenu.Item className="text-sm text-gray-700 px-4 py-2 rounded hover:bg-gray-100">
<Link href="/images" className="block w-full text-left"> <Link href="/images" className="block w-full text-left">
我的文件 我的图片
</Link> </Link>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Separator className="h-px bg-gray-200 my-1" /> <DropdownMenu.Separator className="h-px bg-gray-200 my-1" />
...@@ -185,7 +185,7 @@ export default function Layout({ children, user, title }) { ...@@ -185,7 +185,7 @@ export default function Layout({ children, user, title }) {
href="/images" href="/images"
className="block px-4 py-2 text-base font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100" className="block px-4 py-2 text-base font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100"
> >
我的文件 我的图片
</Link> </Link>
<Link <Link
href="/session" href="/session"
......
...@@ -5,11 +5,11 @@ import ComImageIndex from '../../../components/images/ComImageIndex' ...@@ -5,11 +5,11 @@ import ComImageIndex from '../../../components/images/ComImageIndex'
export default function AdminImagesIndex({ auth, images, pagination, filters }) { export default function AdminImagesIndex({ auth, images, pagination, filters }) {
return ( return (
<Layout user={auth.user}> <Layout user={auth.user}>
<Head title="文件管理" /> <Head title="图片管理" />
<ComImageIndex <ComImageIndex
auth={auth} auth={auth}
title="文件管理" title="图片管理"
description="审批并管理用户上传的文件" description="审批并管理用户上传的图片"
path="/admin/images" path="/admin/images"
images={images} images={images}
pagination={pagination} pagination={pagination}
......
import { Head } from '@inertiajs/react'
import Layout from '../../Layout'
import ComImageShow from '../../../components/images/ComImageShow'
export default function AdminImagesShow({ image, auth }) {
return (
<Layout user={auth}>
<Head title={`管理 - ${image.title}`} />
<ComImageShow
path={`/admin/images/${image.id}`}
image={image}
can_edit={true}
can_approve={true}
can_delete={true}
isAdmin={true}
/>
</Layout>
)
}
...@@ -8,7 +8,7 @@ export default function AdminTagsIndex({ tags, auth, errors = {} }) { ...@@ -8,7 +8,7 @@ export default function AdminTagsIndex({ tags, auth, errors = {} }) {
<Head title="标签管理" /> <Head title="标签管理" />
<ComTagsIndex <ComTagsIndex
title="标签管理" title="标签管理"
description="创建、编辑和删除文件分类标签" description="创建、编辑和删除图片分类标签"
tags={tags} tags={tags}
auth={auth} auth={auth}
isAdmin={true} isAdmin={true}
......
...@@ -5,11 +5,11 @@ import ComImageIndex from '../../../../components/images/ComImageIndex' ...@@ -5,11 +5,11 @@ import ComImageIndex from '../../../../components/images/ComImageIndex'
export default function TagsImagesIndex({ auth, tag, images, pagination, filters }) { export default function TagsImagesIndex({ auth, tag, images, pagination, filters }) {
return ( return (
<Layout user={auth.user}> <Layout user={auth.user}>
<Head title={`${tag.name} - 标签文件`} /> <Head title={`${tag.name} - 标签图片`} />
<ComImageIndex <ComImageIndex
auth={auth} auth={auth}
title={tag.name} title={tag.name}
description={`查看关联文件`} description={`查看关联图片`}
path={`/admin/tags/${tag.id}/images`} path={`/admin/tags/${tag.id}/images`}
images={images} images={images}
pagination={pagination} pagination={pagination}
...@@ -23,7 +23,7 @@ export default function TagsImagesIndex({ auth, tag, images, pagination, filters ...@@ -23,7 +23,7 @@ export default function TagsImagesIndex({ auth, tag, images, pagination, filters
href={`/admin/tags/${tag.id}/images/new`} 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 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> </Link>
)} )}
/> />
......
...@@ -32,7 +32,7 @@ export default function New({ auth, tag, errors = {} }) { ...@@ -32,7 +32,7 @@ export default function New({ auth, tag, errors = {} }) {
return ( return (
<Layout user={auth.user}> <Layout user={auth.user}>
<Head title={`添加文件到标签: ${tag.name}`} /> <Head title={`添加图片到标签: ${tag.name}`} />
<div className="bg-white shadow overflow-hidden sm:rounded-lg"> <div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 sm:px-6"> <div className="px-4 py-5 sm:px-6">
<div className="flex items-center mb-4"> <div className="flex items-center mb-4">
...@@ -43,12 +43,12 @@ export default function New({ auth, tag, errors = {} }) { ...@@ -43,12 +43,12 @@ export default function New({ auth, tag, errors = {} }) {
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg> </svg>
返回标签文件列表 返回标签图片列表
</Link> </Link>
</div> </div>
<h1 className="text-2xl font-bold text-gray-900">添加文件到标签: {tag.name}</h1> <h1 className="text-2xl font-bold text-gray-900">添加图片到标签: {tag.name}</h1>
<p className="mt-1 max-w-2xl text-sm text-gray-500"> <p className="mt-1 max-w-2xl text-sm text-gray-500">
上传新文件并自动添加到标签 "{tag.name}" 上传新图片并自动添加到标签 "{tag.name}"
</p> </p>
</div> </div>
<div className="border-t border-gray-200 px-4 py-5 sm:px-6"> <div className="border-t border-gray-200 px-4 py-5 sm:px-6">
...@@ -76,7 +76,7 @@ export default function New({ auth, tag, errors = {} }) { ...@@ -76,7 +76,7 @@ export default function New({ auth, tag, errors = {} }) {
<Form.Field name="file" className="space-y-2"> <Form.Field name="file" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700"> <Form.Label className="block text-sm font-medium text-gray-700">
文件 图片
</Form.Label> </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="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"> <div className="space-y-1 text-center">
...@@ -109,7 +109,7 @@ export default function New({ auth, tag, errors = {} }) { ...@@ -109,7 +109,7 @@ export default function New({ auth, tag, errors = {} }) {
htmlFor="file-upload" 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" 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> <span>上传图片</span>
<Form.Control asChild> <Form.Control asChild>
<input <input
id="file-upload" id="file-upload"
......
...@@ -19,14 +19,14 @@ export default function Edit({ image, tags, auth, errors = {} }) { ...@@ -19,14 +19,14 @@ export default function Edit({ image, tags, auth, errors = {} }) {
<Head title={`Edit ${image.title}`} /> <Head title={`Edit ${image.title}`} />
<div className="bg-white shadow overflow-hidden sm:rounded-lg"> <div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 sm:px-6"> <div className="px-4 py-5 sm:px-6">
<h1 className="text-2xl font-bold text-gray-900">Edit Image</h1> <h1 className="text-2xl font-bold text-gray-900">编辑图片</h1>
<p className="mt-1 max-w-2xl text-sm text-gray-500"> <p className="mt-1 max-w-2xl text-sm text-gray-500">
Update image details 更新图片信息
</p> </p>
</div> </div>
<div className="border-t border-gray-200 px-4 py-5 sm:px-6"> <div className="border-t border-gray-200 px-4 py-5 sm:px-6">
<div className="mb-6"> <div className="mb-6">
<img <image
src={image.file_url} src={image.file_url}
alt={image.title} alt={image.title}
className="max-h-64 mx-auto object-contain" className="max-h-64 mx-auto object-contain"
...@@ -36,7 +36,7 @@ export default function Edit({ image, tags, auth, errors = {} }) { ...@@ -36,7 +36,7 @@ export default function Edit({ image, tags, auth, errors = {} }) {
<Form.Root className="space-y-6" onSubmit={handleSubmit}> <Form.Root className="space-y-6" onSubmit={handleSubmit}>
<Form.Field name="title" className="space-y-2"> <Form.Field name="title" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700"> <Form.Label className="block text-sm font-medium text-gray-700">
Title 标题
</Form.Label> </Form.Label>
<Form.Control asChild> <Form.Control asChild>
<input <input
...@@ -57,7 +57,7 @@ export default function Edit({ image, tags, auth, errors = {} }) { ...@@ -57,7 +57,7 @@ export default function Edit({ image, tags, auth, errors = {} }) {
<Form.Field name="tags" className="space-y-2"> <Form.Field name="tags" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700"> <Form.Label className="block text-sm font-medium text-gray-700">
Tags 标签
</Form.Label> </Form.Label>
<Form.Control asChild> <Form.Control asChild>
<input <input
...@@ -66,11 +66,11 @@ export default function Edit({ image, tags, auth, errors = {} }) { ...@@ -66,11 +66,11 @@ export default function Edit({ image, tags, auth, errors = {} }) {
value={data.tags} value={data.tags}
onChange={(e) => setData('tags', e.target.value)} 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" 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)" placeholder=""
/> />
</Form.Control> </Form.Control>
<p className="text-xs text-gray-500"> <p className="text-xs text-gray-500">
Enter tags separated by commas {/* 请输入标签 */}
</p> </p>
</Form.Field> </Form.Field>
...@@ -79,7 +79,7 @@ export default function Edit({ image, tags, auth, errors = {} }) { ...@@ -79,7 +79,7 @@ export default function Edit({ image, tags, auth, errors = {} }) {
href={`/images/${image.id}`} href={`/images/${image.id}`}
className="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm 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" className="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm 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"
> >
Cancel 取消
</a> </a>
<Form.Submit asChild> <Form.Submit asChild>
<button <button
...@@ -87,7 +87,7 @@ export default function Edit({ image, tags, auth, errors = {} }) { ...@@ -87,7 +87,7 @@ export default function Edit({ image, tags, auth, errors = {} }) {
disabled={processing} 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" 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 ? 'Saving...' : 'Save Changes'} {processing ? '保存中...' : '保存'}
</button> </button>
</Form.Submit> </Form.Submit>
</div> </div>
......
...@@ -5,11 +5,11 @@ import { Head } from '@inertiajs/react' ...@@ -5,11 +5,11 @@ import { Head } from '@inertiajs/react'
export default function Index({ auth, images, pagination, filters }) { export default function Index({ auth, images, pagination, filters }) {
return ( return (
<Layout user={auth}> <Layout user={auth}>
<Head title="我的文件" /> <Head title="我的图片" />
<ComImageIndex <ComImageIndex
auth={auth} auth={auth}
title="我的文件" title="我的图片"
description="管理已上传的文件" description="管理已上传的图片"
path="/images" path="/images"
images={images} images={images}
pagination={pagination} pagination={pagination}
......
...@@ -33,10 +33,10 @@ export default function New({ auth, errors = {} }) { ...@@ -33,10 +33,10 @@ export default function New({ auth, errors = {} }) {
return ( return (
<Layout user={auth.user}> <Layout user={auth.user}>
<Head title="上传新文件" /> <Head title="上传新图片" />
<div className="bg-white shadow overflow-hidden sm:rounded-lg"> <div className="bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 sm:px-6"> <div className="px-4 py-5 sm:px-6">
<h1 className="text-2xl font-bold text-gray-900">上传新文件</h1> <h1 className="text-2xl font-bold text-gray-900">上传新图片</h1>
<p className="mt-1 max-w-2xl text-sm text-gray-500"> <p className="mt-1 max-w-2xl text-sm text-gray-500">
上传并等待审核 上传并等待审核
</p> </p>
...@@ -66,7 +66,7 @@ export default function New({ auth, errors = {} }) { ...@@ -66,7 +66,7 @@ export default function New({ auth, errors = {} }) {
<Form.Field name="file" className="space-y-2"> <Form.Field name="file" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700"> <Form.Label className="block text-sm font-medium text-gray-700">
文件 图片
</Form.Label> </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="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"> <div className="space-y-1 text-center">
...@@ -99,7 +99,7 @@ export default function New({ auth, errors = {} }) { ...@@ -99,7 +99,7 @@ export default function New({ auth, errors = {} }) {
htmlFor="file-upload" 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" 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> <span>上传图片</span>
<Form.Control asChild> <Form.Control asChild>
<input <input
id="file-upload" id="file-upload"
...@@ -150,7 +150,7 @@ export default function New({ auth, errors = {} }) { ...@@ -150,7 +150,7 @@ export default function New({ auth, errors = {} }) {
disabled={processing} 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" 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 ? '上传中...' : '上传文件'} {processing ? '上传中...' : '上传图片'}
</button> </button>
</Form.Submit> </Form.Submit>
</div> </div>
......
import { Head, Link } from '@inertiajs/react' import { Head } from '@inertiajs/react'
import Layout from '../Layout' import Layout from '../Layout'
import * as Dialog from '@radix-ui/react-dialog' import ComImageShow from '../../components/images/ComImageShow'
import { useState } from 'react'
export default function Show({ image, can_edit, can_approve, auth }) {
const [isModalOpen, setIsModalOpen] = useState(false)
export default function Show({ image, can_edit, can_approve, can_delete,auth }) {
return ( return (
<Layout user={auth.user}> <Layout user={auth}>
<Head title={image.title} /> <Head title={image.title} />
<div className="bg-white shadow overflow-hidden sm:rounded-lg"> <ComImageShow
<div className="px-4 py-5 sm:px-6 flex justify-between items-center"> path={`/images/${image.id}`}
<div> image={image}
<h1 className="text-2xl font-bold text-gray-900">{image.title}</h1> can_edit={can_edit}
<p className="mt-1 max-w-2xl text-sm text-gray-500"> can_approve={can_approve}
Uploaded by {image.user.name} on{' '} can_delete={can_delete}
{new Date(image.created_at).toLocaleDateString()} isAdmin={false}
</p>
</div>
<div className="flex space-x-2">
{can_edit && (
<Link
href={`/images/${image.id}/edit`}
className="inline-flex items-center px-3 py-1.5 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"
>
Edit
</Link>
)}
{can_approve && image.status === 'pending' && (
<>
<Link
href={`/images/${image.id}/approve`}
method="patch"
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"
>
Approve
</Link>
<Link
href={`/images/${image.id}/reject`}
method="patch"
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-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
>
Reject
</Link>
</>
)}
{can_approve && (
<Link
href={`/images/${image.id}`}
method="delete"
as="button"
className="inline-flex items-center px-3 py-1.5 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
onClick={(e) => {
if (!confirm('Are you sure you want to delete this image?')) {
e.preventDefault()
}
}}
>
Delete
</Link>
)}
</div>
</div>
<div className="border-t border-gray-200">
<div className="flex flex-col md:flex-row">
<div className="md:w-2/3 p-4">
<div className="relative pb-[75%]">
<img
src={image.file_url}
alt={image.title}
className="absolute h-full w-full object-contain cursor-pointer"
onClick={() => setIsModalOpen(true)}
/>
</div>
</div>
<div className="md:w-1/3 p-4 border-t md:border-t-0 md:border-l border-gray-200">
<div className="mb-4">
<h3 className="text-lg font-medium text-gray-900">Status</h3>
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
image.status === 'pending'
? 'bg-yellow-100 text-yellow-800'
: image.status === 'approved'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}
>
{image.status}
</span>
</div>
<div className="mb-4">
<h3 className="text-lg font-medium text-gray-900">Tags</h3>
<div className="mt-2 flex flex-wrap gap-1">
{image.tags.length > 0 ? (
image.tags.map((tag) => (
<span
key={tag.id}
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-indigo-100 text-indigo-800"
>
{tag.name}
</span>
))
) : (
<p className="text-sm text-gray-500">No tags</p>
)}
</div>
</div>
<div className="mb-4">
<h3 className="text-lg font-medium text-gray-900">Uploader</h3>
<p className="text-sm text-gray-500">{image.user.name}</p>
<p className="text-sm text-gray-500">
{image.user.email_address}
</p>
</div>
<div className="mb-4">
<h3 className="text-lg font-medium text-gray-900">
Upload Date
</h3>
<p className="text-sm text-gray-500">
{new Date(image.created_at).toLocaleDateString()}
</p>
</div>
</div>
</div>
</div>
</div>
{/* Full screen image modal */}
<Dialog.Root open={isModalOpen} onOpenChange={setIsModalOpen}>
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/50" />
<Dialog.Content className="fixed inset-0 flex items-center justify-center">
<div className="relative w-full h-full max-w-screen-xl max-h-screen p-4">
<img
src={image.file_url}
alt={image.title}
className="w-full h-full object-contain"
/>
<Dialog.Close asChild>
<button
className="absolute top-4 right-4 p-2 rounded-full bg-white/80 hover:bg-white text-gray-800"
aria-label="Close"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/> />
</svg>
</button>
</Dialog.Close>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</Layout> </Layout>
) )
} }
...@@ -9,7 +9,7 @@ class Image < ApplicationRecord ...@@ -9,7 +9,7 @@ class Image < ApplicationRecord
validates :file, presence: true, on: :create validates :file, presence: true, on: :create
# Status for review process # Status for review process
# enum status: { pending: 0, approved: 1, rejected: 2 }, _default: :pending enum :status, { pending: 0, approved: 1, rejected: 2 }, default: :pending
# Scopes for filtering # Scopes for filtering
scope :pending, -> { where(status: :pending) } scope :pending, -> { where(status: :pending) }
...@@ -21,7 +21,7 @@ class Image < ApplicationRecord ...@@ -21,7 +21,7 @@ class Image < ApplicationRecord
end end
def self.ransackable_associations(auth_object = nil) def self.ransackable_associations(auth_object = nil)
["image_tags", "tags", "user"] [ "image_tags", "tags", "user" ]
end end
# Returns the URL for the attached file # Returns the URL for the attached file
......
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