Browse Source

新增闪兑功能,包括路由配置、国际化支持及界面组件更新。

master
TorsenLi 2 days ago
parent
commit
262d2c5240
  1. 1
      doc
  2. 35
      src/api/flashSwap.js
  3. 7
      src/components/CommonHeader.vue
  4. 24
      src/i18n/cn.json
  5. 24
      src/i18n/en.json
  6. 24
      src/i18n/tw.json
  7. 12
      src/router/index.js
  8. 2
      src/router/wallet.js
  9. 114
      src/views/flashSwap/history.vue
  10. 512
      src/views/flashSwap/index.vue
  11. 137
      src/views/wallet/flashswap.vue

1
doc

@ -0,0 +1 @@
/Volumes/MacintoshHD2/www/wwfsv.muzkcoinsss.com/doc

35
src/api/flashSwap.js

@ -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;

7
src/components/CommonHeader.vue

@ -64,6 +64,9 @@
<!-- <li class="nav-item">--> <!-- <li class="nav-item">-->
<!-- <router-link class="nav-link" :to="college.url">{{college.label}}</router-link>--> <!-- <router-link class="nav-link" :to="college.url">{{college.label}}</router-link>-->
<!-- </li>--> <!-- </li>-->
<li class="nav-item">
<router-link class="nav-link" :to="flashSwap.url">{{ flashSwap.label }}</router-link>
</li>
<li class="nav-item"> <li class="nav-item">
<router-link class="nav-link" :to="contactUs.url">{{ contactUs.label }}</router-link> <router-link class="nav-link" :to="contactUs.url">{{ contactUs.label }}</router-link>
</li> </li>
@ -231,6 +234,10 @@ export default {
label: this.$t("common.markets"), label: this.$t("common.markets"),
url: "/market" url: "/market"
}, },
flashSwap: {
label: this.$t("common.flashswap"),
url: "/flash-swap"
},
contactUs: { contactUs: {
label: this.$t("common.contactUs"), label: this.$t("common.contactUs"),
url: "/contact-us" url: "/contact-us"

24
src/i18n/cn.json

@ -1056,5 +1056,29 @@
"IOSScanCodeDownload": "IOS扫码下载", "IOSScanCodeDownload": "IOS扫码下载",
"UseNow": "现在使用" "UseNow": "现在使用"
},
"flashSwap": {
"from": "从",
"to": "至",
"available": "可用",
"max": "最大",
"enterAmount": "请输入金额",
"selectCoin": "请选择币种",
"rate": "汇率",
"query": "查询",
"confirm": "确认兑换",
"confirmTip": "确认执行此次闪兑?",
"cancel": "取消",
"success": "兑换成功",
"history": "闪兑记录",
"back": "返回",
"fromCoin": "支付币种",
"toCoin": "获得币种",
"fromAmount": "支付数量",
"toAmount": "获得数量",
"fee": "手续费",
"time": "时间",
"status": "状态",
"sameCoin": "相同币种不可兑换"
} }
} }

24
src/i18n/en.json

@ -1296,5 +1296,29 @@
"a29":"Cancel", "a29":"Cancel",
"a30":"Confirm", "a30":"Confirm",
"a31":"Please Enter Amount" "a31":"Please Enter Amount"
},
"flashSwap": {
"from": "From",
"to": "To",
"available": "Available",
"max": "Max",
"enterAmount": "Please enter amount",
"selectCoin": "Select coin",
"rate": "Rate",
"query": "Query",
"confirm": "Confirm Swap",
"confirmTip": "Confirm this flash swap?",
"cancel": "Cancel",
"success": "Swap successful",
"history": "Swap History",
"back": "Back",
"fromCoin": "From",
"toCoin": "To",
"fromAmount": "Pay Amount",
"toAmount": "Receive Amount",
"fee": "Fee",
"time": "Time",
"status": "Status",
"sameCoin": "Same coin cannot be swapped"
} }
} }

24
src/i18n/tw.json

@ -1226,5 +1226,29 @@
"a29":"取消", "a29":"取消",
"a30":"確認", "a30":"確認",
"a31":"請輸入金額" "a31":"請輸入金額"
},
"flashSwap": {
"from": "從",
"to": "至",
"available": "可用",
"max": "最大",
"enterAmount": "請輸入金額",
"selectCoin": "請選擇幣種",
"rate": "匯率",
"query": "查詢",
"confirm": "確認兌換",
"confirmTip": "確認執行此次閃兌?",
"cancel": "取消",
"success": "兌換成功",
"history": "閃兌記錄",
"back": "返回",
"fromCoin": "支付幣種",
"toCoin": "獲得幣種",
"fromAmount": "支付數量",
"toAmount": "獲得數量",
"fee": "手續費",
"time": "時間",
"status": "狀態",
"sameCoin": "相同幣種不可兌換"
} }
} }

12
src/router/index.js

@ -48,6 +48,8 @@ import MarketTrends from '@/views/mineview/MarketTrends'
import ExchangeStock from '@/views/exchangeStock' import ExchangeStock from '@/views/exchangeStock'
import EllasAlpha from '@/views/ellasAlpha' import EllasAlpha from '@/views/ellasAlpha'
import FlashSwap from '@/views/flashSwap/index'
import FlashSwapHistory from '@/views/flashSwap/history'
Vue.use(VueRouter); Vue.use(VueRouter);
@ -169,6 +171,16 @@ const routes = [{
name: "ellasAlpha", name: "ellasAlpha",
path: "/ellasAlpha", path: "/ellasAlpha",
component: EllasAlpha component: EllasAlpha
},
{
name: "flashSwap",
path: "/flash-swap",
component: FlashSwap
},
{
name: "flashSwapHistory",
path: "/flash-swap/history",
component: FlashSwapHistory
} }
] ]

2
src/router/wallet.js

@ -7,7 +7,7 @@ import Reward from '@/views/wallet/reward';
import Funding from '@/views/wallet/funding'; import Funding from '@/views/wallet/funding';
import Address from '@/views/wallet/address'; import Address from '@/views/wallet/address';
import Otc from '@/views/wallet/otc-assets'; import Otc from '@/views/wallet/otc-assets';
import FlashSwap from '@/views/wallet/flashswap'; import FlashSwap from '@/views/flashSwap/index';
export default [ export default [
{ {

114
src/views/flashSwap/history.vue

@ -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">&lt; {{ $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>

512
src/views/flashSwap/index.vue

@ -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>

137
src/views/wallet/flashswap.vue

@ -1,142 +1,13 @@
<template> <template>
<div class="wallet-page"> <flash-swap />
<secondary-nav></secondary-nav>
<div class="container">
<div class="w-100 d-flex justify-content-between align-items-center mb-3 mt-4">
<div class="h3">{{$t('common.flashswap')}}</div>
</div>
<div class="row">
<div class="col-md-5">
<inner-transfer :coinName.sync="coinName" :showHistoryLink="false" :isRestCoin="true"></inner-transfer>
</div>
<div class="col-md-7">
<div class="panel-box bg-panel">
<div class="heading d-flex justify-content-between align-items-center">
<div class="h6">{{$t('common.illustrate')}}</div>
</div>
<div class="tab-content">
<ul>
<li class="mb-2">1.{{$t('wallet.handlingFee')}}</li>
<!-- <li class="mb-2">2.{{$t('wallet.onlyForeignExchange',{name:'USDT'})}}</li>
<li class="mb-2">3.{{$t('wallet.onlySustainable',{name:'BTC'})}}</li>
<li class="mb-2">4.{{$t('wallet.inCase1',{name:'USDT'})}}</li> -->
<li class="mb-2">2.{{$t('wallet.inCase2',{name:'USDT'})}}</li>
</ul>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12 mt15">
<div class="markets-pair-list">
<ul class="nav nav-pills" id="pills-tab" role="tablist">
<li class="nav-item" v-if="coinName">
<a class="nav-link active" data-toggle="pill" href @click.prevent="isCondition=true" role="tab" aria-selected="false">{{coinName}} {{$("transfer.transferRecord")}}</a>
</li>
<li class="nav-item">
<a class="nav-link" data-toggle="pill" href @click.prevent="isCondition=false" role="tab" aria-selected="false">{{ $t("wallet.allTransfer") }}</a>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="tab1" role="tabpanel">
<table class="table stripe">
<thead>
<tr>
<th>{{$t('wallet.assets')}}</th>
<th>{{$t('wallet.transferQuantity')}}</th>
<th>{{$t('wallet.direction')}}</th>
<th>{{$t('wallet.date')}}</th>
<th class="text-right">{{$t('wallet.status')}}</th>
</tr>
</thead>
<tbody>
<tr v-for="item in transferRecords" :key="item.id" v-if="seek(item)">
<td>
<img src="../../assets/img/icon/1.png" alt="eth" />
{{ item.coin_name }}
</td>
<td>{{ item.amount }}</td>
<td>{{ $t(`wallet.${item.draw_out_direction}`) }} -> {{ $t(`wallet.${item.into_direction}`) }}</td>
<td>{{ item.datetime|parseTime }}</td>
<td class="text-right">
<!--1 成功 2 失败 -->
<span class="fail" v-if="item.status == Globals.Status.Transfer.FAIL">{{$t('wallet.error')}}</span>
<span class="success" v-else-if="item.status == Globals.Status.Transfer.SUCCESS">{{$t('wallet.success')}}</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template> </template>
<script> <script>
import InnerTransfer from "../components/inner-transfer.vue"; import FlashSwap from '@/views/flashSwap/index.vue';
import Wallet from "@/api/wallet";
export default { export default {
components: { components: {
InnerTransfer FlashSwap
},
data() {
return {
transferRecords: [],
isCondition: false, // tab
coinName: '',
};
},
watch: {
coinName(val, oldVal) {
//
if (val && !oldVal) this.isCondition = true;
//
if (!val && oldVal) this.isCondition = false;
},
isCondition(val) {
// console.log(val)
}
},
computed: {
},
methods: {
//
seek(item) {
return !this.isCondition || item.coin_name === this.coinName;
},
getTransferRecords() {
Wallet.transferRecord()
.then(data => {
this.transferRecords = data.data;
})
.catch(err => {});
}
},
created() {
this.getTransferRecords();
} }
}; };
</script> </script>
<style lang="scss" scoped>
.table {
tr {
td {
cursor: default !important;
}
}
}
</style>

Loading…
Cancel
Save