0.0.8.5
This commit is contained in:
@@ -1,41 +1,22 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||
import { db } from "@/lib/db"
|
||||
import { CircleUser, CircleUserRound } from "lucide-react"
|
||||
import type { Gender } from "@/types/family"
|
||||
|
||||
interface AvatarDisplayProps {
|
||||
imageId?: string
|
||||
fallbackUrl?: string
|
||||
imageUrl?: string // 改为 URL
|
||||
fallbackText?: string
|
||||
gender?: Gender
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function AvatarDisplay({ imageId, fallbackUrl, fallbackText, gender, 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])
|
||||
|
||||
// 如果有头像,显示头像
|
||||
if (blobUrl) {
|
||||
export function AvatarDisplay({ imageUrl, fallbackText, gender, className }: AvatarDisplayProps) {
|
||||
// 如果有头像 URL,显示头像
|
||||
if (imageUrl) {
|
||||
return (
|
||||
<Avatar className={className}>
|
||||
<AvatarImage src={blobUrl} />
|
||||
<AvatarImage src={imageUrl} />
|
||||
<AvatarFallback className="text-lg font-serif bg-muted">{fallbackText}</AvatarFallback>
|
||||
</Avatar>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-serif font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
||||
@@ -32,7 +32,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn('leading-none font-semibold', className)}
|
||||
className={cn('font-serif leading-none font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -42,7 +42,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
className={cn('font-serif text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { solar2lunar, parseDate } from "@/lib/lunar-calendar"
|
||||
import { Calendar } from "lucide-react"
|
||||
|
||||
interface DateInputWithLunarProps {
|
||||
label: string
|
||||
value?: string
|
||||
isLunar?: boolean
|
||||
onChange: (value: string) => void
|
||||
onLunarChange?: (isLunar: boolean) => void
|
||||
id?: string
|
||||
showLunarToggle?: boolean // 是否显示公历/农历切换
|
||||
}
|
||||
|
||||
export function DateInputWithLunar({
|
||||
label,
|
||||
value,
|
||||
isLunar = false,
|
||||
onChange,
|
||||
onLunarChange,
|
||||
id,
|
||||
showLunarToggle = true,
|
||||
}: DateInputWithLunarProps) {
|
||||
const [lunarInfo, setLunarInfo] = useState<string>("")
|
||||
|
||||
// 当日期改变时,更新农历信息
|
||||
useEffect(() => {
|
||||
if (!value) {
|
||||
setLunarInfo("")
|
||||
return
|
||||
}
|
||||
|
||||
const date = parseDate(value)
|
||||
if (!date) {
|
||||
setLunarInfo("")
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const lunar = solar2lunar(date)
|
||||
if (lunar) {
|
||||
setLunarInfo(lunar.toString())
|
||||
} else {
|
||||
const year = date.getFullYear()
|
||||
setLunarInfo(`⚠️ 农历转换仅支持1900-2100年 (当前: ${year}年)`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("日期转换错误:", error)
|
||||
setLunarInfo("")
|
||||
}
|
||||
}, [value])
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<Label htmlFor={id} className="text-base font-medium">
|
||||
{label}
|
||||
</Label>
|
||||
|
||||
{/* 公历日期输入 */}
|
||||
<div className="relative">
|
||||
<Input
|
||||
id={id}
|
||||
type="date"
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className="pr-10"
|
||||
/>
|
||||
<Calendar className="absolute right-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none" />
|
||||
</div>
|
||||
|
||||
{/* 农历对照显示 */}
|
||||
{lunarInfo && (
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<Calendar className="h-4 w-4 text-amber-600 mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="text-sm font-medium text-amber-900">
|
||||
农历对照
|
||||
</div>
|
||||
<div className="text-sm text-amber-800">
|
||||
{lunarInfo}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 纪念日类型选择 */}
|
||||
{showLunarToggle && value && lunarInfo && !lunarInfo.includes('⚠️') && onLunarChange && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">纪念日计算方式</Label>
|
||||
<RadioGroup
|
||||
value={isLunar ? "lunar" : "solar"}
|
||||
onValueChange={(val) => onLunarChange(val === "lunar")}
|
||||
className="flex flex-row gap-6"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="solar" id={`${id}-solar`} />
|
||||
<Label htmlFor={`${id}-solar`} className="text-sm font-normal cursor-pointer">
|
||||
按公历 (每年固定日期)
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="lunar" id={`${id}-lunar`} />
|
||||
<Label htmlFor={`${id}-lunar`} className="text-sm font-normal cursor-pointer">
|
||||
按农历 (对应农历日期)
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{isLunar
|
||||
? "💡 将按农历日期计算每年的纪念日,如生日、祭日等"
|
||||
: "💡 将按公历日期计算每年的纪念日"}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -12,8 +12,8 @@ import "react-image-crop/dist/ReactCrop.css"
|
||||
import imageCompression from "browser-image-compression"
|
||||
|
||||
interface ImageUploadProps {
|
||||
value?: string // The image ID
|
||||
onChange: (imageId: string | undefined) => void
|
||||
value?: string // The image URL
|
||||
onChange: (imageUrl: string | undefined) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
@@ -27,31 +27,12 @@ export function ImageUpload({ value, onChange, className }: ImageUploadProps) {
|
||||
const imgRef = useRef<HTMLImageElement>(null)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Load preview if value (imageId) exists
|
||||
// Load preview if value (URL) 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)
|
||||
if (value) {
|
||||
setPreviewUrl(value)
|
||||
} else {
|
||||
setPreviewUrl(undefined)
|
||||
}
|
||||
}, [value])
|
||||
|
||||
@@ -147,20 +128,27 @@ export function ImageUpload({ value, onChange, className }: ImageUploadProps) {
|
||||
imageBlob = await response.blob()
|
||||
}
|
||||
|
||||
const imageId = uuidv4()
|
||||
await db.images.add({
|
||||
id: imageId,
|
||||
blob: imageBlob,
|
||||
mimeType: imageBlob.type,
|
||||
createdAt: new Date().toISOString(),
|
||||
// 上传到服务器
|
||||
const formData = new FormData()
|
||||
formData.append('file', imageBlob, 'avatar.jpg')
|
||||
|
||||
const response = await fetch('/api/upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
onChange(imageId)
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.error || '上传失败')
|
||||
}
|
||||
|
||||
const { url } = await response.json()
|
||||
onChange(url)
|
||||
setShowCropDialog(false)
|
||||
setImageToCrop(null)
|
||||
} catch (error) {
|
||||
console.error("Failed to save image:", error)
|
||||
alert("图片保存失败")
|
||||
alert("图片保存失败: " + (error instanceof Error ? error.message : '未知错误'))
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'font-serif file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
className,
|
||||
|
||||
@@ -13,7 +13,7 @@ function Label({
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
'font-serif flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user