'use client'; import { useEffect, useMemo, useState } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; import { fetchSmartFundNetValue } from '../api/fund'; import { DatePicker, NumericInput } from './Common'; import ConfirmModal from './ConfirmModal'; import { CloseIcon } from './Icons'; dayjs.extend(utc); dayjs.extend(timezone); const DEFAULT_TZ = 'Asia/Shanghai'; const getBrowserTimeZone = () => { if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) { const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; return tz || DEFAULT_TZ; } return DEFAULT_TZ; }; const TZ = getBrowserTimeZone(); dayjs.tz.setDefault(TZ); const nowInTz = () => dayjs().tz(TZ); const toTz = (input) => (input ? dayjs.tz(input, TZ) : nowInTz()); const formatDate = (input) => toTz(input).format('YYYY-MM-DD'); export default function TradeModal({ type, fund, holding, onClose, onConfirm, pendingTrades = [], onDeletePending }) { const isBuy = type === 'buy'; const [share, setShare] = useState(''); const [amount, setAmount] = useState(''); const [feeRate, setFeeRate] = useState('0'); const [date, setDate] = useState(() => { return formatDate(); }); const [isAfter3pm, setIsAfter3pm] = useState(nowInTz().hour() >= 15); const [calcShare, setCalcShare] = useState(null); const currentPendingTrades = useMemo(() => { return pendingTrades.filter(t => t.fundCode === fund?.code); }, [pendingTrades, fund]); const pendingSellShare = useMemo(() => { return currentPendingTrades .filter(t => t.type === 'sell') .reduce((acc, curr) => acc + (Number(curr.share) || 0), 0); }, [currentPendingTrades]); const availableShare = holding ? Math.max(0, holding.share - pendingSellShare) : 0; const [showPendingList, setShowPendingList] = useState(false); useEffect(() => { if (showPendingList && currentPendingTrades.length === 0) { setShowPendingList(false); } }, [showPendingList, currentPendingTrades]); const getEstimatePrice = () => fund?.estPricedCoverage > 0.05 ? fund?.estGsz : (typeof fund?.gsz === 'number' ? fund?.gsz : Number(fund?.dwjz)); const [price, setPrice] = useState(getEstimatePrice()); const [loadingPrice, setLoadingPrice] = useState(false); const [actualDate, setActualDate] = useState(null); useEffect(() => { if (date && fund?.code) { setLoadingPrice(true); setActualDate(null); let queryDate = date; if (isAfter3pm) { queryDate = toTz(date).add(1, 'day').format('YYYY-MM-DD'); } fetchSmartFundNetValue(fund.code, queryDate).then(result => { if (result) { setPrice(result.value); setActualDate(result.date); } else { setPrice(0); setActualDate(null); } }).finally(() => setLoadingPrice(false)); } }, [date, isAfter3pm, isBuy, fund]); const [feeMode, setFeeMode] = useState('rate'); const [feeValue, setFeeValue] = useState('0'); const [showConfirm, setShowConfirm] = useState(false); const sellShare = parseFloat(share) || 0; const sellPrice = parseFloat(price) || 0; const sellAmount = sellShare * sellPrice; let sellFee = 0; if (feeMode === 'rate') { const rate = parseFloat(feeValue) || 0; sellFee = sellAmount * (rate / 100); } else { sellFee = parseFloat(feeValue) || 0; } const estimatedReturn = sellAmount - sellFee; useEffect(() => { if (!isBuy) return; const a = parseFloat(amount); const f = parseFloat(feeRate); const p = parseFloat(price); if (a > 0 && !isNaN(f)) { if (p > 0) { const netAmount = a / (1 + f / 100); const s = netAmount / p; setCalcShare(s.toFixed(2)); } else { setCalcShare('待确认'); } } else { setCalcShare(null); } }, [isBuy, amount, feeRate, price]); const handleSubmit = (e) => { e.preventDefault(); if (isBuy) { if (!amount || !feeRate || !date || calcShare === null) return; setShowConfirm(true); } else { if (!share || !date) return; setShowConfirm(true); } }; const handleFinalConfirm = () => { if (isBuy) { onConfirm({ share: calcShare === '待确认' ? null : Number(calcShare), price: Number(price), totalCost: Number(amount), date: actualDate || date, isAfter3pm, feeRate: Number(feeRate) }); return; } onConfirm({ share: Number(share), price: Number(price), date: actualDate || date, isAfter3pm, feeMode, feeValue }); }; const isValid = isBuy ? (!!amount && !!feeRate && !!date && calcShare !== null) : (!!share && !!date); const handleSetShareFraction = (fraction) => { if (availableShare > 0) { setShare((availableShare * fraction).toFixed(2)); } }; const [revokeTrade, setRevokeTrade] = useState(null); return ( e.stopPropagation()} style={{ maxWidth: '420px' }} >
{isBuy ? '📥' : '📤'} {showPendingList ? '待交易队列' : (showConfirm ? (isBuy ? '买入确认' : '卖出确认') : (isBuy ? '加仓' : '减仓'))}
{!showPendingList && !showConfirm && currentPendingTrades.length > 0 && (
setShowPendingList(true)} > ⚠️ 当前有 {currentPendingTrades.length} 笔待处理交易 查看详情 >
)} {showPendingList ? (
{currentPendingTrades.map((trade, idx) => (
{trade.type === 'buy' ? '买入' : '卖出'} {trade.date} {trade.isAfter3pm ? '(15:00后)' : ''}
份额/金额 {trade.share ? `${trade.share} 份` : `¥${trade.amount}`}
状态
等待净值更新...
))}
) : ( <> {!showConfirm && (
{fund?.name}
#{fund?.code}
)} {showConfirm ? ( isBuy ? (
基金名称 {fund?.name}
买入金额 ¥{Number(amount).toFixed(2)}
买入费率 {Number(feeRate).toFixed(2)}%
参考净值 {loadingPrice ? '查询中...' : (price ? `¥${Number(price).toFixed(4)}` : '待查询 (加入队列)')}
预估份额 {calcShare === '待确认' ? '待确认' : `${Number(calcShare).toFixed(2)} 份`}
买入日期 {date}
交易时段 {isAfter3pm ? '15:00后' : '15:00前'}
{loadingPrice ? '正在获取该日净值...' : `*基于${price === getEstimatePrice() ? '当前净值/估值' : '当日净值'}测算`}
{holding && calcShare !== '待确认' && (
持仓变化预览
持有份额
{holding.share.toFixed(2)} {(holding.share + Number(calcShare)).toFixed(2)}
{price ? (
持有市值 (估)
¥{(holding.share * Number(price)).toFixed(2)} ¥{((holding.share + Number(calcShare)) * Number(price)).toFixed(2)}
) : null}
)}
) : (
基金名称 {fund?.name}
卖出份额 {sellShare.toFixed(2)} 份
预估卖出单价 {loadingPrice ? '查询中...' : (price ? `¥${sellPrice.toFixed(4)}` : '待查询 (加入队列)')}
卖出费率/费用 {feeMode === 'rate' ? `${feeValue}%` : `¥${feeValue}`}
预估手续费 {price ? `¥${sellFee.toFixed(2)}` : '待计算'}
卖出日期 {date}
预计回款 {loadingPrice ? '计算中...' : (price ? `¥${estimatedReturn.toFixed(2)}` : '待计算')}
{loadingPrice ? '正在获取该日净值...' : `*基于${price === getEstimatePrice() ? '当前净值/估值' : '当日净值'}测算`}
{holding && (
持仓变化预览
持有份额
{holding.share.toFixed(2)} {(holding.share - sellShare).toFixed(2)}
{price ? (
持有市值 (估)
¥{(holding.share * sellPrice).toFixed(2)} ¥{((holding.share - sellShare) * sellPrice).toFixed(2)}
) : null}
)}
) ) : (
{isBuy ? ( <>
{loadingPrice ? ( 正在查询净值数据... ) : price === 0 ? null : (
参考净值: {Number(price).toFixed(4)}
)}
) : ( <>
{holding && holding.share > 0 && (
{[ { label: '1/4', value: 0.25 }, { label: '1/3', value: 1 / 3 }, { label: '1/2', value: 0.5 }, { label: '全部', value: 1 } ].map((opt) => ( ))}
)} {holding && (
当前持仓: {holding.share.toFixed(2)} 份 {pendingSellShare > 0 && 冻结: {pendingSellShare.toFixed(2)} 份}
)}
{loadingPrice ? ( 正在查询净值数据... ) : price === 0 ? null : (
参考净值: {price.toFixed(4)}
)}
)}
)} )}
{revokeTrade && ( { onDeletePending?.(revokeTrade.id); setRevokeTrade(null); }} onCancel={() => setRevokeTrade(null)} confirmText="确认撤销" /> )}
); }