Commit 82241f04 by Ivan

feat: 样式与页面补充

parent 5495beb5
...@@ -56,10 +56,13 @@ class Admin::ImagesController < ApplicationController ...@@ -56,10 +56,13 @@ class Admin::ImagesController < ApplicationController
def new def new
if params[:tag_id].present? if params[:tag_id].present?
render inertia: "admin/tags/images/New", props: { 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 else
render inertia: "admin/images/New" render inertia: "admin/images/New", props: {
tags: Tag.all.as_json
}
end end
end end
...@@ -67,11 +70,11 @@ class Admin::ImagesController < ApplicationController ...@@ -67,11 +70,11 @@ class Admin::ImagesController < ApplicationController
@image = Current.user.images.new(image_create_params) @image = Current.user.images.new(image_create_params)
if @image.save if @image.save
if params[:tag_ids].present? # 处理标签 - 从表单数据中获取tag_ids
# 使用新的 set_tags_by_ids 方法来设置标签 if params[:image] && params[:image][:tag_ids].present?
@image.set_tags_by_ids(params[:tag_ids]) @image.set_tags_by_ids(params[:image][:tag_ids])
end end
# Add the tag if we're creating from a tag context # Add the tag if we're creating from a tag context
if params[:tag_id].present? && @tag if params[:tag_id].present? && @tag
@image.tags << @tag unless @image.tags.include?(@tag) @image.tags << @tag unless @image.tags.include?(@tag)
...@@ -98,10 +101,9 @@ class Admin::ImagesController < ApplicationController ...@@ -98,10 +101,9 @@ class Admin::ImagesController < ApplicationController
def update def update
if @image.update(image_params) if @image.update(image_params)
# 处理标签 # 处理标签 - 从表单数据中获取tag_ids
if params[:tag_ids].present? if params[:image] && params[:image][:tag_ids].present?
# 使用新的 set_tags_by_ids 方法来设置标签 @image.set_tags_by_ids(params[:image][:tag_ids])
@image.set_tags_by_ids(params[:tag_ids])
end end
redirect_to admin_image_path(@image), notice: "Image was successfully updated." redirect_to admin_image_path(@image), notice: "Image was successfully updated."
...@@ -197,11 +199,11 @@ class Admin::ImagesController < ApplicationController ...@@ -197,11 +199,11 @@ class Admin::ImagesController < ApplicationController
end end
def image_params def image_params
params.require(:image).permit(:title) params.require(:image).permit(:title, :status, tag_ids: [])
end end
def image_create_params def image_create_params
params.require(:image).permit(:title, :file) params.require(:image).permit(:title, :file, :status, tag_ids: [])
end end
def authorize_admin def authorize_admin
......
...@@ -3,20 +3,39 @@ class ImagesController < ApplicationController ...@@ -3,20 +3,39 @@ 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 :set_tag, only: [ :index ], if: -> { params[:tag_id].present? }
def index def index
@q = Current.user.images.includes(:user, :tags).ransack(params[:q]) if params[:tag_id].present?
@images = @q.result(distinct: true).with_attached_file.order(created_at: :desc).page(params[:page]).per(12) # When accessed via /tags/:tag_id/images
@q = @tag.images.includes(:user, :tags).ransack(params[:q])
render inertia: "images/Index", props: { @images = @q.result(distinct: true).with_attached_file.order(created_at: :desc).page(params[:page]).per(12)
images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]),
filters: params[:q] || {}, render inertia: "tags/images/Index", props: {
pagination: { tag: @tag.as_json(only: [ :id, :name, :catalog ], methods: [ :images_count ]),
current_page: @images.current_page, images: @images.as_json(include: [ :user, :tags ], methods: [ :file_url, :thumbnail_url, :medium_url ]),
total_pages: @images.total_pages, filters: params[:q] || {},
total_count: @images.total_count 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 end
def show def show
...@@ -30,15 +49,18 @@ class ImagesController < ApplicationController ...@@ -30,15 +49,18 @@ class ImagesController < ApplicationController
def new def new
@image = Image.new @image = Image.new
render inertia: "images/New" render inertia: "images/New", props: {
tags: Tag.all.as_json
}
end end
def create def create
@image = Current.user.images.new(image_params) @image = Current.user.images.new(image_params)
if @image.save if @image.save
if params[:tag_ids].present? # 处理标签 - 从表单数据中获取tag_ids
@image.set_tags_by_ids(params[:tag_ids]) if params[:image] && params[:image][:tag_ids].present?
@image.set_tags_by_ids(params[:image][:tag_ids])
end end
redirect_to image_path(@image), notice: "Image was successfully uploaded and is pending review." redirect_to image_path(@image), notice: "Image was successfully uploaded and is pending review."
else else
...@@ -69,8 +91,9 @@ class ImagesController < ApplicationController ...@@ -69,8 +91,9 @@ class ImagesController < ApplicationController
end end
if @image.update(image_params) if @image.update(image_params)
if params[:tag_ids].present? # 处理标签 - 从表单数据中获取tag_ids
@image.set_tags_by_ids(params[:tag_ids]) if params[:image] && params[:image][:tag_ids].present?
@image.set_tags_by_ids(params[:image][:tag_ids])
end end
redirect_to image_path(@image), notice: "Image was successfully updated." redirect_to image_path(@image), notice: "Image was successfully updated."
else else
...@@ -103,8 +126,12 @@ class ImagesController < ApplicationController ...@@ -103,8 +126,12 @@ class ImagesController < ApplicationController
@image = Current.user.images.find(params[:id]) @image = Current.user.images.find(params[:id])
end end
def set_tag
@tag = Tag.find(params[:tag_id])
end
def image_params def image_params
params.require(:image).permit(:title, :file) params.require(:image).permit(:title, :file, tag_ids: [])
end end
def authorize_user 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 { Link } from "@inertiajs/react";
import ComImageStatusTag from "./ComImageStatusTag"; import ComImageStatusTag from "./ComImageStatusTag";
export function ComImageCard({ image, path, showActions = false }) { export function ComImageCard({ image, path, showUserName = false}) {
return ( return (
<div <div
className="bg-white overflow-hidden shadow rounded-lg flex flex-col" className="bg-white overflow-hidden shadow rounded-lg flex flex-col"
...@@ -18,6 +18,11 @@ export function ComImageCard({ image, path, showActions = false }) { ...@@ -18,6 +18,11 @@ export function ComImageCard({ image, path, showActions = false }) {
</div> </div>
</div> </div>
<div className="px-4 py-4 flex flex-col justify-between flex-grow"> <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"> <h3 className="text-lg font-medium text-gray-900 truncate">
{image.title} {image.title}
</h3> </h3>
......
...@@ -5,7 +5,7 @@ import * as Collapsible from '@radix-ui/react-collapsible' ...@@ -5,7 +5,7 @@ import * as Collapsible from '@radix-ui/react-collapsible'
import { uniqBy } from 'lodash-es' import { uniqBy } from 'lodash-es'
import { ComImageCard } from './ComImageCard' 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 [allImages, setAllImages] = useState(images || [])
const [page, setPage] = useState(pagination?.current_page || 1) const [page, setPage] = useState(pagination?.current_page || 1)
const [maxVisiblePage, setMaxVisiblePage] = useState(0) const [maxVisiblePage, setMaxVisiblePage] = useState(0)
...@@ -148,10 +148,10 @@ export default function ComImageIndex({ title, description, path, images, pagina ...@@ -148,10 +148,10 @@ export default function ComImageIndex({ title, description, path, images, pagina
{description} {description}
</p> </p>
</div> </div>
<div className="flex space-x-2"> <div className="flex flex-col space-y-0 sm:flex-row">
<button <button
onClick={() => setIsFilterOpen(!isFilterOpen)} 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"> <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" /> <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 ...@@ -163,7 +163,7 @@ export default function ComImageIndex({ title, description, path, images, pagina
) : auth && ( ) : auth && (
<Link <Link
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 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> </Link>
...@@ -292,6 +292,7 @@ export default function ComImageIndex({ title, description, path, images, pagina ...@@ -292,6 +292,7 @@ export default function ComImageIndex({ title, description, path, images, pagina
ref={index === allImages.length - 1 ? lastImageElementRef : null} ref={index === allImages.length - 1 ? lastImageElementRef : null}
image={image} image={image}
path={path} path={path}
showUserName={showUserName}
// showActions={showActions} // showActions={showActions}
/> />
))} ))}
......
...@@ -7,16 +7,16 @@ export default function ComImageShow({ path, image, can_edit, can_approve, isAdm ...@@ -7,16 +7,16 @@ export default function ComImageShow({ path, image, can_edit, can_approve, isAdm
const [isModalOpen, setIsModalOpen] = useState(false) const [isModalOpen, setIsModalOpen] = useState(false)
return ( return (
<div className="bg-white shadow overflow-hidden sm:rounded-lg"> <div className="w-full bg-white shadow overflow-hidden sm:rounded-lg">
<div className="px-4 py-5 sm:px-6 flex justify-between items-center"> <div className="w-full px-4 py-5 sm:px-6 flex justify-between items-center">
<div> <div className="flex-grow w-0">
<h1 className="text-2xl font-bold text-gray-900">{image.title}</h1> <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"> <p className="mt-1 max-w-2xl text-sm text-gray-500">
{image.user.name}{' '} {image.user.name}{' '}
{new Date(image.created_at).toLocaleDateString()}{' '}上传 {new Date(image.created_at).toLocaleDateString()}{' '}上传
</p> </p>
</div> </div>
<div className="flex space-x-2"> <div className="flex-shrink-0 grid grid-cols-2 gap-2">
{can_edit && ( {can_edit && (
<Link <Link
href={`${path}/edit`} href={`${path}/edit`}
...@@ -33,7 +33,7 @@ export default function ComImageShow({ path, image, can_edit, can_approve, isAdm ...@@ -33,7 +33,7 @@ export default function ComImageShow({ path, image, can_edit, can_approve, isAdm
as="button" 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" 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>
<Link <Link
href={`${path}/reject`} href={`${path}/reject`}
......
...@@ -10,7 +10,7 @@ export default function ComImageStatusTag({ image }) { ...@@ -10,7 +10,7 @@ export default function ComImageStatusTag({ image }) {
}`} }`}
> >
{image.status === 'pending' ? '待审核' : {image.status === 'pending' ? '待审核' :
image.status === 'approved' ? '已批准' : '已拒绝'} image.status === 'approved' ? '已通过' : '已拒绝'}
</span> </span>
) )
} }
\ No newline at end of file
...@@ -144,7 +144,7 @@ export default function ComTagsIndex({ ...@@ -144,7 +144,7 @@ export default function ComTagsIndex({
href={`${path}/${tag.id}/images`} 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" 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> </Link>
</div> </div>
......
import { useState } from 'react' import { useState } from 'react'
import { Link } from '@inertiajs/react' import { Link } from '@inertiajs/react'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { AcademicCapIcon } from '@heroicons/react/24/outline'
export default function Layout({ children, user, title }) { export default function Layout({ children, user, title }) {
const [isMenuOpen, setIsMenuOpen] = useState(false) const [isMenuOpen, setIsMenuOpen] = useState(false)
...@@ -13,7 +14,7 @@ export default function Layout({ children, user, title }) { ...@@ -13,7 +14,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">
图片管理 <AcademicCapIcon className="h-6 w-6" />
</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">
...@@ -31,12 +32,28 @@ export default function Layout({ children, user, title }) { ...@@ -31,12 +32,28 @@ export default function Layout({ children, user, title }) {
上传 上传
</Link> </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') && ( {user?.roles.includes('admin') && (
<Link <Link
href="/admin/images" 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" 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> </Link>
)} )}
</nav> </nav>
...@@ -154,12 +171,28 @@ export default function Layout({ children, user, title }) { ...@@ -154,12 +171,28 @@ export default function Layout({ children, user, title }) {
上传 上传
</Link> </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') && ( {user?.roles?.includes('admin') && (
<Link <Link
href="/admin/images" 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" 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> </Link>
)} )}
</div> </div>
......
...@@ -14,6 +14,7 @@ export default function AdminImagesIndex({ auth, images, pagination, filters }) ...@@ -14,6 +14,7 @@ export default function AdminImagesIndex({ auth, images, pagination, filters })
images={images} images={images}
pagination={pagination} pagination={pagination}
filters={filters} filters={filters}
showUserName={true}
/> />
</Layout> </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 ...@@ -14,6 +14,7 @@ export default function TagsImagesIndex({ auth, tag, images, pagination, filters
images={images} images={images}
pagination={pagination} pagination={pagination}
filters={filters} filters={filters}
showUserName={true}
backLink={{ backLink={{
url: '/admin/tags', url: '/admin/tags',
label: '返回标签列表' label: '返回标签列表'
...@@ -21,7 +22,7 @@ export default function TagsImagesIndex({ auth, tag, images, pagination, filters ...@@ -21,7 +22,7 @@ export default function TagsImagesIndex({ auth, tag, images, pagination, filters
actionButton={( actionButton={(
<Link <Link
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 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> </Link>
......
import { useState } from 'react'
import { Head, useForm, router, Link } from '@inertiajs/react' import { Head, useForm, router, Link } from '@inertiajs/react'
import Layout from '../../../Layout' import Layout from '../../../Layout'
import * as Form from '@radix-ui/react-form' import ImageUploadForm from '../../../../components/ImageUploadForm'
export default function New({ auth, tag, errors = {} }) { export default function New({ auth, tag, tags, errors = {} }) {
const [preview, setPreview] = useState(null)
const { data, setData, processing } = useForm({ const { data, setData, processing } = useForm({
title: '', title: '',
file: null, file: null,
tag_ids: [tag.id], // Pre-select the current tag
status: 'approved'
}) })
const handleSubmit = (e) => { const handleSubmit = (e) => {
...@@ -15,21 +15,6 @@ export default function New({ auth, tag, errors = {} }) { ...@@ -15,21 +15,6 @@ export default function New({ auth, tag, errors = {} }) {
router.post(`/admin/tags/${tag.id}/images`, { image: data }) 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 ( return (
<Layout user={auth}> <Layout user={auth}>
<Head title={`添加图片到标签: ${tag.name}`} /> <Head title={`添加图片到标签: ${tag.name}`} />
...@@ -52,100 +37,18 @@ export default function New({ auth, tag, errors = {} }) { ...@@ -52,100 +37,18 @@ export default function New({ auth, tag, errors = {} }) {
</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">
<Form.Root className="space-y-6" onSubmit={handleSubmit}> <ImageUploadForm
<Form.Field name="title" className="space-y-2"> data={data}
<Form.Label className="block text-sm font-medium text-gray-700"> setData={setData}
标题 processing={processing}
</Form.Label> errors={errors}
<Form.Control asChild> onSubmit={handleSubmit}
<input submitButtonText="上传并添加到标签"
type="text" processingButtonText="上传中..."
name="title" showTagsField={true}
value={data.title} tags={tags}
onChange={(e) => setData('title', e.target.value)} cancelUrl={`/admin/tags/${tag.id}/images`}
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>
</div> </div>
</div> </div>
</Layout> </Layout>
......
import { Head } from '@inertiajs/react' import { Head, useForm } from '@inertiajs/react'
import Layout from '../Layout' import Layout from '../Layout'
import ComImageEdit from '../../components/images/ComImageEdit' import ImageUploadForm from '../../components/ImageUploadForm'
export default function Edit({ image, tags, auth, errors = {} }) { 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 ( return (
<Layout user={auth}> <Layout user={auth}>
<Head title={`Edit ${image.title}`} /> <Head title={`编辑 ${image.title}`} />
<ComImageEdit <div className="bg-white shadow overflow-hidden sm:rounded-lg">
image={image} <div className="px-4 py-5 sm:px-6">
tags={tags} <h1 className="text-2xl font-bold text-gray-900">编辑图片</h1>
path={`/images/${image.id}`} <p className="mt-1 max-w-2xl text-sm text-gray-500">
isAdmin={false} 更新图片信息
/> </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> </Layout>
) )
} }
import { useState } from 'react'
import { Head, useForm, router } from '@inertiajs/react' import { Head, useForm, router } from '@inertiajs/react'
import Layout from '../Layout' import Layout from '../Layout'
import * as Form from '@radix-ui/react-form' import ImageUploadForm from '../../components/ImageUploadForm'
export default function New({ auth, errors = {} }) { export default function New({ auth, tags = [], errors = {} }) {
const [preview, setPreview] = useState(null)
const { data, setData, processing } = useForm({ const { data, setData, processing } = useForm({
title: '', title: '',
file: null, file: null,
tags: '', tag_ids: [],
}) })
const handleSubmit = (e) => { const handleSubmit = (e) => {
...@@ -16,21 +14,6 @@ export default function New({ auth, errors = {} }) { ...@@ -16,21 +14,6 @@ export default function New({ auth, errors = {} }) {
router.post('/images', { image: data }) 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 ( return (
<Layout user={auth}> <Layout user={auth}>
<Head title="上传新图片" /> <Head title="上传新图片" />
...@@ -42,119 +25,17 @@ export default function New({ auth, errors = {} }) { ...@@ -42,119 +25,17 @@ export default function New({ auth, errors = {} }) {
</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">
<Form.Root className="space-y-6" onSubmit={handleSubmit}> <ImageUploadForm
<Form.Field name="title" className="space-y-2"> data={data}
<Form.Label className="block text-sm font-medium text-gray-700"> setData={setData}
标题 processing={processing}
</Form.Label> errors={errors}
<Form.Control asChild> onSubmit={handleSubmit}
<input submitButtonText="上传图片"
type="text" processingButtonText="上传中..."
name="title" showTagsField={false}
value={data.title} cancelUrl="/images"
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>
</div> </div>
</div> </div>
</Layout> </Layout>
......
...@@ -5,10 +5,10 @@ import ComTagsIndex from '../../components/tags/ComTagsIndex' ...@@ -5,10 +5,10 @@ import ComTagsIndex from '../../components/tags/ComTagsIndex'
export default function TagsIndex({ tags, auth }) { export default function TagsIndex({ tags, auth }) {
return ( return (
<Layout user={auth}> <Layout user={auth}>
<Head title="Tags" /> <Head title="标签" />
<ComTagsIndex <ComTagsIndex
title="Tags" title="标签"
description="标签管理" description="标签总览"
tags={tags} tags={tags}
auth={auth} auth={auth}
isAdmin={false} 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 ...@@ -45,14 +45,6 @@ class Image < ApplicationRecord
# Rails.application.routes.url_helpers.rails_blob_path(variant, only_path: true) # Rails.application.routes.url_helpers.rails_blob_path(variant, only_path: true)
end 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 # Method to set tags by IDs
def set_tags_by_ids(tag_ids) def set_tags_by_ids(tag_ids)
return if tag_ids.nil? return if tag_ids.nil?
......
...@@ -45,7 +45,9 @@ Rails.application.routes.draw do ...@@ -45,7 +45,9 @@ Rails.application.routes.draw do
end end
# Tag management routes # 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 # Admin namespace for admin-only actions
namespace :admin do namespace :admin do
......
...@@ -4,11 +4,13 @@ ...@@ -4,11 +4,13 @@
"watchAdditionalPaths": [] "watchAdditionalPaths": []
}, },
"development": { "development": {
"host": "127.0.0.1",
"autoBuild": true, "autoBuild": true,
"publicOutputDir": "vite-dev", "publicOutputDir": "vite-dev",
"port": 3036 "port": 3036
}, },
"test": { "test": {
"host": "127.0.0.1",
"autoBuild": true, "autoBuild": true,
"publicOutputDir": "vite-test", "publicOutputDir": "vite-test",
"port": 3037 "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