feat: 加仓自动获取费率数据
This commit is contained in:
@@ -720,7 +720,7 @@ const snapshotPingzhongdataGlobals = (fundCode) => {
|
||||
};
|
||||
};
|
||||
|
||||
const jsonpLoadPingzhongdata = (fundCode, timeoutMs = 10000) => {
|
||||
const jsonpLoadPingzhongdata = (fundCode, timeoutMs = 20000) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof document === 'undefined' || !document.body) {
|
||||
reject(new Error('无浏览器环境'));
|
||||
|
||||
@@ -6,7 +6,7 @@ import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import { isNumber } from 'lodash';
|
||||
import { fetchSmartFundNetValue } from '../api/fund';
|
||||
import { fetchFundPingzhongdata, fetchSmartFundNetValue } from '../api/fund';
|
||||
import { DatePicker, NumericInput } from './Common';
|
||||
import ConfirmModal from './ConfirmModal';
|
||||
import { CloseIcon } from './Icons';
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import PendingTradesModal from './PendingTradesModal';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
@@ -39,12 +40,65 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
|
||||
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]);
|
||||
@@ -148,7 +202,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
|
||||
};
|
||||
|
||||
const isValid = isBuy
|
||||
? (!!amount && !!feeRate && !!date && calcShare !== null)
|
||||
? (!!amount && !!feeRate && !!date && calcShare !== null && !loadingBuyMeta && (parseFloat(amount) || 0) >= (Number(minBuyAmount) || 0))
|
||||
: (!!share && !!date);
|
||||
|
||||
const handleSetShareFraction = (fraction) => {
|
||||
@@ -372,72 +426,112 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
|
||||
<form onSubmit={handleSubmit}>
|
||||
{isBuy ? (
|
||||
<>
|
||||
<div className="form-group" style={{ marginBottom: 16 }}>
|
||||
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
|
||||
加仓金额 (¥) <span style={{ color: 'var(--danger)' }}>*</span>
|
||||
</label>
|
||||
<div style={{ border: !amount ? '1px solid var(--danger)' : '1px solid var(--border)', borderRadius: 12 }}>
|
||||
<NumericInput
|
||||
value={amount}
|
||||
onChange={setAmount}
|
||||
step={100}
|
||||
min={0}
|
||||
placeholder="请输入加仓金额"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{ pointerEvents: loadingBuyMeta ? 'none' : 'auto', opacity: loadingBuyMeta ? 0.55 : 1 }}>
|
||||
<div className="form-group" style={{ marginBottom: 16 }}>
|
||||
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
|
||||
加仓金额 (¥) <span style={{ color: 'var(--danger)' }}>*</span>
|
||||
</label>
|
||||
<div
|
||||
style={{
|
||||
border: (!amount || (Number(minBuyAmount) > 0 && (parseFloat(amount) || 0) < Number(minBuyAmount)))
|
||||
? '1px solid var(--danger)'
|
||||
: '1px solid var(--border)',
|
||||
borderRadius: 12
|
||||
}}
|
||||
>
|
||||
<NumericInput
|
||||
value={amount}
|
||||
onChange={setAmount}
|
||||
step={100}
|
||||
min={Number(minBuyAmount) || 0}
|
||||
placeholder={(Number(minBuyAmount) || 0) > 0 ? `最少 ¥${Number(minBuyAmount)},请输入加仓金额` : '请输入加仓金额'}
|
||||
/>
|
||||
</div>
|
||||
{(Number(minBuyAmount) || 0) > 0 && (
|
||||
<div className="muted" style={{ fontSize: '12px', marginTop: 6 }}>
|
||||
最小加仓金额:¥{Number(minBuyAmount)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="row" style={{ gap: 12, marginBottom: 16 }}>
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
|
||||
买入费率 (%) <span style={{ color: 'var(--danger)' }}>*</span>
|
||||
</label>
|
||||
<div style={{ border: !feeRate ? '1px solid var(--danger)' : '1px solid var(--border)', borderRadius: 12 }}>
|
||||
<NumericInput
|
||||
value={feeRate}
|
||||
onChange={setFeeRate}
|
||||
step={0.01}
|
||||
min={0}
|
||||
placeholder="0.12"
|
||||
/>
|
||||
<div className="row" style={{ gap: 12, marginBottom: 16 }}>
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
|
||||
买入费率 (%) <span style={{ color: 'var(--danger)' }}>*</span>
|
||||
</label>
|
||||
<div style={{ border: !feeRate ? '1px solid var(--danger)' : '1px solid var(--border)', borderRadius: 12 }}>
|
||||
<NumericInput
|
||||
value={feeRate}
|
||||
onChange={setFeeRate}
|
||||
step={0.01}
|
||||
min={0}
|
||||
placeholder="0.12"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
|
||||
加仓日期 <span style={{ color: 'var(--danger)' }}>*</span>
|
||||
</label>
|
||||
<DatePicker value={date} onChange={setDate} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group" style={{ marginBottom: 12 }}>
|
||||
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
|
||||
交易时段
|
||||
</label>
|
||||
<div className="trade-time-slot row" style={{ gap: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
className={!isAfter3pm ? 'trade-time-btn active' : 'trade-time-btn'}
|
||||
onClick={() => setIsAfter3pm(false)}
|
||||
>
|
||||
15:00前
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={isAfter3pm ? 'trade-time-btn active' : 'trade-time-btn'}
|
||||
onClick={() => setIsAfter3pm(true)}
|
||||
>
|
||||
15:00后
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12, fontSize: '12px' }}>
|
||||
{buyMetaError ? (
|
||||
<span className="muted" style={{ color: 'var(--danger)' }}>{buyMetaError}</span>
|
||||
) : null}
|
||||
{loadingPrice ? (
|
||||
<span className="muted">正在查询净值数据...</span>
|
||||
) : price === 0 ? null : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<span className="muted">参考净值: {Number(price).toFixed(4)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group" style={{ flex: 1 }}>
|
||||
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
|
||||
加仓日期 <span style={{ color: 'var(--danger)' }}>*</span>
|
||||
</label>
|
||||
<DatePicker value={date} onChange={setDate} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group" style={{ marginBottom: 12 }}>
|
||||
<label className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
|
||||
交易时段
|
||||
</label>
|
||||
<div className="trade-time-slot row" style={{ gap: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
className={!isAfter3pm ? 'trade-time-btn active' : 'trade-time-btn'}
|
||||
onClick={() => setIsAfter3pm(false)}
|
||||
{loadingBuyMeta && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 10,
|
||||
padding: 12,
|
||||
borderRadius: 12,
|
||||
background: 'rgba(0,0,0,0.25)',
|
||||
backdropFilter: 'blur(2px)',
|
||||
WebkitBackdropFilter: 'blur(2px)',
|
||||
}}
|
||||
>
|
||||
15:00前
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={isAfter3pm ? 'trade-time-btn active' : 'trade-time-btn'}
|
||||
onClick={() => setIsAfter3pm(true)}
|
||||
>
|
||||
15:00后
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 12, fontSize: '12px' }}>
|
||||
{loadingPrice ? (
|
||||
<span className="muted">正在查询净值数据...</span>
|
||||
) : price === 0 ? null : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<span className="muted">参考净值: {Number(price).toFixed(4)}</span>
|
||||
<Spinner className="size-5" />
|
||||
<span className="muted" style={{ fontSize: 12 }}>正在加载买入费率/最小金额...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -564,8 +658,8 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
|
||||
<button
|
||||
type="submit"
|
||||
className="button"
|
||||
disabled={!isValid || loadingPrice}
|
||||
style={{ flex: 1, opacity: (!isValid || loadingPrice) ? 0.6 : 1 }}
|
||||
disabled={!isValid || loadingPrice || (isBuy && loadingBuyMeta)}
|
||||
style={{ flex: 1, opacity: (!isValid || loadingPrice || (isBuy && loadingBuyMeta)) ? 0.6 : 1 }}
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
|
||||
21
components/ui/spinner.jsx
Normal file
21
components/ui/spinner.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Loader2Icon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Spinner({
|
||||
className,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<Loader2Icon
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
className={cn(
|
||||
"size-4 animate-spin text-muted-foreground motion-reduce:animate-none",
|
||||
className
|
||||
)}
|
||||
{...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Spinner }
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -5923,7 +5923,7 @@
|
||||
},
|
||||
"node_modules/class-variance-authority": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||
"resolved": "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
||||
Reference in New Issue
Block a user