11 changed files with 758 additions and 134 deletions
@ -0,0 +1 @@ |
|||||
|
/Volumes/MacintoshHD2/www/wwfsv.muzkcoinsss.com/doc |
||||
@ -0,0 +1,35 @@ |
|||||
|
import server from './server'; |
||||
|
|
||||
|
class FlashSwap { |
||||
|
static getCoinList() { |
||||
|
return server.get('/flashSwap/getCoinList'); |
||||
|
} |
||||
|
|
||||
|
static getRate(fromCoin, toCoin) { |
||||
|
return server.get('/flashSwap/getRate', { |
||||
|
params: { |
||||
|
from_coin: fromCoin, |
||||
|
to_coin: toCoin |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
static preview(data) { |
||||
|
return server.post('/flashSwap/preview', data); |
||||
|
} |
||||
|
|
||||
|
static execute(data) { |
||||
|
return server.post('/flashSwap/execute', data); |
||||
|
} |
||||
|
|
||||
|
static getHistory(params) { |
||||
|
return server.get('/flashSwap/getHistory', { params }); |
||||
|
} |
||||
|
|
||||
|
static getUserCoinBalance(coinName) { |
||||
|
const params = coinName ? { coin_name: coinName } : {}; |
||||
|
return server.get('/flashSwap/getUserCoinBalance', { params }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export default FlashSwap; |
||||
@ -0,0 +1,114 @@ |
|||||
|
<template> |
||||
|
<div class="flash-swap-history-page"> |
||||
|
<div class="container"> |
||||
|
<div class="page-title-row"> |
||||
|
<router-link to="/flash-swap" class="back-link">< {{ $t('flashSwap.back') }}</router-link> |
||||
|
<h3>{{ $t('flashSwap.history') }}</h3> |
||||
|
</div> |
||||
|
|
||||
|
<div class="panel-box bg-panel"> |
||||
|
<table class="table stripe"> |
||||
|
<thead> |
||||
|
<tr> |
||||
|
<th>{{ $t('flashSwap.fromCoin') }}</th> |
||||
|
<th>{{ $t('flashSwap.toCoin') }}</th> |
||||
|
<th>{{ $t('flashSwap.fromAmount') }}</th> |
||||
|
<th>{{ $t('flashSwap.toAmount') }}</th> |
||||
|
<th>{{ $t('flashSwap.fee') }}</th> |
||||
|
<th>{{ $t('flashSwap.time') }}</th> |
||||
|
<th class="text-right">{{ $t('flashSwap.status') }}</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<tr v-for="item in records" :key="item.id || item.order_id"> |
||||
|
<td>{{ item.from_coin_name || item.from_coin }}</td> |
||||
|
<td>{{ item.to_coin_name || item.to_coin }}</td> |
||||
|
<td>{{ item.from_amount }}</td> |
||||
|
<td>{{ item.to_amount_net || item.to_amount }}</td> |
||||
|
<td>{{ item.fee_amount || '--' }}</td> |
||||
|
<td>{{ item.created_at || item.datetime | parseTime }}</td> |
||||
|
<td class="text-right">{{ item.status_text || item.status }}</td> |
||||
|
</tr> |
||||
|
<tr v-if="!records.length"> |
||||
|
<td colspan="7" class="text-center text-muted py-4">{{ $t('common.notData') }}</td> |
||||
|
</tr> |
||||
|
</tbody> |
||||
|
</table> |
||||
|
|
||||
|
<div class="pagination-wrap" v-if="total > perPage"> |
||||
|
<el-pagination |
||||
|
background |
||||
|
layout="prev, pager, next" |
||||
|
:current-page.sync="page" |
||||
|
:page-size="perPage" |
||||
|
:total="total" |
||||
|
@current-change="loadHistory" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import FlashSwap from '@/api/flashSwap'; |
||||
|
|
||||
|
export default { |
||||
|
data() { |
||||
|
return { |
||||
|
records: [], |
||||
|
page: 1, |
||||
|
perPage: 20, |
||||
|
total: 0 |
||||
|
}; |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
loadHistory() { |
||||
|
FlashSwap.getHistory({ |
||||
|
page: this.page, |
||||
|
per_page: this.perPage |
||||
|
}).then(data => { |
||||
|
this.records = data.list || data.data || data.records || []; |
||||
|
this.total = data.total || data.count || this.records.length; |
||||
|
}).catch(() => {}); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
created() { |
||||
|
if (!localStorage.token) { |
||||
|
this.$router.push('/sign-in'); |
||||
|
return; |
||||
|
} |
||||
|
this.loadHistory(); |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.flash-swap-history-page { |
||||
|
padding: 40px 15px 60px; |
||||
|
} |
||||
|
|
||||
|
.page-title-row { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 16px; |
||||
|
margin-bottom: 24px; |
||||
|
|
||||
|
h3 { |
||||
|
margin: 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.back-link { |
||||
|
color: #007bff; |
||||
|
font-size: 14px; |
||||
|
} |
||||
|
|
||||
|
.pagination-wrap { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
margin-top: 20px; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,512 @@ |
|||||
|
<template> |
||||
|
<div class="flash-swap-page"> |
||||
|
<div class="container flash-swap-container"> |
||||
|
<div class="page-header"> |
||||
|
<router-link class="history-link" to="/flash-swap/history">{{ $t('flashSwap.history') }}</router-link> |
||||
|
</div> |
||||
|
|
||||
|
<div class="swap-card from-card"> |
||||
|
<div class="card-top"> |
||||
|
<span class="card-label">{{ $t('flashSwap.from') }}</span> |
||||
|
<div class="balance-row"> |
||||
|
<span class="balance-text"> |
||||
|
{{ $t('flashSwap.available') }}: |
||||
|
<span class="balance-value">{{ formatAmount(fromBalance, fromCoin) }}</span> |
||||
|
{{ fromCoinName }} |
||||
|
</span> |
||||
|
<button type="button" class="btn-max" @click="setMaxAmount">{{ $t('flashSwap.max') }}</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="card-bottom"> |
||||
|
<el-dropdown trigger="click" @command="selectFromCoin"> |
||||
|
<div class="coin-selector"> |
||||
|
<img v-if="fromCoinIcon" :src="fromCoinIcon" alt="" class="coin-icon" /> |
||||
|
<span class="coin-name">{{ fromCoinName || $t('flashSwap.selectCoin') }}</span> |
||||
|
<i class="el-icon-arrow-down"></i> |
||||
|
</div> |
||||
|
<el-dropdown-menu slot="dropdown"> |
||||
|
<el-dropdown-item |
||||
|
v-for="item in enabledCoins" |
||||
|
:key="item.coin_id" |
||||
|
:command="item.coin_name" |
||||
|
:disabled="item.coin_name === toCoinName" |
||||
|
> |
||||
|
<img :src="item.coin_icon" alt="" class="dropdown-coin-icon" /> |
||||
|
{{ item.coin_name }} |
||||
|
</el-dropdown-item> |
||||
|
</el-dropdown-menu> |
||||
|
</el-dropdown> |
||||
|
<input |
||||
|
v-model="fromAmount" |
||||
|
type="number" |
||||
|
class="amount-input" |
||||
|
:placeholder="$t('flashSwap.enterAmount')" |
||||
|
min="0" |
||||
|
step="any" |
||||
|
@input="clearPreview" |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="swap-toggle-wrap"> |
||||
|
<button type="button" class="swap-toggle" @click="swapCoins"> |
||||
|
<i class="ion-ios-swap"></i> |
||||
|
</button> |
||||
|
</div> |
||||
|
|
||||
|
<div class="swap-card to-card"> |
||||
|
<div class="card-bottom"> |
||||
|
<el-dropdown trigger="click" @command="selectToCoin"> |
||||
|
<div class="coin-selector"> |
||||
|
<img v-if="toCoinIcon" :src="toCoinIcon" alt="" class="coin-icon" /> |
||||
|
<span class="coin-name">{{ toCoinName || $t('flashSwap.selectCoin') }}</span> |
||||
|
<i class="el-icon-arrow-down"></i> |
||||
|
</div> |
||||
|
<el-dropdown-menu slot="dropdown"> |
||||
|
<el-dropdown-item |
||||
|
v-for="item in enabledCoins" |
||||
|
:key="item.coin_id" |
||||
|
:command="item.coin_name" |
||||
|
:disabled="item.coin_name === fromCoinName" |
||||
|
> |
||||
|
<img :src="item.coin_icon" alt="" class="dropdown-coin-icon" /> |
||||
|
{{ item.coin_name }} |
||||
|
</el-dropdown-item> |
||||
|
</el-dropdown-menu> |
||||
|
</el-dropdown> |
||||
|
<input |
||||
|
v-model="toAmount" |
||||
|
type="text" |
||||
|
class="amount-input" |
||||
|
:placeholder="$t('flashSwap.enterAmount')" |
||||
|
readonly |
||||
|
/> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="rate-info" v-if="fromCoinName && toCoinName"> |
||||
|
{{ $t('flashSwap.rate') }}: 1 {{ fromCoinName }} ≈ {{ rateDisplay }} {{ toCoinName }} |
||||
|
</div> |
||||
|
|
||||
|
<button |
||||
|
type="button" |
||||
|
class="btn-action" |
||||
|
:disabled="submitting" |
||||
|
@click="handleAction" |
||||
|
> |
||||
|
{{ actionLabel }} |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</template> |
||||
|
|
||||
|
<script> |
||||
|
import FlashSwap from '@/api/flashSwap'; |
||||
|
|
||||
|
export default { |
||||
|
data() { |
||||
|
return { |
||||
|
coins: [], |
||||
|
fee: null, |
||||
|
fromCoinName: '', |
||||
|
toCoinName: '', |
||||
|
fromAmount: '', |
||||
|
toAmount: '', |
||||
|
fromBalance: '0', |
||||
|
rate: '', |
||||
|
previewToken: '', |
||||
|
previewData: null, |
||||
|
submitting: false |
||||
|
}; |
||||
|
}, |
||||
|
|
||||
|
computed: { |
||||
|
isLogin() { |
||||
|
return Boolean(localStorage.token); |
||||
|
}, |
||||
|
|
||||
|
enabledCoins() { |
||||
|
return this.coins.filter(item => item.swap_enabled == 1); |
||||
|
}, |
||||
|
|
||||
|
fromCoin() { |
||||
|
return this.coins.find(item => item.coin_name === this.fromCoinName) || {}; |
||||
|
}, |
||||
|
|
||||
|
toCoin() { |
||||
|
return this.coins.find(item => item.coin_name === this.toCoinName) || {}; |
||||
|
}, |
||||
|
|
||||
|
fromCoinIcon() { |
||||
|
return this.fromCoin.coin_icon || ''; |
||||
|
}, |
||||
|
|
||||
|
toCoinIcon() { |
||||
|
return this.toCoin.coin_icon || ''; |
||||
|
}, |
||||
|
|
||||
|
rateDisplay() { |
||||
|
if (!this.rate) return '--'; |
||||
|
return this.formatAmount(this.rate, this.toCoin); |
||||
|
}, |
||||
|
|
||||
|
actionLabel() { |
||||
|
return this.previewToken |
||||
|
? this.$t('flashSwap.confirm') |
||||
|
: this.$t('flashSwap.query'); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
watch: { |
||||
|
fromCoinName() { |
||||
|
this.loadBalance(); |
||||
|
this.loadRate(); |
||||
|
this.clearPreview(); |
||||
|
}, |
||||
|
toCoinName() { |
||||
|
this.loadRate(); |
||||
|
this.clearPreview(); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
methods: { |
||||
|
formatAmount(value, coin = {}) { |
||||
|
const decimals = coin.qty_decimals != null ? coin.qty_decimals : 8; |
||||
|
const num = parseFloat(value); |
||||
|
if (Number.isNaN(num)) return '0'; |
||||
|
return num.toFixed(decimals).replace(/\.?0+$/, ''); |
||||
|
}, |
||||
|
|
||||
|
getCoinByName(name) { |
||||
|
return this.coins.find(item => item.coin_name === name) || {}; |
||||
|
}, |
||||
|
|
||||
|
clearPreview() { |
||||
|
this.previewToken = ''; |
||||
|
this.previewData = null; |
||||
|
this.toAmount = ''; |
||||
|
}, |
||||
|
|
||||
|
selectFromCoin(coinName) { |
||||
|
if (coinName === this.toCoinName) return; |
||||
|
this.fromCoinName = coinName; |
||||
|
}, |
||||
|
|
||||
|
selectToCoin(coinName) { |
||||
|
if (coinName === this.fromCoinName) return; |
||||
|
this.toCoinName = coinName; |
||||
|
}, |
||||
|
|
||||
|
swapCoins() { |
||||
|
const from = this.fromCoinName; |
||||
|
const to = this.toCoinName; |
||||
|
const amount = this.fromAmount; |
||||
|
this.fromCoinName = to; |
||||
|
this.toCoinName = from; |
||||
|
this.fromAmount = this.toAmount || ''; |
||||
|
this.clearPreview(); |
||||
|
if (amount) { |
||||
|
this.fromAmount = amount; |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
setMaxAmount() { |
||||
|
if (!this.fromBalance) return; |
||||
|
this.fromAmount = this.fromBalance; |
||||
|
this.clearPreview(); |
||||
|
}, |
||||
|
|
||||
|
ensureLogin() { |
||||
|
if (this.isLogin) return true; |
||||
|
this.$router.push('/sign-in'); |
||||
|
return false; |
||||
|
}, |
||||
|
|
||||
|
loadCoinList() { |
||||
|
FlashSwap.getCoinList() |
||||
|
.then(data => { |
||||
|
this.coins = data.coins || []; |
||||
|
this.fee = data.fee || null; |
||||
|
if (!this.fromCoinName && this.enabledCoins.length) { |
||||
|
this.fromCoinName = this.enabledCoins[0].coin_name; |
||||
|
} |
||||
|
if (!this.toCoinName && this.enabledCoins.length > 1) { |
||||
|
const target = this.enabledCoins.find(item => item.coin_name !== this.fromCoinName); |
||||
|
this.toCoinName = target ? target.coin_name : this.enabledCoins[1].coin_name; |
||||
|
} |
||||
|
this.loadRate(); |
||||
|
if (this.isLogin) { |
||||
|
this.loadBalance(); |
||||
|
} |
||||
|
}) |
||||
|
.catch(() => {}); |
||||
|
}, |
||||
|
|
||||
|
loadBalance() { |
||||
|
if (!this.isLogin || !this.fromCoinName) return; |
||||
|
FlashSwap.getUserCoinBalance(this.fromCoinName) |
||||
|
.then(data => { |
||||
|
this.fromBalance = data.usable_balance |
||||
|
|| data.balance |
||||
|
|| data.amount |
||||
|
|| '0'; |
||||
|
}) |
||||
|
.catch(() => { |
||||
|
this.fromBalance = '0'; |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
loadRate() { |
||||
|
if (!this.fromCoinName || !this.toCoinName || this.fromCoinName === this.toCoinName) { |
||||
|
this.rate = ''; |
||||
|
return; |
||||
|
} |
||||
|
FlashSwap.getRate(this.fromCoinName, this.toCoinName) |
||||
|
.then(data => { |
||||
|
this.rate = data.rate |
||||
|
|| data.cross_rate |
||||
|
|| data.to_amount |
||||
|
|| ''; |
||||
|
}) |
||||
|
.catch(() => { |
||||
|
this.rate = ''; |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
handleAction() { |
||||
|
if (this.previewToken) { |
||||
|
this.executeSwap(); |
||||
|
} else { |
||||
|
this.previewSwap(); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
previewSwap() { |
||||
|
if (!this.ensureLogin()) return; |
||||
|
if (!this.fromCoinName || !this.toCoinName) { |
||||
|
this.$message.error(this.$t('flashSwap.selectCoin')); |
||||
|
return; |
||||
|
} |
||||
|
if (this.fromCoinName === this.toCoinName) { |
||||
|
this.$message.error(this.$t('flashSwap.sameCoin')); |
||||
|
return; |
||||
|
} |
||||
|
if (!this.fromAmount || parseFloat(this.fromAmount) <= 0) { |
||||
|
this.$message.error(this.$t('flashSwap.enterAmount')); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.submitting = true; |
||||
|
FlashSwap.preview({ |
||||
|
from_coin: this.fromCoinName, |
||||
|
to_coin: this.toCoinName, |
||||
|
from_amount: String(this.fromAmount) |
||||
|
}) |
||||
|
.then(data => { |
||||
|
this.previewToken = data.preview_token; |
||||
|
this.previewData = data; |
||||
|
this.toAmount = data.to_amount_net || data.to_amount || ''; |
||||
|
}) |
||||
|
.catch(() => {}) |
||||
|
.finally(() => { |
||||
|
this.submitting = false; |
||||
|
}); |
||||
|
}, |
||||
|
|
||||
|
executeSwap() { |
||||
|
if (!this.ensureLogin() || !this.previewToken) return; |
||||
|
|
||||
|
this.$confirm(this.$t('flashSwap.confirmTip'), this.$t('flashSwap.confirm'), { |
||||
|
confirmButtonText: this.$t('flashSwap.confirm'), |
||||
|
cancelButtonText: this.$t('flashSwap.cancel'), |
||||
|
type: 'warning' |
||||
|
}).then(() => { |
||||
|
this.submitting = true; |
||||
|
return FlashSwap.execute({ preview_token: this.previewToken }); |
||||
|
}).then(() => { |
||||
|
this.$message.success(this.$t('flashSwap.success')); |
||||
|
this.fromAmount = ''; |
||||
|
this.clearPreview(); |
||||
|
this.loadBalance(); |
||||
|
}).catch(err => { |
||||
|
if (err !== 'cancel') { |
||||
|
this.clearPreview(); |
||||
|
} |
||||
|
}).finally(() => { |
||||
|
this.submitting = false; |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
created() { |
||||
|
this.loadCoinList(); |
||||
|
} |
||||
|
}; |
||||
|
</script> |
||||
|
|
||||
|
<style lang="scss" scoped> |
||||
|
.flash-swap-page { |
||||
|
min-height: calc(100vh - 120px); |
||||
|
padding: 40px 15px 60px; |
||||
|
} |
||||
|
|
||||
|
.flash-swap-container { |
||||
|
max-width: 520px; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
|
||||
|
.page-header { |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
margin-bottom: 24px; |
||||
|
} |
||||
|
|
||||
|
.history-link { |
||||
|
color: #007bff; |
||||
|
font-size: 14px; |
||||
|
text-decoration: none; |
||||
|
|
||||
|
&:hover { |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.swap-card { |
||||
|
background: #f5f5f5; |
||||
|
border-radius: 12px; |
||||
|
padding: 16px 18px; |
||||
|
} |
||||
|
|
||||
|
.card-top { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
margin-bottom: 18px; |
||||
|
} |
||||
|
|
||||
|
.card-label { |
||||
|
color: #999; |
||||
|
font-size: 13px; |
||||
|
} |
||||
|
|
||||
|
.balance-row { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
|
||||
|
.balance-text { |
||||
|
color: #666; |
||||
|
font-size: 13px; |
||||
|
} |
||||
|
|
||||
|
.balance-value { |
||||
|
color: #007bff; |
||||
|
} |
||||
|
|
||||
|
.btn-max { |
||||
|
border: none; |
||||
|
background: #111; |
||||
|
color: #fff; |
||||
|
border-radius: 999px; |
||||
|
padding: 4px 14px; |
||||
|
font-size: 12px; |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
|
||||
|
.card-bottom { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: space-between; |
||||
|
gap: 16px; |
||||
|
} |
||||
|
|
||||
|
.coin-selector { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: 8px; |
||||
|
cursor: pointer; |
||||
|
min-width: 120px; |
||||
|
} |
||||
|
|
||||
|
.coin-icon, |
||||
|
.dropdown-coin-icon { |
||||
|
width: 28px; |
||||
|
height: 28px; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
|
||||
|
.dropdown-coin-icon { |
||||
|
width: 20px; |
||||
|
height: 20px; |
||||
|
margin-right: 6px; |
||||
|
vertical-align: middle; |
||||
|
} |
||||
|
|
||||
|
.coin-name { |
||||
|
font-size: 18px; |
||||
|
font-weight: 600; |
||||
|
} |
||||
|
|
||||
|
.amount-input { |
||||
|
flex: 1; |
||||
|
border: none; |
||||
|
background: transparent; |
||||
|
text-align: right; |
||||
|
font-size: 16px; |
||||
|
outline: none; |
||||
|
min-width: 0; |
||||
|
|
||||
|
&::placeholder { |
||||
|
color: #bbb; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.swap-toggle-wrap { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
margin: -14px 0; |
||||
|
position: relative; |
||||
|
z-index: 1; |
||||
|
} |
||||
|
|
||||
|
.swap-toggle { |
||||
|
width: 42px; |
||||
|
height: 42px; |
||||
|
border-radius: 50%; |
||||
|
border: 4px solid #fff; |
||||
|
background: #f5f5f5; |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
cursor: pointer; |
||||
|
font-size: 22px; |
||||
|
color: #111; |
||||
|
} |
||||
|
|
||||
|
.to-card { |
||||
|
margin-top: 0; |
||||
|
} |
||||
|
|
||||
|
.rate-info { |
||||
|
margin: 18px 0 24px; |
||||
|
color: #999; |
||||
|
font-size: 13px; |
||||
|
} |
||||
|
|
||||
|
.btn-action { |
||||
|
width: 100%; |
||||
|
border: none; |
||||
|
background: #111; |
||||
|
color: #fff; |
||||
|
border-radius: 8px; |
||||
|
padding: 14px 16px; |
||||
|
font-size: 16px; |
||||
|
cursor: pointer; |
||||
|
|
||||
|
&:disabled { |
||||
|
opacity: 0.6; |
||||
|
cursor: not-allowed; |
||||
|
} |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue