Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions src/components/ImageGallery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useState } from "react"
import { Card, CardBody, Modal, ModalContent, ModalBody, Button } from "@heroui/react"

interface ImageGalleryProps {
images: string[]
columns?: 2 | 3
}

export default function ImageGallery({ images, columns = 3 }: ImageGalleryProps) {
const [selectedImage, setSelectedImage] = useState<string | null>(null)

if (!images || images.length === 0) {
return null
}

const gridCols = columns === 2 ? "grid-cols-2" : "grid-cols-2 md:grid-cols-3"

return (
<>
<div className={`grid ${gridCols} gap-2`}>
{images.map((image, index) => (
<Card
key={index}
className="cursor-pointer hover:opacity-80 transition"
isPressable
onPress={() => setSelectedImage(image)}
>
<CardBody className="p-0 overflow-hidden">
<img
src={image}
alt={`Image ${index + 1}`}
className="h-32 aspect-square object-cover rounded-lg"
/>
</CardBody>
</Card>
))}
</div>

{/* Image Modal */}
<Modal
isOpen={!!selectedImage}
onClose={() => setSelectedImage(null)}
size="4xl"
scrollBehavior="inside"
>
<ModalContent>
<ModalBody className="p-0">
{selectedImage && (
<div className="relative">
<img
src={selectedImage}
alt="Full size"
className="w-full h-auto max-h-[80vh] object-contain"
/>
<Button
isIconOnly
color="default"
variant="flat"
className="absolute top-2 right-2"
onPress={() => setSelectedImage(null)}
>
</Button>
</div>
)}
</ModalBody>
</ModalContent>
</Modal>
</>
)
}
135 changes: 131 additions & 4 deletions src/pages/repair/EditRepairModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect } from "react"
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, Input, Textarea } from "@heroui/react"
import { useState, useEffect, useRef } from "react"
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, Input, Textarea, Card, CardBody } from "@heroui/react"
import { saturdayClient } from "../../utils/client"
import { makeLogtoClient } from "../../utils/auth"
import type { components } from "../../types/saturday"
Expand All @@ -21,15 +21,19 @@ export default function EditRepairModal({ isOpen, onClose, event, onSaved }: Edi
model: "",
phone: "",
qq: "",
images: [],
})
const [uploadError, setUploadError] = useState<string>("")
const fileInputRef = useRef<HTMLInputElement>(null)

useEffect(() => {
if (event) {
setFormData({
problem: event.problem || "",
model: event.model || "",
phone: event.phone || "",
qq: event.qq || "",
phone: "",
qq: "",
images: event.images || [],
})
}
}, [event])
Expand Down Expand Up @@ -74,6 +78,72 @@ export default function EditRepairModal({ isOpen, onClose, event, onSaved }: Edi
}
}

const handleImageSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files || files.length === 0) return

setUploadError("")
const newImages: string[] = []
const logtoToken = await makeLogtoClient().getAccessToken()

for (let i = 0; i < files.length; i++) {
const file = files[i]

// Validate file type
if (!file.type.startsWith("image/")) {
continue
}

// Validate file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
setUploadError(`图片 ${file.name} 超过10MB大小限制`)
continue
}

// Upload to server
try {
const formDataUpload = new FormData()
formDataUpload.append("file", file)

const { data, error } = await saturdayClient.POST("/upload", {
params: {
header: {
Authorization: `Bearer ${logtoToken}`,
},
},
body: formDataUpload as unknown as { file: string },
bodySerializer: () => formDataUpload as unknown as string,
})

if (error || !data) {
throw new Error("Upload failed")
}

newImages.push(data.url)
}
catch (error) {
console.error("Failed to upload image:", error)
setUploadError(`图片 ${file.name} 上传失败`)
}
}

setFormData({
...formData,
images: [...(formData.images || []), ...newImages],
})

// Reset file input
if (fileInputRef.current) {
fileInputRef.current.value = ""
}
}

const handleRemoveImage = (index: number) => {
const newImages = [...(formData.images || [])]
newImages.splice(index, 1)
setFormData({ ...formData, images: newImages })
}

return (
<Modal isOpen={isOpen} onClose={handleClose} size="2xl" scrollBehavior="inside">
<ModalContent>
Expand Down Expand Up @@ -114,6 +184,63 @@ export default function EditRepairModal({ isOpen, onClose, event, onSaved }: Edi
value={formData.qq || ""}
onChange={e => setFormData({ ...formData, qq: e.target.value })}
/>

{/* Image Upload Section */}
<div className="flex flex-col gap-2">
<div className="text-sm font-bold">
问题图片
<span className="text-xs font-normal text-default-400 ml-2">
(可选,最多5张,每张最大10MB)
</span>
</div>
{uploadError && (
<div className="text-xs text-danger">{uploadError}</div>
)}
<div className="flex flex-col gap-2">
<input
ref={fileInputRef}
type="file"
accept="image/*"
multiple
onChange={handleImageSelect}
style={{ display: "none" }}
/>
<Button
onPress={() => fileInputRef.current?.click()}
variant="bordered"
size="sm"
isDisabled={(formData.images?.length || 0) >= 5}
>
{(formData.images?.length || 0) >= 5 ? "已达到最大数量" : "选择图片"}
</Button>

{formData.images && formData.images.length > 0 && (
<div className="grid grid-cols-2 gap-2">
{formData.images.map((image, index) => (
<Card key={index} className="relative">
<CardBody className="p-0">
<img
src={image}
alt={`Preview ${index + 1}`}
className="w-full aspect-square object-cover rounded-lg"
/>
<Button
isIconOnly
color="danger"
variant="flat"
size="sm"
className="absolute top-1 right-1"
onPress={() => handleRemoveImage(index)}
>
</Button>
</CardBody>
</Card>
))}
</div>
)}
</div>
</div>
</div>
</ModalBody>
<ModalFooter>
Expand Down
Loading