/** * 下单面板 */ import React, { useCallback, useEffect, useMemo, useRef, useState, useImperativeHandle, forwardRef, ForwardRefRenderFunction, Fragment } from "react"; import { PlaceOrderTypeTab, OrderTypeTab, AvbBalance, OrderValue, OrderValueProps, Buttons, CenterQuantity, CloseValue } from "../Form"; import { OrderTypeEnum, OrderSideEnum, TimeInForceEnum, PriceSourceEnum } from "@shared/types"; import { FuturesFormProps, IPlaceOrderParams } from "./interface"; import Slider from "@components/Slider"; import type { IFormatCurrentPair } from "@shared/types/interface"; import { getQuantityByCost, getCostByQuantity, getValueByContr, getRateValue, getContrByValue } from "@utils/contractRules"; import { getPositionSide, getPositionSideWithSideText, formatAvbBalance, _DEFAULT } from "./helper"; import { formatFloor, toDefaultNumber } from "@utils/FormatNumber"; import { notification } from "@atomintl/aui-next"; import { useBaseOn, useOrderPrice, useMaxInputQuantity } from "./hooks"; import { PlaceOrderInput, InputType } from "../Form"; import { PriceInput } from "@components/PlaceOrder/Form/PlaceOrderInput/PriceInput"; import { QuantityInput } from "@components/PlaceOrder/Form/PlaceOrderInput/QuantityInput"; import { PriceSourceSelect } from "@components/PlaceOrder/Form/PlaceOrderInput/SuffixPrice/PriceSourceSelect"; import PostOnlyAndTimeInForce from "./Form/PostOnlyAndTimeInForce"; const Form: ForwardRefRenderFunction<{ reset: () => void }, FuturesFormProps> = (props, ref) => { const { closeValues, openBaseQty, lastPrice, buyOnePrice, sellOnePrice, isLogin = false } = props; const currentPair = props.currentPair ?? ({} as IFormatCurrentPair); const { settleCurrency, base, quote, tickSizeScale, tickSizeStep, costSizeScale = 4, ctValScale = 4, ctVal } = currentPair; const init = useMemo(() => currentPair?.symbol?.length, [currentPair?.symbol]); const avbBalance = useMemo(() => { return formatAvbBalance(props.avbBalance, costSizeScale); }, [props.avbBalance, costSizeScale]); const leverage = useMemo(() => { // 不需要格式化计算,只要格式化展示 return props.leverage ?? 1; }, [props.leverage]); const takerRate = useMemo(() => { return toDefaultNumber(currentPair.takerRate); }, [currentPair.takerRate]); // 输入框 const [inputValues, setInputValues] = useState({ price: "", triggerPrice: "", quantity: "", sliderValue: 0 }); const [triggerPxType, setPriceSource] = useState(PriceSourceEnum.MarkPrice); const [timeInForce, setTimeInForce] = useState(TimeInForceEnum.GTC); const [postOnly, setPostOnly] = useState(false); const [orderType, setOrderType] = useState(OrderTypeEnum.Limit); const [isClose, setIsClose] = useState(false); const qtyInputRef = useRef(null); const priceInputRef = useRef(null); const orderValueRef = useRef(null); // 多空方向的数量和成本 const [sideQuantity, setSideQuantity] = useState(_DEFAULT); const [sideCost, setSideCost] = useState(_DEFAULT); const isCondition = useMemo( () => orderType === OrderTypeEnum.TriggerLimit || orderType === OrderTypeEnum.TriggerMarket, [orderType] ); const baseOn = useBaseOn(isClose, isCondition, openBaseQty); const orderPrice = useOrderPrice(inputValues.price, inputValues.triggerPrice, buyOnePrice, sellOnePrice, orderType); const handleClearQuantityInput = (focus?: boolean) => { setInputValues({ ...inputValues, sliderValue: 0, quantity: "" }); if (focus) qtyInputRef.current?.focus?.(); setSideQuantity(_DEFAULT); setSideCost(_DEFAULT); }; const onChangePlaceOrderType = (bool: boolean) => { setIsClose(bool); handleClearQuantityInput(); setOrderType(OrderTypeEnum.Limit); }; const onChangeOrderType = (type: OrderTypeEnum) => { console.log(type, "订单类型"); setOrderType(type); handleClearQuantityInput(); }; const onChangeQuantityInput = (val: string) => { setInputValues({ ...inputValues, quantity: val, sliderValue: 0 }); if (baseOn) { setSideQuantity({ buy: val, sell: val, show: false }); } else { setSideCost({ buy: val, sell: val, show: false }); } }; // 最大数量value const maxInputQuantity = useMaxInputQuantity( isClose, closeValues, orderPrice, avbBalance, ctVal, ctValScale, takerRate, leverage ); const onChangeBaseQuantity = (bool: boolean) => { // 按面值/成本下单 handleClearQuantityInput(); props.handleBaseOnChange?.(bool); }; const onChangePriceSource = useCallback((val: PriceSourceEnum) => { setPriceSource(val); }, []); const handleSliderChange = (rate: number) => { qtyInputRef.current?.blur?.(); let cost = ""; if (rate && !baseOn && avbBalance) { // 计算成本 cost = formatFloor(getRateValue({ rate, max: avbBalance }), costSizeScale); setSideCost({ buy: cost, sell: cost, show: toDefaultNumber(cost) > 0 }); } // 清空数量输入框 setInputValues({ ...inputValues, quantity: cost, sliderValue: rate }); }; const onChangePriceInput = (val: string, key: InputType, focus?: boolean) => { console.log(val, key, "更新价格"); setInputValues({ ...inputValues, [key]: val }); if (focus) { priceInputRef?.current?.focus?.(); } }; const sideText = useMemo(() => { return getPositionSideWithSideText(isClose); }, [isClose]); const isMarket = useMemo( () => orderType === OrderTypeEnum.Market || orderType === OrderTypeEnum.TriggerMarket, [orderType] ); useImperativeHandle(ref, () => ({ reset: () => { // 清除下单面板(场景下单成功) setInputValues({ price: "", triggerPrice: "", quantity: "", sliderValue: 0 }); setSideCost(_DEFAULT); setSideQuantity(_DEFAULT); } })); const handlePlaceOrder = (side: OrderSideEnum) => { if (!init) return; // 限制价格 const { price, triggerPrice, quantity, sliderValue } = inputValues; const isBuy = side === OrderSideEnum.Buy; const quantityValue = isBuy ? sideQuantity.buy : sideQuantity.sell; let message: null | string = null; if (!price && !isMarket) { message = "请输入价格"; } else if (!triggerPrice && isCondition) { message = "请输入触发价格"; } else if (!quantity && !sliderValue) { message = "请输入数量"; } if (message) { return notification.error({ duration: 2000, message }); } // 可开为0处理 const orderQty = getContrByValue({ value: quantityValue, multiplier: ctVal }); const params: IPlaceOrderParams = { quantityValue, isClose, isBuy, isCondition, isMarket, sideText: isBuy ? sideText.buy : sideText.sell, side, positionSide: getPositionSide(isBuy, isClose), orderType, orderQty, cost: isBuy ? sideCost.buy : sideCost.sell, orderValue: orderValueRef?.current?.getOrderValue(side), postOnly, timeInForce, price, triggerPxType, ordPx: inputValues.triggerPrice }; props.handlePlaceOrder?.(params); }; useEffect(() => { // 滑块计算数量 if (inputValues.sliderValue && baseOn) { const buy = getRateValue({ rate: inputValues.sliderValue, max: maxInputQuantity.maxBuyValue ?? 0 }); const sell = getRateValue({ rate: inputValues.sliderValue, max: maxInputQuantity.maxSellValue ?? 0 }); setSideQuantity({ buy: formatFloor(buy, ctValScale), sell: formatFloor(sell, ctValScale), show: buy > 0 || sell > 0 }); console.log("计算数量:", buy, sell); } }, [inputValues.sliderValue, baseOn, maxInputQuantity.maxBuyValue, maxInputQuantity.maxSellValue, ctValScale]); useEffect(() => { if (!baseOn) { // 按成本下单。根据成本计算数量 let buy = "", sell = ""; const qtyBalance = toDefaultNumber(inputValues.quantity); if (qtyBalance) { buy = formatFloor( getQuantityByCost({ balance: qtyBalance, price: orderPrice.buy, takerFee: takerRate, leverage }), ctValScale ); sell = formatFloor( getQuantityByCost({ balance: qtyBalance, price: orderPrice.sell, takerFee: takerRate, leverage }), ctValScale ); } console.log("计算数量:", buy, sell); setSideQuantity({ buy, sell, show: false }); } }, [baseOn, inputValues.quantity, orderPrice.buy, orderPrice.sell, ctValScale, takerRate]); useEffect(() => { if (baseOn && !isClose) { // 计算成本 const buy = getCostByQuantity({ price: orderPrice.buy, quantity: sideQuantity.buy, leverage, takerFee: takerRate }); const sell = getCostByQuantity({ price: orderPrice.sell, quantity: sideQuantity.sell, leverage, takerFee: takerRate }); console.log("计算成本:", buy, sell); setSideCost({ buy: formatFloor(buy, costSizeScale), sell: formatFloor(sell, costSizeScale), show: buy > 0 || sell > 0 }); } }, [ baseOn, isClose, orderPrice.buy, orderPrice.sell, leverage, takerRate, costSizeScale, sideQuantity.buy, sideQuantity.sell ]); return (
{isCondition ? ( } onChange={val => onChangePriceInput(val, InputType.TriggerPrice)} value={inputValues.triggerPrice} limitInputStep={true} scale={tickSizeScale} step={tickSizeStep} name={InputType.TriggerPrice} /> ) : ( "" )} onChangePriceInput(val, InputType.Price, focus)} triggerTypeChange={onChangeOrderType} lastPrice={lastPrice} optimumLevel={currentPair.optimumLevel} /> {sideCost.show && baseOn ? (
成本
{baseOn ? settleCurrency : base}
) : ( "" )}
{isLogin ? ( {isClose ? ( ) : baseOn ? ( ) : ( "" )} ) : null}
); }; export { FuturesFormProps, IPlaceOrderParams, IPlaceNormalOrderParams, IPlaceStopOrderParams } from "./interface"; export const FuturesForm = forwardRef(Form); FuturesForm.defaultProps = { buyOnePrice: "17350", sellOnePrice: "17400", lastPrice: "17320" };