feat: 添加交易方式配置,更新相关组件以支持限价和市价委托
This commit is contained in:
@@ -94,6 +94,8 @@ export type EaringsDetailData = Treaty.Data<typeof client.api.earnings.details.p
|
||||
|
||||
export type EaringsDetailBody = TreatyBody<typeof client.api.earnings.details.post>;
|
||||
|
||||
export type SpotOrderBody = TreatyBody<typeof client.api.spot_order.create.post>;
|
||||
|
||||
/**
|
||||
* 应用版本信息
|
||||
*/
|
||||
|
||||
9
src/views/trade/components/confirm-order.vue
Normal file
9
src/views/trade/components/confirm-order.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang='ts' setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
Hello world
|
||||
</template>
|
||||
|
||||
<style lang='css' scoped></style>
|
||||
@@ -1,292 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
type OrderType = "limit" | "market";
|
||||
type TradeAction = "buy" | "sell";
|
||||
|
||||
interface TradeFormData {
|
||||
orderType: OrderType;
|
||||
price: string;
|
||||
amount: string;
|
||||
total: string;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
action: TradeAction;
|
||||
}>();
|
||||
|
||||
const formData = reactive<TradeFormData>({
|
||||
orderType: "limit",
|
||||
price: "",
|
||||
amount: "",
|
||||
total: "",
|
||||
});
|
||||
|
||||
// TODO: 后续从API获取用户余额
|
||||
// const { data: balance } = await client.api.wallet.balances.get({ query: { accountType: 'trading' } })
|
||||
const availableBalance = ref({
|
||||
base: "0.00000000", // BTC
|
||||
quote: "10000.00", // USDT
|
||||
});
|
||||
|
||||
const isBuy = computed(() => props.action === "buy");
|
||||
const buttonColor = computed(() => isBuy.value ? "success" : "danger");
|
||||
const buttonText = computed(() => isBuy.value ? "买入 BTC" : "卖出 BTC");
|
||||
|
||||
// 计算总价
|
||||
watch(() => [formData.price, formData.amount], () => {
|
||||
if (formData.price && formData.amount) {
|
||||
const price = Number.parseFloat(formData.price);
|
||||
const amount = Number.parseFloat(formData.amount);
|
||||
if (!Number.isNaN(price) && !Number.isNaN(amount)) {
|
||||
formData.total = (price * amount).toFixed(2);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 根据总价计算数量
|
||||
watch(() => formData.total, (newTotal) => {
|
||||
if (newTotal && formData.price) {
|
||||
const total = Number.parseFloat(newTotal);
|
||||
const price = Number.parseFloat(formData.price);
|
||||
if (!Number.isNaN(total) && !Number.isNaN(price) && price > 0) {
|
||||
formData.amount = (total / price).toFixed(8);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function setPercentage(percent: number) {
|
||||
if (isBuy.value) {
|
||||
// 买入:根据USDT余额计算
|
||||
const balance = Number.parseFloat(availableBalance.value.quote);
|
||||
const total = balance * percent;
|
||||
formData.total = total.toFixed(2);
|
||||
}
|
||||
else {
|
||||
// 卖出:根据BTC余额计算
|
||||
const balance = Number.parseFloat(availableBalance.value.base);
|
||||
formData.amount = (balance * percent).toFixed(8);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
// TODO: 后续调用交易API
|
||||
// await client.api.trading.order.post({
|
||||
// symbol: 'BTC_USDT',
|
||||
// side: props.action,
|
||||
// type: formData.orderType,
|
||||
// price: formData.orderType === 'limit' ? formData.price : undefined,
|
||||
// quantity: formData.amount
|
||||
// })
|
||||
console.log("提交订单", {
|
||||
action: props.action,
|
||||
...formData,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="trade-form">
|
||||
<!-- 订单类型切换 -->
|
||||
<div class="order-type-tabs">
|
||||
<button
|
||||
:class="{ active: formData.orderType === 'limit' }"
|
||||
@click="formData.orderType = 'limit'"
|
||||
>
|
||||
限价
|
||||
</button>
|
||||
<button
|
||||
:class="{ active: formData.orderType === 'market' }"
|
||||
@click="formData.orderType = 'market'"
|
||||
>
|
||||
市价
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 可用余额 -->
|
||||
<div class="balance-info">
|
||||
<span class="label">可用</span>
|
||||
<span class="value">
|
||||
{{ isBuy ? availableBalance.quote : availableBalance.base }}
|
||||
{{ isBuy ? 'USDT' : 'BTC' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 价格输入 -->
|
||||
<div v-if="formData.orderType === 'limit'" class="input-group">
|
||||
<label>价格</label>
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
v-model="formData.price"
|
||||
type="number"
|
||||
placeholder="请输入价格"
|
||||
>
|
||||
<span class="unit">USDT</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数量输入 -->
|
||||
<div class="input-group">
|
||||
<label>数量</label>
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
v-model="formData.amount"
|
||||
type="number"
|
||||
placeholder="请输入数量"
|
||||
>
|
||||
<span class="unit">BTC</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 百分比选择 -->
|
||||
<div class="percentage-buttons">
|
||||
<button @click="setPercentage(0.25)">
|
||||
25%
|
||||
</button>
|
||||
<button @click="setPercentage(0.5)">
|
||||
50%
|
||||
</button>
|
||||
<button @click="setPercentage(0.75)">
|
||||
75%
|
||||
</button>
|
||||
<button @click="setPercentage(1)">
|
||||
100%
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 总额 -->
|
||||
<div class="input-group">
|
||||
<label>总额</label>
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
v-model="formData.total"
|
||||
type="number"
|
||||
placeholder="请输入总额"
|
||||
>
|
||||
<span class="unit">USDT</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<ion-button
|
||||
expand="block"
|
||||
:color="buttonColor"
|
||||
class="submit-button"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ buttonText }}
|
||||
</ion-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.trade-form {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.order-type-tabs {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.order-type-tabs button {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--ion-text-color-step-400);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.order-type-tabs button.active {
|
||||
color: var(--ion-color-primary);
|
||||
border-bottom-color: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
.balance-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--ion-text-color-step-400);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.balance-info .value {
|
||||
color: var(--ion-text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
font-size: 12px;
|
||||
color: var(--ion-text-color-step-400);
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--ion-color-light);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.input-wrapper input {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: 14px;
|
||||
color: var(--ion-text-color);
|
||||
}
|
||||
|
||||
.input-wrapper input::placeholder {
|
||||
color: var(--ion-text-color-step-600);
|
||||
}
|
||||
|
||||
.input-wrapper .unit {
|
||||
font-size: 12px;
|
||||
color: var(--ion-text-color-step-400);
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.percentage-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.percentage-buttons button {
|
||||
padding: 6px 0;
|
||||
background: var(--ion-color-light);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--ion-text-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.percentage-buttons button:active {
|
||||
transform: scale(0.95);
|
||||
background: var(--ion-color-light-shade);
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
margin-top: 8px;
|
||||
--border-radius: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
@@ -1,19 +1,71 @@
|
||||
<script lang='ts' setup>
|
||||
import type { PropType } from "vue";
|
||||
import type { TradeWayConfig } from "../config";
|
||||
import type { SpotOrderBody } from "@/api/types";
|
||||
import { modalController } from "@ionic/vue";
|
||||
import { caretDownOutline } from "ionicons/icons";
|
||||
import { tradeWayConfig } from "../config";
|
||||
|
||||
const model = defineModel({ type: Object as PropType<SpotOrderBody>, required: true });
|
||||
|
||||
function onSelectTradeWay(item: TradeWayConfig) {
|
||||
model.value.orderType = item.value;
|
||||
modalController.dismiss();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="bg-faint flex items-center justify-between px-4 py-2 rounded-md">
|
||||
<div id="open-modal" class="bg-faint flex items-center justify-between px-4 py-2 rounded-md">
|
||||
<div class="text-xs font-medium text-text-300">
|
||||
市场
|
||||
</div>
|
||||
<ion-icon :icon="caretDownOutline" />
|
||||
</div>
|
||||
<div class="bg-faint flex items-center justify-between px-4 py-2 rounded-md">
|
||||
<div class="text-xs font-medium text-text-400">
|
||||
市场价格
|
||||
|
||||
<ion-modal trigger="open-modal" :breakpoints="[0, 0.8]" :initial-breakpoint="0.8" handle>
|
||||
<div class="ion-padding">
|
||||
<div class="text-xs pb-3 text-text-300">
|
||||
基础委托
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div v-for="(item, index) in tradeWayConfig" :key="index" class="flex items-center" @click="onSelectTradeWay(item)">
|
||||
<div class="flex items-center flex-1">
|
||||
<Icon :icon="item.icon" class="text-2xl mr-4" />
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-base font-semibold">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="text-xs text-text-500">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="model.orderType === item.value" class="text-primary">
|
||||
<Icon icon="ic:sharp-check-circle" class="text-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<Icon icon="ic:twotone-more-horiz" class="text-2xl mr-4" />
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-sm text-text-500">
|
||||
更多委托类型,敬请期待
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ion-modal>
|
||||
</template>
|
||||
|
||||
<style lang='css' scoped></style>
|
||||
<style lang='css' scoped>
|
||||
ion-input.count {
|
||||
--background: var(--ion-color-faint);
|
||||
--padding-start: 16px;
|
||||
--padding-end: 16px;
|
||||
--padding-top: 4px;
|
||||
--padding-bottom: 4px;
|
||||
--border-radius: 6px;
|
||||
font-size: 12px;
|
||||
min-height: 40px;
|
||||
}
|
||||
</style>
|
||||
|
||||
48
src/views/trade/config.ts
Normal file
48
src/views/trade/config.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as yup from "yup";
|
||||
|
||||
export enum TradeWayValueEnum {
|
||||
LIMIT = "limit",
|
||||
MARKET = "market",
|
||||
}
|
||||
|
||||
export type TradeWayValue = `${TradeWayValueEnum}`;
|
||||
|
||||
export interface TradeWayConfig {
|
||||
name: string;
|
||||
value: TradeWayValue;
|
||||
description: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export const tradeWayConfig: TradeWayConfig[] = [
|
||||
{
|
||||
name: "限价委托",
|
||||
value: "limit",
|
||||
description: "以指定价格买入或卖出",
|
||||
icon: "hugeicons:trade-up",
|
||||
},
|
||||
];
|
||||
|
||||
export const confirmOrderSchema = yup.object({
|
||||
price: yup.number().when("way", {
|
||||
is: TradeWayValueEnum.LIMIT !== undefined,
|
||||
then: yup
|
||||
.number()
|
||||
.typeError("请输入有效的价格")
|
||||
.required("价格为必填项")
|
||||
.moreThan(0, "价格必须大于0"),
|
||||
otherwise: yup.number().notRequired(),
|
||||
}),
|
||||
amount: yup
|
||||
.number()
|
||||
.typeError("请输入有效的数量")
|
||||
.required("数量为必填项")
|
||||
.moreThan(0, "数量必须大于0"),
|
||||
way: yup
|
||||
.mixed<TradeWayValue>()
|
||||
.oneOf(
|
||||
Object.values(TradeWayValueEnum),
|
||||
"请选择有效的交易方式",
|
||||
)
|
||||
.required("交易方式为必填项"),
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { ChartingLibraryWidgetOptions } from "#/charting_library";
|
||||
import type { SpotOrderBody } from "@/api/types";
|
||||
import type { TradingViewInst } from "@/tradingview/index";
|
||||
import { modalController } from "@ionic/vue";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
@@ -12,6 +13,7 @@ import OrdersPanel from "./components/orders-panel.vue";
|
||||
import TradePairsModal from "./components/trade-pairs-modal.vue";
|
||||
import TradeSwitch from "./components/trade-switch.vue";
|
||||
import TradeWay from "./components/trade-way.vue";
|
||||
import { confirmOrderSchema, TradeWayValueEnum } from "./config";
|
||||
|
||||
const mode = useRouteQuery<TradeTypeEnum>("mode", TradeTypeEnum.BUY);
|
||||
const symbol = useRouteQuery<string>("symbol", "BTCUSD");
|
||||
@@ -22,6 +24,14 @@ const tradingviewOptions: Partial<ChartingLibraryWidgetOptions> = {
|
||||
],
|
||||
};
|
||||
const tradingViewInst = useTemplateRef<TradingViewInst>("tradingViewInst");
|
||||
const [form] = useResetRef<SpotOrderBody>({
|
||||
orderType: TradeWayValueEnum.LIMIT,
|
||||
quantity: "",
|
||||
side: mode.value,
|
||||
symbol: symbol.value,
|
||||
memo: "",
|
||||
price: "",
|
||||
});
|
||||
|
||||
async function openTradePairs() {
|
||||
const modal = await modalController.create({
|
||||
@@ -39,6 +49,14 @@ async function openTradePairs() {
|
||||
symbol.value = result;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
confirmOrderSchema.validate(form.value).then(() => {
|
||||
console.log("submit successfully");
|
||||
}).catch((err) => {
|
||||
console.log("submit failed:", err);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -69,14 +87,20 @@ async function openTradePairs() {
|
||||
|
||||
<div class="grid grid-cols-5 px-4">
|
||||
<div class="col-span-3 space-y-2">
|
||||
<TradeSwitch v-model:active="mode" />
|
||||
<TradeWay />
|
||||
<ion-input label="金额" class="count" inputmode="decimal" type="number" placeholder="请输入交易金额">
|
||||
<TradeSwitch v-model:active="mode" @update:active="val => form.side = val" />
|
||||
<TradeWay v-model="form" />
|
||||
<template v-if="form.orderType === 'limit'">
|
||||
<ion-input v-model="form.price" label="价格" class="count" inputmode="decimal" type="number" placeholder="请输入价格(USDT)" />
|
||||
</template>
|
||||
<ion-input v-model="form.quantity" label="数量" class="count" inputmode="decimal" type="number" placeholder="请输入交易数量">
|
||||
<span slot="end">{{ symbol }}</span>
|
||||
</ion-input>
|
||||
<ion-input v-model="form.price" label="金额" class="count" inputmode="decimal" type="number" placeholder="请输入交易金额">
|
||||
<span slot="end">USDT</span>
|
||||
</ion-input>
|
||||
<ion-range class="range" aria-label="Range with ticks" :pin="true" :ticks="true" :snaps="true" :min="0" :max="5" />
|
||||
<ion-button expand="block" size="small" shape="round" :color="mode === 'buy' ? 'success' : 'danger'">
|
||||
{{ mode === 'buy' ? '买入' : '卖出' }}
|
||||
<!-- <ion-range class="range" aria-label="Range with ticks" :pin="true" :ticks="true" :snaps="true" :min="0" :max="5" /> -->
|
||||
<ion-button expand="block" size="small" shape="round" :color="mode === TradeTypeEnum.BUY ? 'success' : 'danger'" @click="handleSubmit">
|
||||
{{ mode === TradeTypeEnum.BUY ? '买入' : '卖出' }}
|
||||
</ion-button>
|
||||
</div>
|
||||
<div class="col-span-2" />
|
||||
|
||||
Reference in New Issue
Block a user