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