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

688 lines
16 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 { alertController, toastController } from "@ionic/vue";
import { cardOutline, checkmarkCircleOutline, documentTextOutline, keyOutline, logoAlipay, walletOutline } from "ionicons/icons";
import { client, safeClient } from "@/api";
type Wallet = Treaty.Data<typeof client.api.wallet.wallets.get>[number];
type Receipt = Treaty.Data<typeof client.api.receipt_method.get>["data"][number];
const router = useRouter();
const walletStore = useWalletStore();
const { wallets } = storeToRefs(walletStore);
const filterWallets = computed(() => wallets.value.filter(w => w.walletType.allowWithdraw === true));
const withdrawAmount = ref("");
const quickAmounts = [100, 500, 1000, 2000];
const selectedWallet = ref<Wallet | null>(null);
const transactionPassword = ref("");
const selectedBankAccount = ref<Receipt | null>(null);
const { data: bankAccounts } = await safeClient(client.api.receipt_method.get());
onMounted(async () => {
await walletStore.syncWallets();
if (wallets.value.length > 0) {
selectedWallet.value = wallets.value[0];
}
});
// 可提现余额
const availableBalance = computed(() => {
return Number(selectedWallet.value?.available) || 0;
});
function selectQuickAmount(amount: number) {
if (amount <= availableBalance.value) {
withdrawAmount.value = amount.toString();
}
else {
showToast("提现金额不能大于可用余额", "warning");
}
}
function selectAllAmount() {
withdrawAmount.value = availableBalance.value.toString();
}
function selectWallet(wallet: Wallet) {
selectedWallet.value = wallet;
// 重新验证提现金额
const amount = Number.parseFloat(withdrawAmount.value);
if (amount > availableBalance.value) {
withdrawAmount.value = "";
}
}
function selectBankAccount(account: Receipt) {
selectedBankAccount.value = account;
}
async function showToast(message: string, color: "success" | "danger" | "warning" = "success") {
const toast = await toastController.create({
message,
duration: 2000,
position: "top",
color,
});
await toast.present();
}
function getPaymentIcon(type: Receipt["type"]) {
return type === "bank_card" ? cardOutline : logoAlipay;
}
async function handleSubmit() {
const amount = Number.parseFloat(withdrawAmount.value);
// 验证提现金额
if (!withdrawAmount.value || Number.isNaN(amount) || amount <= 0) {
await showToast("请输入有效的提现金额", "warning");
return;
}
if (amount < 10) {
await showToast("提现金额不能低于10元", "warning");
return;
}
if (amount > availableBalance.value) {
await showToast("提现金额不能大于可用余额", "warning");
return;
}
// 验证钱包选择
if (!selectedWallet.value) {
await showToast("请选择提现钱包", "warning");
return;
}
// 验证收款方式
if (!selectedBankAccount.value) {
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>${selectedWallet.value.walletType.name}</p>
<p style="margin: 8px 0;"><strong>收款银行:</strong>${selectedBankAccount.value.bankName}</p>
<p style="margin: 8px 0;"><strong>收款账号:</strong>${selectedBankAccount.value.type === "bank_card" ? selectedBankAccount.value.bankCardNumber : selectedBankAccount.value.alipayAccount}</p>
</div>
`,
buttons: [
{
text: "取消",
role: "cancel",
},
{
text: "确认提现",
handler: async () => {
// TODO: 调用提现 API
// const { error } = await safeClient(client.api.withdraw.post({
// walletId: selectedWallet.value.id,
// amount,
// bankAccountId: selectedBankAccount.value.id,
// transactionPassword: transactionPassword.value,
// }));
await showToast("提现申请已提交,请等待审核");
withdrawAmount.value = "";
transactionPassword.value = "";
selectedBankAccount.value = null;
await walletStore.syncWallets();
},
},
],
});
await alert.present();
}
function getPaymentTypeName(type: Receipt["type"]) {
return type === "bank_card" ? "银行卡" : "支付宝";
}
function goToRecords() {
// router.push("/withdraw/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: selectedWallet?.id === wallet.id }"
@click="selectWallet(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="selectedWallet?.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="withdrawAmount"
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: withdrawAmount === amount.toString(),
disabled: amount > availableBalance,
}"
@click="selectQuickAmount(amount)"
>
¥{{ amount }}
</div>
<div
class="quick-amount-btn"
:class="{ active: withdrawAmount === availableBalance.toString() }"
@click="selectAllAmount"
>
全部
</div>
</div>
<div class="amount-hint">
<ion-icon :icon="checkmarkCircleOutline" class="hint-icon" />
可提现余额¥{{ availableBalance.toFixed(2) }}最低提现金额 ¥10
</div>
</div>
<!-- 收款方式选择 -->
<div class="section-card">
<div class="section-title">
<ion-icon :icon="cardOutline" class="title-icon" />
选择收款账户
</div>
<div v-if="bankAccounts?.data?.length === 0" class="empty-state">
<empty title="暂无收款账户">
<template #extra>
<ion-button size="small" @click="router.push('/bank_accounts/add')">
添加收款账户
</ion-button>
</template>
</empty>
</div>
<div v-else class="bank-accounts">
<div
v-for="payment in bankAccounts?.data"
:key="payment.id"
class="payment-card"
:class="{ active: selectedBankAccount?.id === payment.id }"
@click="selectBankAccount(payment)"
>
<!-- 支付方式图标 -->
<div class="payment-icon">
<ion-icon
:icon="getPaymentIcon(payment.type)"
:class="payment.type === 'alipay' ? 'text-[#1677ff]' : 'text-primary'"
/>
</div>
<!-- 收款方式信息 -->
<div class="payment-info">
<div class="payment-type-name">
{{ getPaymentTypeName(payment.type) }}
</div>
<div class="payment-details">
<template v-if="payment.type === 'bank_card'">
<div class="detail-item">
<span class="detail-label">开户行</span>
<span class="detail-value">{{ payment.bankBranchName }}</span>
</div>
<div class="detail-item">
<span class="detail-label">账号</span>
<span class="detail-value">{{ payment.bankCardNumber }}</span>
</div>
</template>
<template v-else>
<div class="detail-item">
<span class="detail-label">姓名</span>
<span class="detail-value">{{ payment.alipayName }}</span>
</div>
<div class="detail-item">
<span class="detail-label">账号</span>
<span class="detail-value">{{ payment.alipayAccount }}</span>
</div>
</template>
</div>
</div>
<!-- 选中图标 -->
<div v-if="selectedBankAccount?.id === payment.id" class="payment-check">
<ion-icon :icon="checkmarkCircleOutline" />
</div>
</div>
</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. 提现将在1-3个工作日内到账请耐心等待</p>
<p>2. 提现金额最低10元单笔最高50000元</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;
}
.no-wallets {
padding: 40px 0;
}
.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);
}
.bank-accounts {
display: flex;
flex-direction: column;
gap: 12px;
}
.payment-card {
display: flex;
align-items: center;
gap: 14px;
padding: 16px;
background: #f7f8fa;
border: 2px solid transparent;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.payment-card:active {
transform: scale(0.98);
}
.payment-card.active {
background: #fff5f5;
border-color: var(--ion-color-primary);
box-shadow: 0 2px 8px rgba(196, 30, 58, 0.15);
}
.payment-icon {
width: 52px;
height: 52px;
display: flex;
align-items: center;
justify-content: center;
background: white;
border-radius: 12px;
font-size: 28px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
flex-shrink: 0;
}
.payment-info {
flex: 1;
min-width: 0;
}
.payment-type-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.payment-details {
display: flex;
flex-direction: column;
gap: 4px;
}
.detail-item {
display: flex;
align-items: baseline;
font-size: 13px;
line-height: 1.5;
}
.detail-label {
color: #999;
flex-shrink: 0;
}
.detail-value {
color: #666;
font-weight: 500;
word-break: break-all;
}
.payment-check {
color: var(--ion-color-primary);
font-size: 26px;
flex-shrink: 0;
animation: checkmark-pop 0.3s ease;
}
@keyframes checkmark-pop {
0% {
transform: scale(0);
opacity: 0;
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
opacity: 1;
}
}
.no-accounts {
padding: 40px 0;
}
.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>