Commit c139b140 by Ivan

feat: update images

parent 75d4a205
......@@ -97,7 +97,7 @@ class Admin::ImagesController < ApplicationController
end
def destroy
@image.destroy
@image.destroy!
redirect_to admin_images_path, notice: "Image was successfully deleted."
end
......
......@@ -3,10 +3,9 @@ class ImagesController < ApplicationController
# allow_unauthenticated_access only: [:index, :show, :search]
before_action :set_image, only: [ :show, :edit, :update, :destroy, :approve, :reject ]
before_action :authorize_admin, only: [ :approve, :reject, :destroy ]
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)
render inertia: "images/Index", props: {
......@@ -23,8 +22,9 @@ class ImagesController < ApplicationController
def show
render inertia: "images/Show", props: {
image: @image.as_json(include: [ :user, :tags ], methods: [ :file_url ]),
can_edit: Current.user == @image.user,
can_approve: Current.user&.admin?
can_edit: Current.user == @image.user && @image.status == "pending",
can_approve: Current.user&.admin?,
can_delete: Current.user == @image.user && @image.status == "pending" || Current.user&.admin?
}
end
......@@ -60,6 +60,10 @@ class ImagesController < ApplicationController
def update
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 params[:tags].present?
@image.tags.clear
......@@ -80,17 +84,6 @@ class ImagesController < ApplicationController
redirect_to images_path, notice: "Image was successfully deleted."
end
def search
@q = Image.approved.includes(:user, :tags).ransack(params[:q])
@images = @q.result(distinct: true).with_attached_file.order(created_at: :desc).page(params[:page]).per(12)
render inertia: "images/Search", props: {
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url ]),
filters: params[:q] || {},
tags: Tag.all.pluck(:name)
}
end
def approve
@image.approved!
redirect_to image_path(@image), notice: "Image was successfully approved."
......@@ -104,7 +97,7 @@ class ImagesController < ApplicationController
private
def set_image
@image = Image.find(params[:id])
@image = Current.user.images.find(params[:id])
end
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'
import * as Form from '@radix-ui/react-form'
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 }) {
const [allImages, setAllImages] = useState(images || [])
......@@ -164,7 +165,7 @@ export default function ComImageIndex({ title, description, path, images, pagina
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"
>
上传新文件
上传新图片
</Link>
)}
</div>
......@@ -286,59 +287,13 @@ export default function ComImageIndex({ title, description, path, images, pagina
</div>
)}
{allImages.map((image, index) => (
<div
<ComImageCard
key={image.id}
ref={index === allImages.length - 1 ? lastImageElementRef : null}
className="bg-white overflow-hidden shadow rounded-lg"
>
<div className="relative pb-[75%]">
<img
loading="lazy"
src={image.file_url}
alt={image.title}
className="absolute h-full w-full object-cover"
/>
<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>
image={image}
path={path}
// showActions={showActions}
/>
))}
</div>
) : (
......@@ -367,17 +322,17 @@ export default function ComImageIndex({ title, description, path, images, pagina
/>
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">
暂无文件
暂无图片
</h3>
<p className="mt-1 text-sm text-gray-500">
开始上传文件
开始上传图片
</p>
<div className="mt-6">
<Link
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"
>
上传新文件
上传新图片
</Link>
</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({
href={`${path}/${tag.id}`}
method="delete"
as="button"
data={{ confirm: "确定要删除这个标签吗?这将从所有关联的文件中移除该标签。" }}
data={{ confirm: "确定要删除这个标签吗?这将从所有关联的图片中移除该标签。" }}
className="text-xs text-red-600 hover:text-red-900"
>
删除
......@@ -206,7 +206,7 @@ export default function ComTagsIndex({
创建新标签
</Dialog.Title>
<Dialog.Description className="mt-2 text-sm text-gray-500">
添加一个新标签用于文件分类。
添加一个新标签用于图片分类。
</Dialog.Description>
<Form.Root className="mt-4" onSubmit={handleCreateSubmit}>
......
......@@ -13,7 +13,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">
文件管理
图片管理
</Link>
</div>
<nav className="hidden sm:ml-6 sm:flex sm:space-x-8">
......@@ -62,7 +62,7 @@ export default function Layout({ children, user, title }) {
</DropdownMenu.Item>
<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>
</DropdownMenu.Item>
<DropdownMenu.Separator className="h-px bg-gray-200 my-1" />
......@@ -185,7 +185,7 @@ export default function Layout({ children, user, title }) {
href="/images"
className="block px-4 py-2 text-base font-medium text-gray-500 hover:text-gray-800 hover:bg-gray-100"
>
我的文件
我的图片
</Link>
<Link
href="/session"
......
......@@ -5,11 +5,11 @@ import ComImageIndex from '../../../components/images/ComImageIndex'
export default function AdminImagesIndex({ auth, images, pagination, filters }) {
return (
<Layout user={auth.user}>
<Head title="文件管理" />
<Head title="图片管理" />
<ComImageIndex
auth={auth}
title="文件管理"
description="审批并管理用户上传的文件"
title="图片管理"
description="审批并管理用户上传的图片"
path="/admin/images"
images={images}
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 = {} }) {
<Head title="标签管理" />
<ComTagsIndex
title="标签管理"
description="创建、编辑和删除文件分类标签"
description="创建、编辑和删除图片分类标签"
tags={tags}
auth={auth}
isAdmin={true}
......
......@@ -5,11 +5,11 @@ import ComImageIndex from '../../../../components/images/ComImageIndex'
export default function TagsImagesIndex({ auth, tag, images, pagination, filters }) {
return (
<Layout user={auth.user}>
<Head title={`${tag.name} - 标签文件`} />
<Head title={`${tag.name} - 标签图片`} />
<ComImageIndex
auth={auth}
title={tag.name}
description={`查看关联文件`}
description={`查看关联图片`}
path={`/admin/tags/${tag.id}/images`}
images={images}
pagination={pagination}
......@@ -23,7 +23,7 @@ export default function TagsImagesIndex({ auth, tag, images, pagination, filters
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"
>
添加新文件到此标签
添加新图片到此标签
</Link>
)}
/>
......
......@@ -32,7 +32,7 @@ export default function New({ auth, tag, errors = {} }) {
return (
<Layout user={auth.user}>
<Head title={`添加文件到标签: ${tag.name}`} />
<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">
......@@ -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">
<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>
<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}"
上传新图片并自动添加到标签 "{tag.name}"
</p>
</div>
<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 = {} }) {
<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">
......@@ -109,7 +109,7 @@ export default function New({ auth, tag, errors = {} }) {
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>
<span>上传图片</span>
<Form.Control asChild>
<input
id="file-upload"
......
......@@ -19,14 +19,14 @@ export default function Edit({ image, tags, auth, errors = {} }) {
<Head title={`Edit ${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">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">
Update image details
更新图片信息
</p>
</div>
<div className="border-t border-gray-200 px-4 py-5 sm:px-6">
<div className="mb-6">
<img
<image
src={image.file_url}
alt={image.title}
className="max-h-64 mx-auto object-contain"
......@@ -36,7 +36,7 @@ export default function Edit({ image, tags, auth, errors = {} }) {
<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">
Title
标题
</Form.Label>
<Form.Control asChild>
<input
......@@ -57,7 +57,7 @@ export default function Edit({ image, tags, auth, errors = {} }) {
<Form.Field name="tags" className="space-y-2">
<Form.Label className="block text-sm font-medium text-gray-700">
Tags
标签
</Form.Label>
<Form.Control asChild>
<input
......@@ -66,11 +66,11 @@ export default function Edit({ image, tags, auth, errors = {} }) {
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)"
placeholder=""
/>
</Form.Control>
<p className="text-xs text-gray-500">
Enter tags separated by commas
{/* 请输入标签 */}
</p>
</Form.Field>
......@@ -79,7 +79,7 @@ export default function Edit({ image, tags, auth, errors = {} }) {
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"
>
Cancel
取消
</a>
<Form.Submit asChild>
<button
......@@ -87,7 +87,7 @@ export default function Edit({ image, tags, auth, errors = {} }) {
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 ? 'Saving...' : 'Save Changes'}
{processing ? '保存中...' : '保存'}
</button>
</Form.Submit>
</div>
......
......@@ -5,11 +5,11 @@ import { Head } from '@inertiajs/react'
export default function Index({ auth, images, pagination, filters }) {
return (
<Layout user={auth}>
<Head title="我的文件" />
<Head title="我的图片" />
<ComImageIndex
auth={auth}
title="我的文件"
description="管理已上传的文件"
title="我的图片"
description="管理已上传的图片"
path="/images"
images={images}
pagination={pagination}
......
......@@ -33,10 +33,10 @@ export default function New({ auth, errors = {} }) {
return (
<Layout user={auth.user}>
<Head title="上传新文件" />
<Head 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>
<h1 className="text-2xl font-bold text-gray-900">上传新图片</h1>
<p className="mt-1 max-w-2xl text-sm text-gray-500">
上传并等待审核
</p>
......@@ -66,7 +66,7 @@ export default function New({ auth, errors = {} }) {
<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">
......@@ -99,7 +99,7 @@ export default function New({ auth, errors = {} }) {
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>
<span>上传图片</span>
<Form.Control asChild>
<input
id="file-upload"
......@@ -150,7 +150,7 @@ export default function New({ auth, errors = {} }) {
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 ? '上传中...' : '上传文件'}
{processing ? '上传中...' : '上传图片'}
</button>
</Form.Submit>
</div>
......
import { Head, Link } from '@inertiajs/react'
import { Head } from '@inertiajs/react'
import Layout from '../Layout'
import * as Dialog from '@radix-ui/react-dialog'
import { useState } from 'react'
export default function Show({ image, can_edit, can_approve, auth }) {
const [isModalOpen, setIsModalOpen] = useState(false)
import ComImageShow from '../../components/images/ComImageShow'
export default function Show({ image, can_edit, can_approve, can_delete,auth }) {
return (
<Layout user={auth.user}>
<Layout user={auth}>
<Head title={image.title} />
<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">
Uploaded by {image.user.name} on{' '}
{new Date(image.created_at).toLocaleDateString()}
</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>
<ComImageShow
path={`/images/${image.id}`}
image={image}
can_edit={can_edit}
can_approve={can_approve}
can_delete={can_delete}
isAdmin={false}
/>
</Layout>
)
}
......@@ -9,7 +9,7 @@ class Image < ApplicationRecord
validates :file, presence: true, on: :create
# 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
scope :pending, -> { where(status: :pending) }
......@@ -21,7 +21,7 @@ class Image < ApplicationRecord
end
def self.ransackable_associations(auth_object = nil)
["image_tags", "tags", "user"]
[ "image_tags", "tags", "user" ]
end
# 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