feat: 添加转账给用户功能,更新路由和二维码扫描成功处理
This commit is contained in:
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -20,6 +20,7 @@ declare module 'vue' {
|
|||||||
IIcRoundArrowForwardIos: typeof import('~icons/ic/round-arrow-forward-ios')['default']
|
IIcRoundArrowForwardIos: typeof import('~icons/ic/round-arrow-forward-ios')['default']
|
||||||
IonApp: typeof import('@ionic/vue')['IonApp']
|
IonApp: typeof import('@ionic/vue')['IonApp']
|
||||||
IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
||||||
|
IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
||||||
IonBadge: typeof import('@ionic/vue')['IonBadge']
|
IonBadge: typeof import('@ionic/vue')['IonBadge']
|
||||||
IonButton: typeof import('@ionic/vue')['IonButton']
|
IonButton: typeof import('@ionic/vue')['IonButton']
|
||||||
IonButtons: typeof import('@ionic/vue')['IonButtons']
|
IonButtons: typeof import('@ionic/vue')['IonButtons']
|
||||||
@@ -79,6 +80,7 @@ declare global {
|
|||||||
const IIcRoundArrowForwardIos: typeof import('~icons/ic/round-arrow-forward-ios')['default']
|
const IIcRoundArrowForwardIos: typeof import('~icons/ic/round-arrow-forward-ios')['default']
|
||||||
const IonApp: typeof import('@ionic/vue')['IonApp']
|
const IonApp: typeof import('@ionic/vue')['IonApp']
|
||||||
const IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
const IonAvatar: typeof import('@ionic/vue')['IonAvatar']
|
||||||
|
const IonBackButton: typeof import('@ionic/vue')['IonBackButton']
|
||||||
const IonBadge: typeof import('@ionic/vue')['IonBadge']
|
const IonBadge: typeof import('@ionic/vue')['IonBadge']
|
||||||
const IonButton: typeof import('@ionic/vue')['IonButton']
|
const IonButton: typeof import('@ionic/vue')['IonButton']
|
||||||
const IonButtons: typeof import('@ionic/vue')['IonButtons']
|
const IonButtons: typeof import('@ionic/vue')['IonButtons']
|
||||||
|
|||||||
@@ -95,6 +95,12 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
component: () => import("@/views/wallet/transfer.vue"),
|
component: () => import("@/views/wallet/transfer.vue"),
|
||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/transfer_to_user/:id",
|
||||||
|
props: true,
|
||||||
|
component: () => import("@/views/wallet/transfer-to-user.vue"),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/wallet/funding",
|
path: "/wallet/funding",
|
||||||
component: () => import("@/views/wallet/funding.vue"),
|
component: () => import("@/views/wallet/funding.vue"),
|
||||||
|
|||||||
@@ -4,13 +4,19 @@ import { toastController } from "@ionic/vue";
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const scanner = useTemplateRef<InstanceType<typeof QrScanner>>("scanner");
|
const scanner = useTemplateRef<InstanceType<typeof QrScanner>>("scanner");
|
||||||
|
|
||||||
function handleSuccess(value: string) {
|
async function handleSuccess(value: string) {
|
||||||
toastController.create({
|
const toast = await toastController.create({
|
||||||
message: String(value),
|
message: "扫描成功",
|
||||||
duration: 2000,
|
duration: 1000,
|
||||||
position: "bottom",
|
position: "bottom",
|
||||||
color: "success",
|
color: "success",
|
||||||
}).then(toast => toast.present());
|
});
|
||||||
|
await toast.present();
|
||||||
|
|
||||||
|
if (value.startsWith("/transfer_to_user/")) {
|
||||||
|
// 完整路径
|
||||||
|
router.replace(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleError(error: Error) {
|
function handleError(error: Error) {
|
||||||
|
|||||||
254
src/views/wallet/transfer-to-user.vue
Normal file
254
src/views/wallet/transfer-to-user.vue
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
<script lang='ts' setup>
|
||||||
|
import type { GenericObject } from "vee-validate";
|
||||||
|
import type { FormInstance } from "@/utils";
|
||||||
|
import { loadingController, modalController, toastController } from "@ionic/vue";
|
||||||
|
import { ErrorMessage, Field, Form } from "vee-validate";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { client, safeClient } from "@/api";
|
||||||
|
import { getCryptoIcon } from "@/config/crypto";
|
||||||
|
import SelectCurrency from "../withdraw/components/select-currency.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
id: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const walletStore = useWalletStore();
|
||||||
|
const { fundingBalances } = storeToRefs(walletStore);
|
||||||
|
const formInst = useTemplateRef<FormInstance>("formInst");
|
||||||
|
|
||||||
|
// 目标用户信息
|
||||||
|
const targetUser = ref<any>(null);
|
||||||
|
|
||||||
|
// 表单初始值
|
||||||
|
const initialValues = {
|
||||||
|
assetCode: "USDT",
|
||||||
|
amount: "",
|
||||||
|
memo: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
// 可用余额
|
||||||
|
const availableBalance = computed(() => {
|
||||||
|
const form = formInst.value?.values as any;
|
||||||
|
if (!form)
|
||||||
|
return "0";
|
||||||
|
|
||||||
|
const balance = fundingBalances.value?.find(item => item.assetCode === form.assetCode);
|
||||||
|
return balance ? balance.available : "0";
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证规则
|
||||||
|
const schema = computed(() => z.object({
|
||||||
|
assetCode: z.string().min(1, "请选择币种"),
|
||||||
|
amount: z
|
||||||
|
.string()
|
||||||
|
.min(1, "请输入转账金额")
|
||||||
|
.refine(value => Number(value) > 0, "转账金额必须大于0")
|
||||||
|
.refine(value => Number(value) <= Number(availableBalance.value), `可用余额不足,当前余额:${availableBalance.value}`),
|
||||||
|
memo: z.string().optional(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 打开币种选择
|
||||||
|
async function openSelectCurrency() {
|
||||||
|
const modal = await modalController.create({
|
||||||
|
component: SelectCurrency,
|
||||||
|
componentProps: {
|
||||||
|
onSelect: (code: string) => {
|
||||||
|
formInst.value?.setFieldValue("assetCode", code);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
breakpoints: [0, 0.8],
|
||||||
|
initialBreakpoint: 0.8,
|
||||||
|
handle: true,
|
||||||
|
});
|
||||||
|
await modal.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置全部金额
|
||||||
|
function setMaxAmount() {
|
||||||
|
formInst.value?.setFieldValue("amount", availableBalance.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取目标用户信息
|
||||||
|
async function fetchTargetUser() {
|
||||||
|
const loading = await loadingController.create({
|
||||||
|
message: "加载用户信息...",
|
||||||
|
});
|
||||||
|
await loading.present();
|
||||||
|
|
||||||
|
const { data, error } = await safeClient(() => client.api.user({ uid: props.id }).get());
|
||||||
|
|
||||||
|
await loading.dismiss();
|
||||||
|
|
||||||
|
if (!error.value && data.value) {
|
||||||
|
targetUser.value = data.value;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const toast = await toastController.create({
|
||||||
|
message: "用户不存在或已注销, 即将返回上一页",
|
||||||
|
duration: 2000,
|
||||||
|
color: "warning",
|
||||||
|
});
|
||||||
|
await toast.present();
|
||||||
|
setTimeout(() => {
|
||||||
|
router.back();
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交转账
|
||||||
|
async function onSubmit(values: GenericObject) {
|
||||||
|
const loading = await loadingController.create({
|
||||||
|
message: "转账中...",
|
||||||
|
});
|
||||||
|
await loading.present();
|
||||||
|
|
||||||
|
const { error } = await safeClient(() => client.api.transfer.post({
|
||||||
|
assetCode: values.assetCode as string,
|
||||||
|
amount: values.amount as string,
|
||||||
|
toUserId: props.id,
|
||||||
|
memo: values.memo as string,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await loading.dismiss();
|
||||||
|
|
||||||
|
if (!error.value) {
|
||||||
|
const toast = await toastController.create({
|
||||||
|
message: "转账成功",
|
||||||
|
duration: 2000,
|
||||||
|
position: "bottom",
|
||||||
|
color: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
await toast.present();
|
||||||
|
|
||||||
|
// 刷新余额
|
||||||
|
walletStore.syncBalances();
|
||||||
|
|
||||||
|
router.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log("Transfer to user ID:", props.id);
|
||||||
|
fetchTargetUser();
|
||||||
|
walletStore.syncFundingBalances();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ion-page>
|
||||||
|
<ion-header class="ion-no-border">
|
||||||
|
<ion-toolbar class="ion-toolbar">
|
||||||
|
<ion-buttons slot="start">
|
||||||
|
<ion-back-button default-href="/layout/user" />
|
||||||
|
</ion-buttons>
|
||||||
|
<ion-title>转账给用户</ion-title>
|
||||||
|
</ion-toolbar>
|
||||||
|
</ion-header>
|
||||||
|
<ion-content :fullscreen="true" class="ion-padding">
|
||||||
|
<!-- 用户信息卡片 -->
|
||||||
|
<div v-if="targetUser" class="bg-text-900 rounded-2xl p-4 mb-6">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<ui-avatar :src="targetUser.avatar" class="size-14" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="text-base font-semibold">
|
||||||
|
{{ targetUser.nickname || targetUser.username }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-text-400 mt-1">
|
||||||
|
ID: {{ targetUser.uid }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Form
|
||||||
|
ref="formInst"
|
||||||
|
:validation-schema="schema"
|
||||||
|
:initial-values="initialValues"
|
||||||
|
@submit="onSubmit"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-5">
|
||||||
|
<!-- 币种选择 -->
|
||||||
|
<div>
|
||||||
|
<Field name="assetCode">
|
||||||
|
<template #default="{ value }">
|
||||||
|
<ion-label class="block text-sm font-medium mb-2">
|
||||||
|
选择币种
|
||||||
|
</ion-label>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between bg-text-900 rounded-2xl p-4 cursor-pointer"
|
||||||
|
@click="openSelectCurrency"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<component :is="getCryptoIcon(value)" class="w-8 h-8" />
|
||||||
|
<span class="text-base font-medium">{{ value }}</span>
|
||||||
|
</div>
|
||||||
|
<ion-icon name="chevron-forward-outline" class="text-text-400" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Field>
|
||||||
|
<ErrorMessage name="assetCode" class="text-red-500 text-xs mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 转账金额 -->
|
||||||
|
<div>
|
||||||
|
<Field name="amount">
|
||||||
|
<template #default="{ field }">
|
||||||
|
<div class="relative">
|
||||||
|
<ui-input-label
|
||||||
|
v-bind="field"
|
||||||
|
label="转账金额"
|
||||||
|
placeholder="请输入转账金额"
|
||||||
|
type="number"
|
||||||
|
inputmode="decimal"
|
||||||
|
/>
|
||||||
|
<ion-button
|
||||||
|
fill="clear"
|
||||||
|
size="small"
|
||||||
|
class="absolute right-0 top-8 text-sm font-semibold"
|
||||||
|
@click="setMaxAmount"
|
||||||
|
>
|
||||||
|
全部
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Field>
|
||||||
|
<ErrorMessage name="amount" class="text-red-500 text-xs mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 可用余额 -->
|
||||||
|
<div class="flex items-center justify-between px-1">
|
||||||
|
<span class="text-sm text-text-400">可用余额</span>
|
||||||
|
<span class="text-sm font-medium">{{ Number(availableBalance).toFixed(2) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 备注 -->
|
||||||
|
<div>
|
||||||
|
<Field name="memo">
|
||||||
|
<template #default="{ field }">
|
||||||
|
<ui-input-label
|
||||||
|
v-bind="field"
|
||||||
|
label="备注(可选)"
|
||||||
|
placeholder="请输入备注信息"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Field>
|
||||||
|
<ErrorMessage name="memo" class="text-red-500 text-xs mt-1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<ion-button
|
||||||
|
expand="block"
|
||||||
|
type="submit"
|
||||||
|
shape="round"
|
||||||
|
class="mt-6 h-12 font-semibold"
|
||||||
|
>
|
||||||
|
确认转账
|
||||||
|
</ion-button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</ion-content>
|
||||||
|
</ion-page>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user