"use client" import * as React from "react" import { useState, useEffect } from "react" import Image from "next/image" import { toast } from "sonner" import { Gift, Copy, Check, ExternalLink, Pencil, X, ImagePlus, History } from "lucide-react" import type { UploadImageResponse } from "@/lib/services" import { getFileUrl } from "@/lib/services/upload/upload.service" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { Button } from "@/components/ui/button" import { Textarea } from "@/components/ui/textarea" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { PasswordDialog } from "@/components/common/general/password-dialog" import { ListData } from "@/components/common/general/list-data" import { ImageCropper } from "@/components/common/redenvelope/image-cropper" import { motion } from "motion/react" import { RedEnvelopeCard } from "@/components/common/redenvelope/red-envelope-card" import { useUser } from "@/contexts/user-context" import services from "@/lib/services" import type { RedEnvelopeType, CreateRedEnvelopeRequest, PublicConfigResponse, RedEnvelope, RedEnvelopeListResponse } from "@/lib/services" interface RedEnvelopeCover { coverImageUrl: string | null heterotypicImageUrl: string | null coverUploadId: string | null heterotypicUploadId: string | null } /** * 红包组件 * * 提供红包发送功能,包括 Banner 和创建对话框 */ export function RedEnvelope({ onSuccess }: { onSuccess?: () => void }) { const { user } = useUser() const [isFormOpen, setIsFormOpen] = useState(false) const [isPasswordOpen, setIsPasswordOpen] = useState(false) const [isResultOpen, setIsResultOpen] = useState(false) const [copiedId, setCopiedId] = useState(null) const [isCropperOpen, setIsCropperOpen] = useState(false) const [cropperType, setCropperType] = useState<'cover' | 'heterotypic'>('cover') const [showCustomization, setShowCustomization] = useState(false) const [historyCoverCovers, setHistoryCoverCovers] = useState([]) const [historyHeterotypicCovers, setHistoryHeterotypicCovers] = useState([]) /* 表单状态 */ const [type, setType] = useState("random") const [totalAmount, setTotalAmount] = useState("") const [totalCount, setTotalCount] = useState("") const [greeting, setGreeting] = useState("") const [loading, setLoading] = useState(false) /* 封面状态 */ const [cover, setCover] = useState({ coverImageUrl: null, heterotypicImageUrl: null, coverUploadId: null, heterotypicUploadId: null }) /* 结果状态 */ const [resultLink, setResultLink] = useState("") /* 配置状态 */ const [config, setConfig] = useState(null) /* 加载配置 */ const loadConfig = async () => { try { const result = await services.config.getPublicConfig() setConfig(result) } catch { toast.error('加载配置失败') } } /* 初始加载 */ useEffect(() => { loadConfig() }, []) /* 验证积分格式 */ const validateAmount = (value: string): boolean => { const regex = /^\d+(\.\d{1,2})?$/ return regex.test(value) && parseFloat(value) > 0 } /* 处理表单提交 */ const handleFormSubmit = () => { if (!totalAmount.trim()) { toast.error("请设置积分总数") return } if (!validateAmount(totalAmount)) { toast.error("积分格式不正确,必须大于1且最多2位小数") return } if (!totalCount.trim()) { toast.error("请设置可领取人数") return } const count = parseInt(totalCount) if (isNaN(count) || count <= 0) { toast.error("可领取人数必须大于0") return } const amount = parseFloat(totalAmount) if (amount < 1) { toast.error("红包总金额不能低于1 LDC") return } if (config && parseFloat(config.red_envelope_max_amount) > 0) { if (amount > parseFloat(config.red_envelope_max_amount)) { toast.error(`红包的积分总数不能超过 ${ config.red_envelope_max_amount } LDC`) return } } if (count > 10000) { toast.error("可领取人数不能超过 10000 个") return } if (amount / count < 0.01) { toast.error("单个红包不可低于 0.01 LDC") return } if (greeting.length > 100) { toast.error("祝福语最多100字符") return } setIsPasswordOpen(true) } /* 提交创建 */ const handleConfirmCreate = async (password: string) => { setLoading(true) try { const data: CreateRedEnvelopeRequest = { type, total_amount: parseFloat(totalAmount), total_count: parseInt(totalCount), greeting: greeting || "恭喜发财,大吉大利", pay_key: password, cover_upload_id: cover.coverUploadId || undefined, heterotypic_upload_id: cover.heterotypicUploadId || undefined, } const result = await services.redEnvelope.create(data) const link = getEnvelopeLink(result.id) setResultLink(link) setIsPasswordOpen(false) setIsFormOpen(false) setIsResultOpen(true) onSuccess?.() /* 重置表单 */ setType("random") setTotalAmount("") setTotalCount("") setGreeting("") setCover({ coverImageUrl: null, heterotypicImageUrl: null, coverUploadId: null, heterotypicUploadId: null }) setShowCustomization(false) } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : '创建失败' toast.error('创建红包失败', { description: errorMessage }) } finally { setLoading(false) } } /* 复制链接 */ const handleCopyLink = async (link: string, envelopeId: string) => { try { await navigator.clipboard.writeText(link) setCopiedId(envelopeId) toast.success("链接已复制") setTimeout(() => setCopiedId(null), 2000) } catch { toast.error("复制失败") } } /* 计算手续费 */ const feeInfo = React.useMemo(() => { if (!totalAmount || !config) return null const amount = parseFloat(totalAmount) if (isNaN(amount)) return null const feeRate = parseFloat(config.red_envelope_fee_rate) const fee = (amount * feeRate).toFixed(2) const total = (amount + parseFloat(fee)).toFixed(2) return { fee, total, rate: feeRate } }, [totalAmount, config]) /* 生成红包链接 */ const getEnvelopeLink = (id: string) => { if (typeof window !== 'undefined') { return `${ window.location.origin }/redenvelope/${ id }` } return `/redenvelope/${ id }` } /* 处理封面裁剪完成 */ const handleCropComplete = async (croppedImage: string, coverType: 'cover' | 'heterotypic') => { const uploadingToast = toast.loading(`正在上传${ coverType === 'cover' ? '背景封面' : '装饰图片' }...`) try { const result = await services.upload.uploadBase64Image( croppedImage, coverType, `${ coverType }-${ Date.now() }.png` ) setCover(prev => ({ ...prev, [coverType === 'cover' ? 'coverImageUrl' : 'heterotypicImageUrl']: getFileUrl(result.id), [coverType === 'cover' ? 'coverUploadId' : 'heterotypicUploadId']: result.id })) toast.dismiss(uploadingToast) toast.success(`${ coverType === 'cover' ? '背景封面' : '装饰图片' }已设置`) } catch (error) { toast.dismiss(uploadingToast) const errorMessage = error instanceof Error ? error.message : '上传失败' toast.error(`上传${ coverType === 'cover' ? '背景封面' : '装饰图片' }失败`, { description: errorMessage }) } } /* 加载历史封面 */ const loadHistoryCovers = async () => { try { const [covers, heterotypics] = await Promise.all([ services.redEnvelope.listCovers('cover'), services.redEnvelope.listCovers('heterotypic'), ]) setHistoryCoverCovers(covers || []) setHistoryHeterotypicCovers(heterotypics || []) } catch { // 静默失败,不影响主功能 } } /* 选择历史封面 */ const handleSelectHistoryCover = (item: UploadImageResponse, type: 'cover' | 'heterotypic') => { // 检查是否已经选中当前项 const currentId = type === 'cover' ? cover.coverUploadId : cover.heterotypicUploadId if (currentId === item.id) { // 取消选择 setCover(prev => ({ ...prev, [type === 'cover' ? 'coverImageUrl' : 'heterotypicImageUrl']: null, [type === 'cover' ? 'coverUploadId' : 'heterotypicUploadId']: null })) toast.success(`已取消选择${ type === 'cover' ? '背景封面' : '装饰图片' }`) return } setCover(prev => ({ ...prev, [type === 'cover' ? 'coverImageUrl' : 'heterotypicImageUrl']: getFileUrl(item.id), [type === 'cover' ? 'coverUploadId' : 'heterotypicUploadId']: item.id })) toast.success(`已选择历史${ type === 'cover' ? '背景封面' : '装饰图片' }`) } /* 打开裁剪对话框 */ const handleOpenCropper = (type: 'cover' | 'heterotypic') => { setCropperType(type) setIsCropperOpen(true) } /* 移除封面 */ const handleRemoveCover = (type: 'cover' | 'heterotypic') => { setCover(prev => ({ ...prev, [type === 'cover' ? 'coverImageUrl' : 'heterotypicImageUrl']: null, [type === 'cover' ? 'coverUploadId' : 'heterotypicUploadId']: null })) toast.success(`${ type === 'cover' ? '背景封面' : '装饰图片' }已移除`) } /* 处理图片加载错误 */ const handleImageError = (id: string, type: 'cover' | 'heterotypic') => { if (type === 'cover') { setHistoryCoverCovers(prev => prev.filter(item => item.id !== id)) } else { setHistoryHeterotypicCovers(prev => prev.filter(item => item.id !== id)) } } return (

发红包

创建积分红包,支持固定金额和拼手气两种模式。

发红包
创建积分红包,支持固定金额和拼手气两种模式{showCustomization && ",可添加个性化封面"}。
0 ? config.red_envelope_max_amount : undefined} placeholder={config && parseFloat(config.red_envelope_max_amount) > 0 ? `最大 ${ config.red_envelope_max_amount }` : "0.00"} value={totalAmount} onChange={(e) => setTotalAmount(e.target.value)} className="font-mono shadow-none" disabled={loading} />
setTotalCount(e.target.value)} className="font-mono shadow-none" disabled={loading} />