Skip to content

Commit

Permalink
feat: 图片批量删除
Browse files Browse the repository at this point in the history
  • Loading branch information
besscroft committed Sep 24, 2024
1 parent 9ce9c06 commit c27d487
Show file tree
Hide file tree
Showing 9 changed files with 318 additions and 20 deletions.
14 changes: 14 additions & 0 deletions app/api/v1/image-batch-delete/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'server-only'
import { deleteBatchImage } from '~/server/lib/operate'
import { NextRequest } from 'next/server'

export async function DELETE(req: NextRequest) {
try {
const data = await req.json()
await deleteBatchImage(data);
return Response.json({ code: 200, message: '删除成功!' })
} catch (e) {
console.log(e)
return Response.json({ code: 500, message: '删除失败!' })
}
}
136 changes: 136 additions & 0 deletions components/admin/list/ImageBatchDeleteSheet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
'use client'

import { DataProps, ImageServerHandleProps, ImageType } from '~/types'
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '~/components/ui/Sheet'
import React, { useState } from 'react'
import { useButtonStore } from '~/app/providers/button-store-Providers'
import { Select, Space } from 'antd'
import { toast } from 'sonner'
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from '@nextui-org/react'
import { useSWRInfiniteServerHook } from '~/hooks/useSWRInfiniteServerHook'
import ListImage from '~/components/admin/list/ListImage'

export default function ImageBatchDeleteSheet(props : Readonly<ImageServerHandleProps & { dataProps: DataProps } & { pageNum: number } & { tag: string }>) {
const { dataProps, pageNum, tag, ...restProps } = props
const { mutate } = useSWRInfiniteServerHook(restProps, pageNum, tag)
const { imageBatchDelete, setImageBatchDelete } = useButtonStore(
(state) => state,
)
const [isOpen, setIsOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [data, setData] = useState([] as any[])

const fieldNames = { label: 'name', value: 'id' }

async function submit() {
if (data.length === 0) {
toast.warning('请选择要删除的图片')
return
}
try {
setLoading(true)
await fetch('/api/v1/image-batch-delete', {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
method: 'DELETE',
}).then(response => response.json())
toast.success('删除成功!')
setImageBatchDelete(false)
setData([])
await mutate()
} catch (e) {
toast.error('删除失败!')
} finally {
setLoading(false)
setIsOpen(false)
}
}

return (
<Sheet
defaultOpen={false}
open={imageBatchDelete}
onOpenChange={(open: boolean) => {
if (!open) {
setImageBatchDelete(false)
setData([])
}
}}
modal={false}
>
<SheetContent side="left" onInteractOutside={(event: any) => event.preventDefault()}>
<SheetHeader>
<SheetTitle>批量删除</SheetTitle>
<SheetDescription className="space-y-2">
{
Array.isArray(data) && data.length > 0 &&
<div className="grid grid-cols-3">
{dataProps.data.filter((item: ImageType) => data.includes(item.id)).map((image: ImageType) => (
<ListImage key={image.id} image={image} />
))}
</div>
}
<Select
mode="multiple"
style={{ width: '100%' }}
placeholder="选择您要删除的图片"
onChange={(value: any) => setData(value)}
options={dataProps.data}
fieldNames={fieldNames}
optionRender={(option) => (
<Space>
<span role="img" aria-label={option.data.id}>
id: {option.data.id}
</span>
name: {option.data.name}
</Space>
)}
/>
<Button
color="primary"
variant="shadow"
onClick={() => setIsOpen(true)}
aria-label="更新"
>
更新
</Button>
</SheetDescription>
</SheetHeader>
</SheetContent>
<Modal
isOpen={isOpen}
hideCloseButton
placement="center"
>
<ModalContent>
<ModalHeader className="flex flex-col gap-1">确定要删掉?</ModalHeader>
<ModalBody>
<p>图片 ID:{JSON.stringify(data)}</p>
</ModalBody>
<ModalFooter>
<Button
color="danger"
variant="flat"
onClick={() => {
setIsOpen(false)
}}
aria-label="不删除"
>
算了
</Button>
<Button
color="primary"
isLoading={loading}
onClick={() => submit()}
aria-label="确认删除"
>
是的
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Sheet>
)
}
68 changes: 48 additions & 20 deletions components/admin/list/ListProps.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import React, { useState } from 'react'
import { ImageServerHandleProps, ImageType, TagType } from '~/types'
import { DataProps, ImageServerHandleProps, ImageType, TagType } from '~/types'
import { useSWRInfiniteServerHook } from '~/hooks/useSWRInfiniteServerHook'
import { useSWRPageTotalServerHook } from '~/hooks/useSWRPageTotalServerHook'
import {
Expand Down Expand Up @@ -41,6 +41,7 @@ import useSWR from 'swr'
import ImageHelpSheet from '~/components/admin/list/ImageHelpSheet'
import { Select as AntdSelect } from 'antd'
import ListImage from '~/components/admin/list/ListImage'
import ImageBatchDeleteSheet from '~/components/admin/list/ImageBatchDeleteSheet'

export default function ListProps(props : Readonly<ImageServerHandleProps>) {
const [pageNum, setPageNum] = useState(1)
Expand All @@ -57,11 +58,15 @@ export default function ListProps(props : Readonly<ImageServerHandleProps>) {
const [updateShowLoading, setUpdateShowLoading] = useState(false)
const [updateImageTagLoading, setUpdateImageTagLoading] = useState(false)
const [updateShowId, setUpdateShowId] = useState(0)
const { setImageEdit, setImageEditData, setImageView, setImageViewData, setImageHelp } = useButtonStore(
const { setImageEdit, setImageEditData, setImageView, setImageViewData, setImageHelp, setImageBatchDelete } = useButtonStore(
(state) => state,
)
const { data: tags, isLoading: tagsLoading } = useSWR('/api/v1/get-tags', fetcher)

const dataProps: DataProps = {
data: data,
}

async function deleteImage() {
setDeleteLoading(true)
if (!image.id) return
Expand Down Expand Up @@ -188,6 +193,15 @@ export default function ListProps(props : Readonly<ImageServerHandleProps>) {
>
<CircleHelp />
</Button>
<Button
isIconOnly
size="sm"
color="danger"
aria-label="批量删除"
onClick={() => setImageBatchDelete(true)}
>
<Trash />
</Button>
<Button
color="primary"
radius="full"
Expand All @@ -209,12 +223,26 @@ export default function ListProps(props : Readonly<ImageServerHandleProps>) {
{Array.isArray(data) && data?.map((image: ImageType) => (
<Card key={image.id} shadow="sm" className="h-72 show-up-motion">
<CardHeader className="justify-between space-x-1 select-none">
{
image.tag_values.includes(',') ?
<Badge content={image.tag_values.split(",").length} color="primary">
<div className="space-x-2">
{
image.tag_values.includes(',') ?
<Badge content={image.tag_values.split(",").length} color="primary">
<Popover placement="top" shadow="sm">
<PopoverTrigger className="cursor-pointer">
<Chip variant="shadow" className="flex-1" aria-label="相册">{image.tag_names.length > 8 ? image.tag_names.substring(0, 8) + '...' : image.tag_names}</Chip>
</PopoverTrigger>
<PopoverContent>
<div className="px-1 py-2 select-none">
<div className="text-small font-bold">相册</div>
<div className="text-tiny">图片在对应的相册上显示</div>
</div>
</PopoverContent>
</Popover>
</Badge>
:
<Popover placement="top" shadow="sm">
<PopoverTrigger className="cursor-pointer">
<Chip variant="shadow" className="flex-1" aria-label="相册">{image.tag_names.length > 8 ? image.tag_names.substring(0, 8) + '...' : image.tag_names}</Chip>
<Chip variant="shadow" className="flex-1" aria-label="相册">{image.tag_names}</Chip>
</PopoverTrigger>
<PopoverContent>
<div className="px-1 py-2 select-none">
Expand All @@ -223,20 +251,19 @@ export default function ListProps(props : Readonly<ImageServerHandleProps>) {
</div>
</PopoverContent>
</Popover>
</Badge>
:
<Popover placement="top" shadow="sm">
<PopoverTrigger className="cursor-pointer">
<Chip variant="shadow" className="flex-1" aria-label="相册">{image.tag_names}</Chip>
</PopoverTrigger>
<PopoverContent>
<div className="px-1 py-2 select-none">
<div className="text-small font-bold">相册</div>
<div className="text-tiny">图片在对应的相册上显示</div>
</div>
</PopoverContent>
</Popover>
}
}
<Popover placement="top" shadow="sm">
<PopoverTrigger className="cursor-pointer">
<Chip variant="shadow" className="flex-1" aria-label="id">{image.id}</Chip>
</PopoverTrigger>
<PopoverContent>
<div className="px-1 py-2 select-none">
<div className="text-small font-bold">id</div>
<div className="text-tiny">图片的id</div>
</div>
</PopoverContent>
</Popover>
</div>
<div className="flex items-center">
<Tooltip content="查看图片">
<Button
Expand Down Expand Up @@ -424,6 +451,7 @@ export default function ListProps(props : Readonly<ImageServerHandleProps>) {
<ImageEditSheet {...{...props, pageNum, tag}} />
<ImageView />
<ImageHelpSheet />
<ImageBatchDeleteSheet {...{...props, dataProps, pageNum, tag}} />
</div>
)
}
56 changes: 56 additions & 0 deletions components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'

import { cn } from '~/utils'

const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)

export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"

export { Button, buttonVariants }
30 changes: 30 additions & 0 deletions components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client'

import * as React from 'react'
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
import { Check } from 'lucide-react'

import { cn } from '~/utils'

const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName

export { Checkbox }
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.1.0",
"antd": "^5.20.5",
"canvas-confetti": "^1.9.3",
"class-variance-authority": "^0.7.0",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c27d487

Please sign in to comment.