0.0.0.2
This commit is contained in:
@@ -11,6 +11,7 @@ import { Textarea } from "@/components/ui/textarea"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { CalendarIcon, User, Scroll, Users } from "lucide-react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { ImageUpload } from "@/components/ui/image-upload"
|
||||
|
||||
interface MemberFormProps {
|
||||
initialData?: Partial<FamilyMember>
|
||||
@@ -75,6 +76,13 @@ export function MemberForm({ initialData, existingMembers = [], onSubmit, onCanc
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex justify-center mb-4">
|
||||
<ImageUpload
|
||||
value={formData.avatarImageId}
|
||||
onChange={(imageId) => handleChange("avatarImageId", imageId)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="surname">姓 (Surname)</Label>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-16 items-center px-4">
|
||||
<div className="container mx-auto flex h-16 items-center px-4">
|
||||
<div className="mr-8 flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded bg-primary text-primary-foreground">
|
||||
<BookOpen className="h-5 w-5" />
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { db } from "@/lib/db"
|
||||
|
||||
interface AvatarDisplayProps {
|
||||
imageId?: string
|
||||
fallbackUrl?: string
|
||||
fallbackText?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function AvatarDisplay({ imageId, fallbackUrl, fallbackText, className }: AvatarDisplayProps) {
|
||||
const [blobUrl, setBlobUrl] = useState<string | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
if (imageId) {
|
||||
db.images.get(imageId).then((image) => {
|
||||
if (image) {
|
||||
const url = URL.createObjectURL(image.blob)
|
||||
setBlobUrl(url)
|
||||
return () => URL.revokeObjectURL(url)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setBlobUrl(undefined)
|
||||
}
|
||||
}, [imageId])
|
||||
|
||||
return (
|
||||
<Avatar className={className}>
|
||||
<AvatarImage src={blobUrl || fallbackUrl || "/placeholder.svg"} />
|
||||
<AvatarFallback className="text-lg font-serif bg-muted">{fallbackText}</AvatarFallback>
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useRef, useEffect } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Upload, X, Loader2 } from "lucide-react"
|
||||
import { db } from "@/lib/db"
|
||||
import { v4 as uuidv4 } from "uuid"
|
||||
|
||||
interface ImageUploadProps {
|
||||
value?: string // The image ID
|
||||
onChange: (imageId: string | undefined) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function ImageUpload({ value, onChange, className }: ImageUploadProps) {
|
||||
const [previewUrl, setPreviewUrl] = useState<string | undefined>(undefined)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Load preview if value (imageId) exists
|
||||
useEffect(() => {
|
||||
let objectUrl: string | undefined
|
||||
|
||||
const loadPreview = async () => {
|
||||
if (!value) {
|
||||
setPreviewUrl(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const image = await db.images.get(value)
|
||||
if (image) {
|
||||
objectUrl = URL.createObjectURL(image.blob)
|
||||
setPreviewUrl(objectUrl)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load image:", error)
|
||||
}
|
||||
}
|
||||
|
||||
loadPreview()
|
||||
|
||||
return () => {
|
||||
if (objectUrl) URL.revokeObjectURL(objectUrl)
|
||||
}
|
||||
}, [value])
|
||||
|
||||
const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// Basic validation
|
||||
if (!file.type.startsWith("image/")) {
|
||||
alert("请选择图片文件")
|
||||
return
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
alert("图片大小不能超过 5MB")
|
||||
return
|
||||
}
|
||||
|
||||
// Save to DB
|
||||
const imageId = uuidv4()
|
||||
await db.images.add({
|
||||
id: imageId,
|
||||
blob: file,
|
||||
mimeType: file.type,
|
||||
createdAt: new Date().toISOString(),
|
||||
})
|
||||
|
||||
onChange(imageId)
|
||||
} catch (error) {
|
||||
console.error("Failed to save image:", error)
|
||||
alert("图片上传失败")
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
// Reset input
|
||||
if (fileInputRef.current) fileInputRef.current.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemove = () => {
|
||||
onChange(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-4 ${className}`}>
|
||||
<Avatar className="h-20 w-20 border-2 border-dashed border-muted-foreground/50">
|
||||
<AvatarImage src={previewUrl} className="object-cover" />
|
||||
<AvatarFallback className="bg-transparent">
|
||||
{isLoading ? <Loader2 className="h-6 w-6 animate-spin" /> : <Upload className="h-6 w-6 text-muted-foreground" />}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{value ? "更换头像" : "上传头像"}
|
||||
</Button>
|
||||
{value && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-destructive hover:text-destructive"
|
||||
onClick={handleRemove}
|
||||
>
|
||||
<X className="mr-2 h-3 w-3" /> 移除
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user