584 lines
14 KiB
Vue
584 lines
14 KiB
Vue
<script lang='ts' setup>
|
||
import type { Treaty } from "@elysiajs/eden";
|
||
import type { client } from "@/api";
|
||
import { alertController, toastController } from "@ionic/vue";
|
||
import { checkmarkCircleOutline, documentTextOutline, keyOutline, swapHorizontalOutline, walletOutline } from "ionicons/icons";
|
||
import { safeClient } from "@/api";
|
||
|
||
type Wallet = Treaty.Data<typeof client.api.wallet.wallets.get>[number];
|
||
|
||
const router = useRouter();
|
||
const walletStore = useWalletStore();
|
||
const { wallets } = storeToRefs(walletStore);
|
||
const filterWallets = computed(() => wallets.value.filter(w => w.walletType.allowExchange === true));
|
||
|
||
// 兑换金额
|
||
const exchangeAmount = ref("");
|
||
const quickAmounts = [100, 500, 1000, 2000];
|
||
|
||
// 转出钱包
|
||
const fromWallet = ref<Wallet | null>(null);
|
||
|
||
// 转入钱包
|
||
const toWallet = ref<Wallet | null>(null);
|
||
|
||
// 交易密码
|
||
const transactionPassword = ref("");
|
||
|
||
onMounted(async () => {
|
||
await walletStore.syncWallets();
|
||
if (filterWallets.value.length > 0) {
|
||
fromWallet.value = filterWallets.value[0];
|
||
if (filterWallets.value.length > 1) {
|
||
toWallet.value = filterWallets.value[1];
|
||
}
|
||
}
|
||
});
|
||
|
||
// 可兑换余额
|
||
const availableBalance = computed(() => {
|
||
return Number(fromWallet.value?.available) || 0;
|
||
});
|
||
|
||
// 可选择的转入钱包(排除转出钱包)
|
||
const toWalletOptions = computed(() => {
|
||
if (!fromWallet.value) {
|
||
return filterWallets.value;
|
||
}
|
||
return filterWallets.value.filter(w => w.id !== fromWallet.value?.id);
|
||
});
|
||
|
||
function selectQuickAmount(amount: number) {
|
||
if (amount <= availableBalance.value) {
|
||
exchangeAmount.value = amount.toString();
|
||
}
|
||
else {
|
||
showToast("兑换金额不能大于可用余额", "warning");
|
||
}
|
||
}
|
||
|
||
function selectAllAmount() {
|
||
exchangeAmount.value = availableBalance.value.toString();
|
||
}
|
||
|
||
function selectFromWallet(wallet: Wallet) {
|
||
fromWallet.value = wallet;
|
||
// 如果转入钱包和转出钱包相同,自动切换转入钱包
|
||
if (toWallet.value?.id === wallet.id) {
|
||
const otherWallets = filterWallets.value.filter(w => w.id !== wallet.id);
|
||
toWallet.value = otherWallets.length > 0 ? otherWallets[0] : null;
|
||
}
|
||
// 重新验证兑换金额
|
||
const amount = Number.parseFloat(exchangeAmount.value);
|
||
if (amount > availableBalance.value) {
|
||
exchangeAmount.value = "";
|
||
}
|
||
}
|
||
|
||
function selectToWallet(wallet: Wallet) {
|
||
toWallet.value = wallet;
|
||
}
|
||
|
||
function swapWallets() {
|
||
if (fromWallet.value && toWallet.value) {
|
||
const temp = fromWallet.value;
|
||
fromWallet.value = toWallet.value;
|
||
toWallet.value = temp;
|
||
// 重新验证兑换金额
|
||
const amount = Number.parseFloat(exchangeAmount.value);
|
||
if (amount > availableBalance.value) {
|
||
exchangeAmount.value = "";
|
||
}
|
||
}
|
||
}
|
||
|
||
async function showToast(message: string, color: "success" | "danger" | "warning" = "success") {
|
||
const toast = await toastController.create({
|
||
message,
|
||
duration: 2000,
|
||
position: "top",
|
||
color,
|
||
});
|
||
await toast.present();
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
const amount = Number.parseFloat(exchangeAmount.value);
|
||
|
||
// 验证兑换金额
|
||
if (!exchangeAmount.value || Number.isNaN(amount) || amount <= 0) {
|
||
await showToast("请输入有效的兑换金额", "warning");
|
||
return;
|
||
}
|
||
|
||
if (amount < 1) {
|
||
await showToast("兑换金额不能低于1元", "warning");
|
||
return;
|
||
}
|
||
|
||
if (amount > availableBalance.value) {
|
||
await showToast("兑换金额不能大于可用余额", "warning");
|
||
return;
|
||
}
|
||
|
||
// 验证转出钱包选择
|
||
if (!fromWallet.value) {
|
||
await showToast("请选择转出钱包", "warning");
|
||
return;
|
||
}
|
||
|
||
// 验证转入钱包选择
|
||
if (!toWallet.value) {
|
||
await showToast("请选择转入钱包", "warning");
|
||
return;
|
||
}
|
||
|
||
if (fromWallet.value.id === toWallet.value.id) {
|
||
await showToast("转出和转入钱包不能相同", "warning");
|
||
return;
|
||
}
|
||
|
||
// 验证交易密码
|
||
if (!transactionPassword.value) {
|
||
await showToast("请输入交易密码", "warning");
|
||
return;
|
||
}
|
||
|
||
if (transactionPassword.value.length < 6) {
|
||
await showToast("交易密码至少6位", "warning");
|
||
return;
|
||
}
|
||
|
||
const alert = await alertController.create({
|
||
header: "确认兑换",
|
||
message: `
|
||
<div style="text-align: left; padding: 12px 0;">
|
||
<p style="margin: 8px 0;"><strong>兑换金额:</strong>¥${amount.toFixed(2)}</p>
|
||
<p style="margin: 8px 0;"><strong>转出钱包:</strong>${fromWallet.value.walletType.name}</p>
|
||
<p style="margin: 8px 0;"><strong>转入钱包:</strong>${toWallet.value.walletType.name}</p>
|
||
</div>
|
||
`,
|
||
buttons: [
|
||
{
|
||
text: "取消",
|
||
role: "cancel",
|
||
},
|
||
{
|
||
text: "确认兑换",
|
||
handler: async () => {
|
||
// TODO: 调用兑换 API
|
||
// const { error } = await safeClient(client.api.exchange.post({
|
||
// fromWalletId: fromWallet.value.id,
|
||
// toWalletId: toWallet.value.id,
|
||
// amount,
|
||
// transactionPassword: transactionPassword.value,
|
||
// }));
|
||
|
||
await showToast("兑换成功");
|
||
exchangeAmount.value = "";
|
||
transactionPassword.value = "";
|
||
await walletStore.syncWallets();
|
||
},
|
||
},
|
||
],
|
||
});
|
||
|
||
await alert.present();
|
||
}
|
||
|
||
function goToRecords() {
|
||
// router.push("/exchange/records");
|
||
showToast("功能开发中", "warning");
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<ion-page>
|
||
<ion-header class="ion-no-border">
|
||
<ion-toolbar class="ion-toolbar">
|
||
<ion-buttons slot="start">
|
||
<back-button />
|
||
</ion-buttons>
|
||
<ion-title>兑换</ion-title>
|
||
<ion-buttons slot="end">
|
||
<ion-button color="light" size="small" @click="goToRecords">
|
||
<ion-icon slot="icon-only" :icon="documentTextOutline" />
|
||
</ion-button>
|
||
</ion-buttons>
|
||
</ion-toolbar>
|
||
</ion-header>
|
||
|
||
<ion-content>
|
||
<!-- 转出钱包 -->
|
||
<div class="section-card">
|
||
<div class="section-title">
|
||
<ion-icon :icon="walletOutline" class="title-icon" />
|
||
转出钱包
|
||
</div>
|
||
<div v-if="filterWallets.length > 0" class="wallet-list">
|
||
<div
|
||
v-for="wallet in filterWallets"
|
||
:key="wallet.id"
|
||
class="wallet-item"
|
||
:class="{ active: fromWallet?.id === wallet.id }"
|
||
@click="selectFromWallet(wallet)"
|
||
>
|
||
<div class="wallet-info">
|
||
<div class="wallet-name">
|
||
{{ wallet.walletType.name }}
|
||
</div>
|
||
<div class="wallet-balance">
|
||
可用余额:¥{{ Number(wallet.available).toFixed(2) }}
|
||
</div>
|
||
</div>
|
||
<div v-if="fromWallet?.id === wallet.id" class="wallet-check">
|
||
<ion-icon :icon="checkmarkCircleOutline" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="no-wallets">
|
||
<empty title="暂无可兑换钱包" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 兑换金额输入 -->
|
||
<div class="section-card">
|
||
<div class="section-title">
|
||
兑换金额
|
||
</div>
|
||
<div class="amount-input-wrapper">
|
||
<span class="currency-symbol">¥</span>
|
||
<input
|
||
v-model="exchangeAmount"
|
||
type="number"
|
||
inputmode="decimal"
|
||
placeholder="请输入兑换金额"
|
||
class="amount-input"
|
||
:max="availableBalance"
|
||
>
|
||
</div>
|
||
|
||
<!-- 快速选择金额 -->
|
||
<div class="quick-amounts">
|
||
<div
|
||
v-for="amount in quickAmounts"
|
||
:key="amount"
|
||
class="quick-amount-btn"
|
||
:class="{
|
||
active: exchangeAmount === amount.toString(),
|
||
disabled: amount > availableBalance,
|
||
}"
|
||
@click="selectQuickAmount(amount)"
|
||
>
|
||
¥{{ amount }}
|
||
</div>
|
||
<div
|
||
class="quick-amount-btn"
|
||
:class="{ active: exchangeAmount === availableBalance.toString() }"
|
||
@click="selectAllAmount"
|
||
>
|
||
全部
|
||
</div>
|
||
</div>
|
||
|
||
<div class="amount-hint">
|
||
<ion-icon :icon="checkmarkCircleOutline" class="hint-icon" />
|
||
可兑换余额:¥{{ availableBalance.toFixed(2) }}(最低兑换金额 ¥1)
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 交易密码 -->
|
||
<div class="section-card">
|
||
<div class="section-title">
|
||
<ion-icon :icon="keyOutline" class="title-icon" />
|
||
交易密码
|
||
</div>
|
||
<div class="password-input-wrapper">
|
||
<ion-input
|
||
v-model="transactionPassword"
|
||
type="password"
|
||
placeholder="请输入交易密码"
|
||
class="password-input"
|
||
inputmode="numeric"
|
||
:maxlength="20"
|
||
/>
|
||
</div>
|
||
<div class="password-hint">
|
||
交易密码用于验证兑换操作,请妥善保管
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 提交按钮 -->
|
||
<div class="submit-wrapper">
|
||
<ion-button
|
||
expand="block"
|
||
class="submit-btn"
|
||
@click="handleSubmit"
|
||
>
|
||
确认兑换
|
||
</ion-button>
|
||
</div>
|
||
|
||
<!-- 温馨提示 -->
|
||
<div class="notice-card">
|
||
<div class="notice-title">
|
||
温馨提示
|
||
</div>
|
||
<div class="notice-content">
|
||
<p>1. 兑换将实时完成,请确认兑换信息无误</p>
|
||
<p>2. 兑换金额最低1元,请确保账户余额充足</p>
|
||
<p>3. 兑换完成后资金将在对应钱包中显示</p>
|
||
<p>4. 兑换记录可在资产明细中查看</p>
|
||
<p>5. 如遇问题,请及时联系客服处理</p>
|
||
</div>
|
||
</div>
|
||
</ion-content>
|
||
</ion-page>
|
||
</template>
|
||
|
||
<style lang='css' scoped>
|
||
.section-card {
|
||
background: white;
|
||
padding: 20px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.title-icon {
|
||
font-size: 20px;
|
||
color: var(--ion-color-primary);
|
||
}
|
||
|
||
.wallet-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.wallet-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px;
|
||
background: #f5f5f5;
|
||
border: 2px solid transparent;
|
||
border-radius: 12px;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.wallet-item.active {
|
||
background: #fff5f5;
|
||
border-color: var(--ion-color-primary);
|
||
}
|
||
|
||
.wallet-item:active {
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
.wallet-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.wallet-name {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.wallet-balance {
|
||
font-size: 13px;
|
||
color: #666;
|
||
}
|
||
|
||
.wallet-check {
|
||
color: var(--ion-color-primary);
|
||
font-size: 24px;
|
||
}
|
||
|
||
.amount-input-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
background: #f5f5f5;
|
||
border-radius: 12px;
|
||
padding: 16px 20px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.currency-symbol {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.amount-input {
|
||
flex: 1;
|
||
border: none;
|
||
background: transparent;
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
outline: none;
|
||
}
|
||
|
||
.amount-input::placeholder {
|
||
color: #bbb;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.quick-amounts {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.quick-amount-btn {
|
||
padding: 12px;
|
||
background: #f5f5f5;
|
||
border: 2px solid transparent;
|
||
border-radius: 8px;
|
||
text-align: center;
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #666;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.quick-amount-btn.active {
|
||
background: white;
|
||
border-color: var(--ion-color-primary);
|
||
color: var(--ion-color-primary);
|
||
}
|
||
|
||
.quick-amount-btn.disabled {
|
||
opacity: 0.4;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.quick-amount-btn:not(.disabled):active {
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
.amount-hint {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 12px;
|
||
color: #999;
|
||
}
|
||
|
||
.hint-icon {
|
||
font-size: 14px;
|
||
color: var(--ion-color-success);
|
||
}
|
||
|
||
.swap-button-wrapper {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 0 20px;
|
||
margin: -6px 0 12px 0;
|
||
}
|
||
|
||
.swap-button {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 24px;
|
||
background: white;
|
||
border: 2px solid var(--ion-color-primary);
|
||
border-radius: 20px;
|
||
color: var(--ion-color-primary);
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 2px 8px rgba(196, 30, 58, 0.15);
|
||
}
|
||
|
||
.swap-button:active {
|
||
transform: scale(0.95);
|
||
background: #fff5f5;
|
||
}
|
||
|
||
.swap-button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
}
|
||
|
||
.swap-icon {
|
||
font-size: 20px;
|
||
}
|
||
|
||
.password-input-wrapper {
|
||
background: #f5f5f5;
|
||
border-radius: 12px;
|
||
padding: 4px 12px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.password-input {
|
||
--padding-start: 8px;
|
||
--padding-end: 8px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.password-hint {
|
||
font-size: 12px;
|
||
color: #999;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.submit-wrapper {
|
||
padding: 0 20px 20px;
|
||
}
|
||
|
||
.submit-btn {
|
||
--background: linear-gradient(135deg, #c41e3a 0%, #8b1a2e 100%);
|
||
--background-activated: linear-gradient(135deg, #8b1a2e 0%, #c41e3a 100%);
|
||
--border-radius: 12px;
|
||
--padding-top: 14px;
|
||
--padding-bottom: 14px;
|
||
font-weight: 600;
|
||
font-size: 16px;
|
||
text-transform: none;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.notice-card {
|
||
background: #f9fafb;
|
||
padding: 16px 20px;
|
||
margin: 0 0 20px 0;
|
||
}
|
||
|
||
.notice-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.notice-content {
|
||
font-size: 12px;
|
||
color: #666;
|
||
line-height: 1.8;
|
||
}
|
||
|
||
.notice-content p {
|
||
margin: 6px 0;
|
||
}
|
||
</style>
|