'use client'; import { useEffect, useMemo, useState } from 'react'; import { AnimatePresence } from 'framer-motion'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import timezone from 'dayjs/plugin/timezone'; import { isNumber } from 'lodash'; import { fetchFundPingzhongdata, fetchSmartFundNetValue } from '../api/fund'; import { DatePicker, NumericInput } from './Common'; import ConfirmModal from './ConfirmModal'; import { CloseIcon } from './Icons'; import { Dialog, DialogContent, DialogTitle, } from '@/components/ui/dialog'; import PendingTradesModal from './PendingTradesModal'; import { Spinner } from '@/components/ui/spinner'; 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 [minBuyAmount, setMinBuyAmount] = useState(0); const [loadingBuyMeta, setLoadingBuyMeta] = useState(false); const [buyMetaError, setBuyMetaError] = useState(null); const [date, setDate] = useState(() => { return formatDate(); }); const [isAfter3pm, setIsAfter3pm] = useState(nowInTz().hour() >= 15); const [calcShare, setCalcShare] = useState(null); const parseNumberish = (input) => { if (input === null || typeof input === 'undefined') return null; if (typeof input === 'number') return Number.isFinite(input) ? input : null; const cleaned = String(input).replace(/[^\d.]/g, ''); const n = parseFloat(cleaned); return Number.isFinite(n) ? n : null; }; useEffect(() => { if (!isBuy || !fund?.code) return; let cancelled = false; setLoadingBuyMeta(true); setBuyMetaError(null); fetchFundPingzhongdata(fund.code) .then((pz) => { if (cancelled) return; const rate = parseNumberish(pz?.fund_Rate); const minsg = parseNumberish(pz?.fund_minsg); if (Number.isFinite(minsg)) { setMinBuyAmount(minsg); } else { setMinBuyAmount(0); } if (Number.isFinite(rate)) { setFeeRate((prev) => { const prevNum = parseNumberish(prev); const shouldOverride = prev === '' || prev === '0' || prevNum === 0 || prevNum === null; return shouldOverride ? rate.toFixed(2) : prev; }); } }) .catch((e) => { if (cancelled) return; setBuyMetaError(e?.message || '买入信息加载失败'); setMinBuyAmount(0); }) .finally(() => { if (cancelled) return; setLoadingBuyMeta(false); }); return () => { cancelled = true; }; }, [isBuy, fund?.code]); 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 : (isNumber(fund?.gsz) ? 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 && !loadingBuyMeta && (parseFloat(amount) || 0) >= (Number(minBuyAmount) || 0)) : (!!share && !!date); const handleSetShareFraction = (fraction) => { if (availableShare > 0) { setShare((availableShare * fraction).toFixed(2)); } }; const [revokeTrade, setRevokeTrade] = useState(null); const handleOpenChange = (open) => { if (!open) { onClose?.(); } }; return ( {isBuy ? '加仓' : '减仓'}
{isBuy ? '📥' : '📤'} {showConfirm ? (isBuy ? '买入确认' : '卖出确认') : (isBuy ? '加仓' : '减仓')}
{!showConfirm && currentPendingTrades.length > 0 && (
setShowPendingList(true)} > ⚠️ 当前有 {currentPendingTrades.length} 笔待处理交易 查看详情 >
)} {!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 ? ( <>
0 && (parseFloat(amount) || 0) < Number(minBuyAmount))) ? '1px solid var(--danger)' : '1px solid var(--border)', borderRadius: 12 }} > 0 ? `最少 ¥${Number(minBuyAmount)},请输入加仓金额` : '请输入加仓金额'} />
{(Number(minBuyAmount) || 0) > 0 && (
最小加仓金额:¥{Number(minBuyAmount)}
)}
{buyMetaError ? ( {buyMetaError} ) : null} {loadingPrice ? ( 正在查询净值数据... ) : price === 0 ? null : (
参考净值: {Number(price).toFixed(4)}
)}
{loadingBuyMeta && (
正在加载买入费率/最小金额...
)}
) : ( <>
{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="确认撤销" /> )} setShowPendingList(false)} onRevoke={(trade) => setRevokeTrade(trade)} />
); }