Files
financial/src/views/exchange/index.vue

584 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>