Initial commit: Monisuo - 虚拟货币模拟交易平台
功能模块: - 用户注册/登录/KYC - 资金账户/交易账户 - 实时行情/币种管理 - 即时交易/充提审核 - 管理后台 技术栈: - 后端: SpringBoot 2.2.4 + MyBatis Plus - 前端: uni-app x (Vue3 + UTS) - 数据库: MySQL Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
361
app/pages/trade/trade.uvue
Normal file
361
app/pages/trade/trade.uvue
Normal file
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<view class="trade-container">
|
||||
<!-- 交易对选择 -->
|
||||
<view class="pair-selector" @click="showPairPicker = true">
|
||||
<text class="pair-text">{{ selectedPair }}</text>
|
||||
<text class="pair-arrow">▼</text>
|
||||
</view>
|
||||
|
||||
<!-- 价格信息 -->
|
||||
<view class="price-section">
|
||||
<text class="current-price">${{ currentPrice }}</text>
|
||||
<text :class="['price-change', priceChange >= 0 ? 'up' : 'down']">
|
||||
{{ priceChange >= 0 ? '+' : '' }}{{ priceChange }}%
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 买卖切换 -->
|
||||
<view class="trade-tabs">
|
||||
<text :class="['trade-tab', tradeType === 'buy' ? 'active buy' : '']" @click="tradeType = 'buy'">买入</text>
|
||||
<text :class="['trade-tab', tradeType === 'sell' ? 'active sell' : '']" @click="tradeType = 'sell'">卖出</text>
|
||||
</view>
|
||||
|
||||
<!-- 交易表单 -->
|
||||
<view class="trade-form">
|
||||
<view class="form-item">
|
||||
<text class="form-label">价格(USDT)</text>
|
||||
<view class="input-row">
|
||||
<input class="form-input" type="digit" v-model="price" />
|
||||
<text class="input-unit">USDT</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="form-label">数量</text>
|
||||
<view class="input-row">
|
||||
<input class="form-input" type="digit" v-model="quantity" />
|
||||
<text class="input-unit">{{ coinCode }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="amount-row">
|
||||
<text class="amount-label">交易金额</text>
|
||||
<text class="amount-value">{{ totalAmount }} USDT</text>
|
||||
</view>
|
||||
|
||||
<view class="balance-row">
|
||||
<text class="balance-label">可用</text>
|
||||
<text class="balance-value">{{ availableBalance }} USDT</text>
|
||||
</view>
|
||||
|
||||
<!-- 交易按钮 -->
|
||||
<button :class="['trade-btn', tradeType]" @click="handleTrade">
|
||||
<text class="btn-text">{{ tradeType === 'buy' ? '买入' : '卖出' }} {{ coinCode }}</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 持仓信息 -->
|
||||
<view class="position-section">
|
||||
<text class="section-title">当前持仓</text>
|
||||
<view class="position-info">
|
||||
<text class="position-label">{{ coinCode }}</text>
|
||||
<text class="position-value">{{ positionQuantity }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="uts">
|
||||
import { buy, sell } from '@/api/trade.uts'
|
||||
import { getOverview, getTradeAccount } from '@/api/asset.uts'
|
||||
import { getCoinDetail } from '@/api/market.uts'
|
||||
|
||||
const coinCode = ref('BTC')
|
||||
const selectedPair = ref('BTC/USDT')
|
||||
const currentPrice = ref('0.00')
|
||||
const priceChange = ref(0)
|
||||
const tradeType = ref('buy')
|
||||
const price = ref('')
|
||||
const quantity = ref('')
|
||||
const availableBalance = ref('0.00')
|
||||
const positionQuantity = ref('0.0000')
|
||||
const showPairPicker = ref(false)
|
||||
|
||||
const totalAmount = computed((): string => {
|
||||
const p = parseFloat(price.value) || 0
|
||||
const q = parseFloat(quantity.value) || 0
|
||||
return (p * q).toFixed(2)
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
loadData()
|
||||
})
|
||||
|
||||
func loadData () {
|
||||
loadBalance()
|
||||
loadCoinInfo()
|
||||
loadPosition()
|
||||
}
|
||||
|
||||
func loadBalance () {
|
||||
getOverview()
|
||||
.then((res) => {
|
||||
const data = res.data as UTSJSONObject
|
||||
// 获取交易账户USDT余额
|
||||
const positions = data['positions'] as Array<UTSJSONObject>
|
||||
for (let i = 0; i < positions.length; i++) {
|
||||
const pos = positions[i]
|
||||
if (pos['coinCode'] === 'USDT') {
|
||||
availableBalance.value = (pos['quantity'] as number).toFixed(2)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
func loadCoinInfo () {
|
||||
getCoinDetail(coinCode.value)
|
||||
.then((res) => {
|
||||
const data = res.data as UTSJSONObject
|
||||
currentPrice.value = formatPrice(data['price'] as number)
|
||||
priceChange.value = (data['change24h'] as number) || 0
|
||||
price.value = currentPrice.value
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
func loadPosition () {
|
||||
getTradeAccount()
|
||||
.then((res) => {
|
||||
const data = res.data as UTSJSONObject
|
||||
const positions = data['positions'] as Array<UTSJSONObject>
|
||||
for (let i = 0; i < positions.length; i++) {
|
||||
const pos = positions[i]
|
||||
if (pos['coinCode'] === coinCode.value) {
|
||||
positionQuantity.value = (pos['quantity'] as number).toFixed(4)
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
func handleTrade () {
|
||||
const priceValue = parseFloat(price.value) || 0
|
||||
const quantityValue = parseFloat(quantity.value) || 0
|
||||
|
||||
if (priceValue <= 0) {
|
||||
uni.showToast({ title: '请输入有效价格', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (quantityValue <= 0) {
|
||||
uni.showToast({ title: '请输入有效数量', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const api = tradeType.value === 'buy' ? buy : sell
|
||||
|
||||
api(coinCode.value, price.value, quantity.value)
|
||||
.then((res) => {
|
||||
uni.showToast({ title: tradeType.value === 'buy' ? '买入成功' : '卖出成功', icon: 'success' })
|
||||
price.value = ''
|
||||
quantity.value = ''
|
||||
loadData()
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
func formatPrice (value: number): string {
|
||||
if (value >= 1000) return value.toFixed(2)
|
||||
if (value >= 1) return value.toFixed(4)
|
||||
return value.toFixed(6)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.trade-container {
|
||||
min-height: 100vh;
|
||||
background: #1A1A2E;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.pair-selector {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.pair-text {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.pair-arrow {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.price-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.current-price {
|
||||
font-size: 56rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.price-change {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.price-change.up { color: #00C853; }
|
||||
.price-change.down { color: #FF5252; }
|
||||
|
||||
.trade-tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 32rpx;
|
||||
background: #16213E;
|
||||
border-radius: 16rpx;
|
||||
padding: 8rpx;
|
||||
}
|
||||
|
||||
.trade-tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 16rpx 0;
|
||||
font-size: 30rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.trade-tab.active.buy {
|
||||
background: rgba(0, 200, 83, 0.2);
|
||||
color: #00C853;
|
||||
}
|
||||
|
||||
.trade-tab.active.sell {
|
||||
background: rgba(255, 82, 82, 0.2);
|
||||
color: #FF5252;
|
||||
}
|
||||
|
||||
.trade-form {
|
||||
margin-top: 32rpx;
|
||||
background: #16213E;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 12rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12rpx;
|
||||
padding: 0 24rpx;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.input-unit {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.amount-row, .balance-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.amount-label, .balance-label {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.amount-value, .balance-value {
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.trade-btn {
|
||||
margin-top: 24rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.trade-btn.buy {
|
||||
background: linear-gradient(90deg, #00C853 0%, #00A844 100%);
|
||||
}
|
||||
|
||||
.trade-btn.sell {
|
||||
background: linear-gradient(90deg, #FF5252 0%, #E04040 100%);
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.position-section {
|
||||
margin-top: 32rpx;
|
||||
background: #16213E;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 16rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.position-info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.position-label {
|
||||
font-size: 30rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.position-value {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #00D4AA;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user