feat: 添加转账给用户功能,更新路由和二维码扫描成功处理

This commit is contained in:
2026-01-12 12:58:37 +07:00
parent 8a7af3783b
commit aff0d9019a
4 changed files with 273 additions and 5 deletions

View File

@@ -95,6 +95,12 @@ const routes: Array<RouteRecordRaw> = [
component: () => import("@/views/wallet/transfer.vue"),
meta: { requiresAuth: true },
},
{
path: "/transfer_to_user/:id",
props: true,
component: () => import("@/views/wallet/transfer-to-user.vue"),
meta: { requiresAuth: true },
},
{
path: "/wallet/funding",
component: () => import("@/views/wallet/funding.vue"),

View File

@@ -4,13 +4,19 @@ import { toastController } from "@ionic/vue";
const router = useRouter();
const scanner = useTemplateRef<InstanceType<typeof QrScanner>>("scanner");
function handleSuccess(value: string) {
toastController.create({
message: String(value),
duration: 2000,
async function handleSuccess(value: string) {
const toast = await toastController.create({
message: "扫描成功",
duration: 1000,
position: "bottom",
color: "success",
}).then(toast => toast.present());
});
await toast.present();
if (value.startsWith("/transfer_to_user/")) {
// 完整路径
router.replace(value);
}
}
function handleError(error: Error) {

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