'use client'; import { useEffect, useMemo, useRef, useState } from 'react'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; import { CloseIcon, SettingsIcon, SwitchIcon } from './Icons'; import { DatePicker } from './Common'; import { Dialog, DialogContent, DialogTitle, } from '@/components/ui/dialog'; dayjs.extend(utc); dayjs.extend(timezone); const TZ = typeof Intl !== 'undefined' && Intl.DateTimeFormat ? (Intl.DateTimeFormat().resolvedOptions().timeZone || 'Asia/Shanghai') : 'Asia/Shanghai'; export default function HoldingEditModal({ fund, holding, onClose, onSave, onOpenTrade }) { const [mode, setMode] = useState('amount'); // 'amount' | 'share' const [dateMode, setDateMode] = useState('date'); // 'date' | 'days' const dwjz = fund?.dwjz || fund?.gsz || 0; const dwjzRef = useRef(dwjz); useEffect(() => { dwjzRef.current = dwjz; }, [dwjz]); const [share, setShare] = useState(''); const [cost, setCost] = useState(''); const [amount, setAmount] = useState(''); const [profit, setProfit] = useState(''); const [firstPurchaseDate, setFirstPurchaseDate] = useState(''); const [holdingDaysInput, setHoldingDaysInput] = useState(''); const holdingSig = useMemo(() => { if (!holding) return ''; return `${holding.id ?? ''}|${holding.share ?? ''}|${holding.cost ?? ''}|${holding.firstPurchaseDate ?? ''}`; }, [holding]); useEffect(() => { if (holding) { const s = holding.share || 0; const c = holding.cost || 0; setShare(String(s)); setCost(String(c)); setFirstPurchaseDate(holding.firstPurchaseDate || ''); if (holding.firstPurchaseDate) { const days = dayjs.tz(undefined, TZ).diff(dayjs.tz(holding.firstPurchaseDate, TZ), 'day'); setHoldingDaysInput(days > 0 ? String(days) : ''); } else { setHoldingDaysInput(''); } const price = dwjzRef.current; if (price > 0) { const a = s * price; const p = (price - c) * s; setAmount(a.toFixed(2)); setProfit(p.toFixed(2)); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [holdingSig]); const handleModeChange = (newMode) => { if (newMode === mode) return; setMode(newMode); if (newMode === 'share') { if (amount && dwjz > 0) { const a = parseFloat(amount); const p = parseFloat(profit || 0); const s = a / dwjz; const principal = a - p; const c = s > 0 ? principal / s : 0; setShare(s.toFixed(2)); setCost(c.toFixed(4)); } } else { if (share && dwjz > 0) { const s = parseFloat(share); const c = parseFloat(cost || 0); const a = s * dwjz; const p = (dwjz - c) * s; setAmount(a.toFixed(2)); setProfit(p.toFixed(2)); } } }; const handleDateModeToggle = () => { const newMode = dateMode === 'date' ? 'days' : 'date'; setDateMode(newMode); if (newMode === 'days' && firstPurchaseDate) { const days = dayjs.tz(undefined, TZ).diff(dayjs.tz(firstPurchaseDate, TZ), 'day'); setHoldingDaysInput(days > 0 ? String(days) : ''); } else if (newMode === 'date' && holdingDaysInput) { const days = parseInt(holdingDaysInput, 10); if (Number.isFinite(days) && days >= 0) { const date = dayjs.tz(undefined, TZ).subtract(days, 'day').format('YYYY-MM-DD'); setFirstPurchaseDate(date); } } }; const handleHoldingDaysChange = (value) => { setHoldingDaysInput(value); const days = parseInt(value, 10); if (Number.isFinite(days) && days >= 0) { const date = dayjs.tz(undefined, TZ).subtract(days, 'day').format('YYYY-MM-DD'); setFirstPurchaseDate(date); } }; const handleFirstPurchaseDateChange = (value) => { setFirstPurchaseDate(value); if (value) { const days = dayjs.tz(undefined, TZ).diff(dayjs.tz(value, TZ), 'day'); setHoldingDaysInput(days > 0 ? String(days) : ''); } else { setHoldingDaysInput(''); } }; const handleSubmit = (e) => { e.preventDefault(); let finalShare = 0; let finalCost = 0; if (mode === 'share') { if (!share || !cost) return; finalShare = Number(Number(share).toFixed(2)); finalCost = Number(cost); } else { if (!amount || !dwjz) return; const a = Number(amount); const p = Number(profit || 0); const rawShare = a / dwjz; finalShare = Number(rawShare.toFixed(2)); const principal = a - p; finalCost = finalShare > 0 ? principal / finalShare : 0; } const trimmedDate = firstPurchaseDate ? firstPurchaseDate.trim() : ''; onSave({ share: finalShare, cost: finalCost, ...(trimmedDate && { firstPurchaseDate: trimmedDate }) }); onClose(); }; const isValid = mode === 'share' ? (share && cost && !isNaN(share) && !isNaN(cost)) : (amount && !isNaN(amount) && (!profit || !isNaN(profit)) && dwjz > 0); const handleOpenChange = (open) => { if (!open) { onClose?.(); } }; return ( 编辑持仓
设置持仓 {typeof onOpenTrade === 'function' && ( )}
{fund?.name}
#{fund?.code}
最新净值:{dwjz}
{mode === 'amount' ? ( <>
setAmount(e.target.value)} placeholder="请输入持有总金额" style={{ width: '100%', border: !amount ? '1px solid var(--danger)' : undefined }} />
setProfit(e.target.value)} placeholder="请输入持有总收益 (可为负)" style={{ width: '100%' }} />
) : ( <>
setShare(e.target.value)} placeholder="请输入持有份额" style={{ width: '100%', border: !share ? '1px solid var(--danger)' : undefined }} />
setCost(e.target.value)} placeholder="请输入持仓成本价" style={{ width: '100%', border: !cost ? '1px solid var(--danger)' : undefined }} />
)}
{dateMode === 'date' ? '首次买入日期' : '持有天数'}
{dateMode === 'date' ? ( ) : ( handleHoldingDaysChange(e.target.value)} placeholder="请输入持有天数" style={{ width: '100%' }} /> )}
); }