Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-01-11 15:51:22 +07:00
parent 6f8a8de9be
commit 309606565b
46 changed files with 28649 additions and 28522 deletions

View File

@@ -1,8 +1,8 @@
import type { PhoneCountry } from "./type";
import { toTypedSchema } from "@vee-validate/yup";
import { toTypedSchema } from "@vee-validate/zod";
import { emailOTPClient, phoneNumberClient, usernameClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/vue";
import * as yup from "yup";
import { z } from "zod";
import CircleFlagsCnHk from "~icons/circle-flags/cn-hk";
import CircleFlagsEnUs from "~icons/circle-flags/en-us";
import CircleFlagsTw from "~icons/circle-flags/tw";
@@ -23,15 +23,15 @@ export const authClient = createAuthClient({
plugins: [emailOTPClient(), phoneNumberClient(), usernameClient()],
});
export const emailSchema = toTypedSchema(yup.object({
email: yup
.string()
.required(i18n.global.t("auth.login.validation.emailRequired"))
export const emailSchema = toTypedSchema(z.object({
email: z
.string({ message: i18n.global.t("auth.login.validation.emailRequired") })
.min(1, i18n.global.t("auth.login.validation.emailRequired"))
.email(i18n.global.t("auth.login.validation.emailInvalid")),
otp: yup
.string()
.required(i18n.global.t("auth.login.validation.otpRequired"))
.matches(/^\d{6}$/, i18n.global.t("auth.login.validation.otpInvalid")),
otp: z
.string({ message: i18n.global.t("auth.login.validation.otpRequired") })
.min(1, i18n.global.t("auth.login.validation.otpRequired"))
.regex(/^\d{6}$/, i18n.global.t("auth.login.validation.otpInvalid")),
}));
export const countries: PhoneCountry[] = [

View File

@@ -1,5 +1,5 @@
.unselectable {
user-select: none;
--moz-user-select: none;
-webkit-user-select: none;
}
user-select: none;
--moz-user-select: none;
-webkit-user-select: none;
}

View File

@@ -1,3 +1,3 @@
@import "tailwindcss";
@config "../../tailwind.config.ts";
@import "./common.css";
@import "./common.css";

View File

@@ -3,7 +3,7 @@ import type { GenericObject } from "vee-validate";
import type { EmailVerifyClient } from "@/api/types";
import { toastController } from "@ionic/vue";
import { Field, Form } from "vee-validate";
import * as yup from "yup";
import { z } from "zod";
import { authClient, emailSchema } from "@/auth";
const emit = defineEmits<{
@@ -19,7 +19,7 @@ const canResend = computed(() => countdown.value === 0 && !isSending.value);
const email = ref("");
const emailError = ref("");
let timer: number | null = null;
let timer: NodeJS.Timeout | null = null;
function startCountdown() {
countdown.value = 60;
@@ -42,7 +42,7 @@ async function sendOtp() {
}
try {
await yup.string().email().validate(emailValue);
await z.string().email().parseAsync(emailValue);
}
catch {
emailError.value = t("auth.login.validation.emailInvalid");

View File

@@ -3,10 +3,10 @@ import type { GenericObject } from "vee-validate";
import type { PhoneNumberVerifyClient } from "@/api/types";
import type { PhoneCountry } from "@/auth/type";
import { toastController } from "@ionic/vue";
import { toTypedSchema } from "@vee-validate/yup";
import { toTypedSchema } from "@vee-validate/zod";
import { chevronDown } from "ionicons/icons";
import { Field, Form } from "vee-validate";
import * as yup from "yup";
import { z } from "zod";
import { authClient, countries } from "@/auth";
import Country from "./country.vue";
@@ -42,19 +42,18 @@ function validatePhoneNumber(phone: string): boolean {
return currentCountry.value.pattern.test(phone);
}
const schema = computed(() => toTypedSchema(yup.object({
phoneNumber: yup
.string()
.required(t("auth.login.validation.phoneNumberRequired"))
.test(
"phone-format",
const schema = computed(() => toTypedSchema(z.object({
phoneNumber: z
.string({ message: t("auth.login.validation.phoneNumberRequired") })
.min(1, t("auth.login.validation.phoneNumberRequired"))
.refine(
value => validatePhoneNumber(value),
t("auth.login.validation.phoneNumberInvalid"),
value => !value || validatePhoneNumber(value),
),
code: yup
.string()
.required(t("auth.login.validation.codeRequired"))
.matches(/^\d{6}$/, t("auth.login.validation.codeInvalid")),
code: z
.string({ message: t("auth.login.validation.codeRequired") })
.min(1, t("auth.login.validation.codeRequired"))
.regex(/^\d{6}$/, t("auth.login.validation.codeInvalid")),
})));
function startCountdown() {

View File

@@ -1,10 +1,10 @@
<script lang='ts' setup>
import type { GenericObject } from "vee-validate";
import type { RwaIssuanceCategoriesData, RwaIssuanceProductBody } from "@/api/types";
import { toTypedSchema } from "@vee-validate/yup";
import { toTypedSchema } from "@vee-validate/zod";
import { ErrorMessage, Field, Form } from "vee-validate";
import { useI18n } from "vue-i18n";
import * as yup from "yup";
import { z } from "zod";
const props = defineProps<{
initialData: RwaIssuanceProductBody["product"];
@@ -16,10 +16,10 @@ const emit = defineEmits<{
const { t } = useI18n();
const schema = toTypedSchema(
yup.object({
name: yup.string().required(t("asset.issue.apply.validation.nameRequired")),
code: yup.string().required(t("asset.issue.apply.validation.codeRequired")),
categoryId: yup.string().required(t("asset.issue.apply.validation.categoryRequired")),
z.object({
name: z.string({ message: t("asset.issue.apply.validation.nameRequired") }).min(1, t("asset.issue.apply.validation.nameRequired")),
code: z.string({ message: t("asset.issue.apply.validation.codeRequired") }).min(1, t("asset.issue.apply.validation.codeRequired")),
categoryId: z.string({ message: t("asset.issue.apply.validation.categoryRequired") }).min(1, t("asset.issue.apply.validation.categoryRequired")),
}),
);

View File

@@ -1,11 +1,11 @@
<script lang='ts' setup>
import type { GenericObject } from "vee-validate";
import type { RwaIssuanceProductBody } from "@/api/types";
import { toTypedSchema } from "@vee-validate/yup";
import { toTypedSchema } from "@vee-validate/zod";
import { addOutline, removeOutline } from "ionicons/icons";
import { ErrorMessage, Field, FieldArray, Form } from "vee-validate";
import { useI18n } from "vue-i18n";
import * as yup from "yup";
import { z } from "zod";
const props = defineProps<{
initialData: RwaIssuanceProductBody["editions"];
@@ -35,45 +35,40 @@ const launchDate = ref(new Date().toISOString());
const subscriptionStartDate = ref(new Date().toISOString());
const subscriptionEndDate = ref(new Date().toISOString());
const schema = toTypedSchema(yup.object({
editions: yup.array().of(
yup.object({
editionName: yup.string().required(t("asset.issue.apply.validation.editionNameRequired")),
launchDate: yup.string()
.required(t("asset.issue.apply.validation.launchDateRequired"))
.test("not-past", t("asset.issue.apply.validation.launchDateNotPast"), (value) => {
if (!value)
return true;
return new Date(value) >= new Date(now.value.toDateString());
})
.test("before-subscription", t("asset.issue.apply.validation.launchBeforeSubscription"), (value) => {
if (!value || !subscriptionStartDate.value)
return true;
return new Date(value) < new Date(subscriptionStartDate.value);
}),
subscriptionStartDate: yup.string()
.required(t("asset.issue.apply.validation.subscriptionStartDateRequired"))
.test("not-past", t("asset.issue.apply.validation.subscriptionStartDateNotPast"), (value) => {
if (!value)
return true;
return new Date(value) >= new Date(now.value.toDateString());
})
.test("after-launch", t("asset.issue.apply.validation.subscriptionAfterLaunch"), (value) => {
if (!value || !launchDate.value)
return true;
return new Date(value) > new Date(launchDate.value);
}),
subscriptionEndDate: yup.string()
.required(t("asset.issue.apply.validation.subscriptionEndDateRequired"))
.test("after-start", t("asset.issue.apply.validation.subscriptionEndAfterStart"), (value) => {
if (!value || !subscriptionStartDate.value)
return true;
return new Date(value) > new Date(subscriptionStartDate.value);
}),
perUserLimit: yup.string().required(t("asset.issue.apply.validation.perUserLimitRequired")),
totalSupply: yup.string().required(t("asset.issue.apply.validation.totalSupplyRequired")),
unitPrice: yup.string().required(t("asset.issue.apply.validation.unitPriceRequired")),
dividendRate: yup.string().required(t("asset.issue.apply.validation.dividendRateRequired")),
const schema = toTypedSchema(z.object({
editions: z.array(
z.object({
editionName: z.string({ message: t("asset.issue.apply.validation.editionNameRequired") }).min(1, t("asset.issue.apply.validation.editionNameRequired")),
launchDate: z.string({ message: t("asset.issue.apply.validation.launchDateRequired") })
.min(1, t("asset.issue.apply.validation.launchDateRequired"))
.refine(
value => new Date(value) >= new Date(now.value.toDateString()),
t("asset.issue.apply.validation.launchDateNotPast"),
)
.refine(
value => !subscriptionStartDate.value || new Date(value) < new Date(subscriptionStartDate.value),
t("asset.issue.apply.validation.launchBeforeSubscription"),
),
subscriptionStartDate: z.string({ message: t("asset.issue.apply.validation.subscriptionStartDateRequired") })
.min(1, t("asset.issue.apply.validation.subscriptionStartDateRequired"))
.refine(
value => new Date(value) >= new Date(now.value.toDateString()),
t("asset.issue.apply.validation.subscriptionStartDateNotPast"),
)
.refine(
value => !launchDate.value || new Date(value) > new Date(launchDate.value),
t("asset.issue.apply.validation.subscriptionAfterLaunch"),
),
subscriptionEndDate: z.string({ message: t("asset.issue.apply.validation.subscriptionEndDateRequired") })
.min(1, t("asset.issue.apply.validation.subscriptionEndDateRequired"))
.refine(
value => !subscriptionStartDate.value || new Date(value) > new Date(subscriptionStartDate.value),
t("asset.issue.apply.validation.subscriptionEndAfterStart"),
),
perUserLimit: z.string({ message: t("asset.issue.apply.validation.perUserLimitRequired") }).min(1, t("asset.issue.apply.validation.perUserLimitRequired")),
totalSupply: z.string({ message: t("asset.issue.apply.validation.totalSupplyRequired") }).min(1, t("asset.issue.apply.validation.totalSupplyRequired")),
unitPrice: z.string({ message: t("asset.issue.apply.validation.unitPriceRequired") }).min(1, t("asset.issue.apply.validation.unitPriceRequired")),
dividendRate: z.string({ message: t("asset.issue.apply.validation.dividendRateRequired") }).min(1, t("asset.issue.apply.validation.dividendRateRequired")),
}),
),
}));

View File

@@ -1,10 +1,10 @@
<script lang='ts' setup>
import type { GenericObject } from "vee-validate";
import { SelectChangeEventDetail, toastController } from "@ionic/vue";
import { toTypedSchema } from "@vee-validate/yup";
import { toTypedSchema } from "@vee-validate/zod";
import { informationCircle, shieldCheckmark } from "ionicons/icons";
import { ErrorMessage, Field, Form } from "vee-validate";
import * as yup from "yup";
import { z } from "zod";
import { client, safeClient } from "@/api";
const { t } = useI18n();
@@ -17,12 +17,12 @@ const formInst = useTemplateRef<FormInstance>("formInst");
// 表单验证 Schema
const schema = toTypedSchema(
yup.object({
bankName: yup.string().required(t("bankCard.form.validation.bankRequired")),
accountNumber: yup
.string()
.required(t("bankCard.form.validation.accountNumberRequired")),
accountName: yup.string().required(t("bankCard.form.validation.accountNameRequired")),
z.object({
bankName: z.string({ message: t("bankCard.form.validation.bankRequired") }).min(1, t("bankCard.form.validation.bankRequired")),
accountNumber: z
.string({ message: t("bankCard.form.validation.accountNumberRequired") })
.min(1, t("bankCard.form.validation.accountNumberRequired")),
accountName: z.string({ message: t("bankCard.form.validation.accountNameRequired") }).min(1, t("bankCard.form.validation.accountNameRequired")),
}),
);

View File

@@ -1,9 +1,130 @@
<script lang='ts' setup>
import type { SpotOrderBody } from "@/api/types";
import { modalController, toastController } from "@ionic/vue";
import { closeOutline } from "ionicons/icons";
import { client, safeClient } from "@/api";
import { tradeWayConfig } from "../config";
const props = defineProps<{
form: SpotOrderBody & { amount: string };
}>();
const currentTradeWay = computed(() => {
return tradeWayConfig.find(item => item.value === props.form.orderType);
});
function onClose() {
modalController.dismiss();
}
async function onConfirm() {
await safeClient(client.api.spot_order.create.post({
orderType: props.form.orderType,
quantity: props.form.quantity,
side: props.form.side,
symbol: props.form.symbol,
memo: props.form.memo,
price: props.form.price,
}));
const toast = await toastController.create({
message: "订单提交成功",
duration: 2000,
position: "top",
color: "success",
});
await toast.present();
modalController.dismiss();
}
</script>
<template>
Hello world
<div class="ion-padding h-80">
<div class="flex justify-between items-center mb-5">
<div class="font-semibold">
下单确认
</div>
<ion-icon :icon="closeOutline" class="text-2xl" @click="onClose" />
</div>
<div>
<div class="flex items-center gap-2 mb-2">
<div class="text-sm">
{{ form.symbol }}
</div>
<ui-tag size="mini" :type="form.side === 'buy' ? 'success' : 'danger'">
{{ form.side === 'buy' ? '买入' : '卖出' }}
</ui-tag>
</div>
<template v-if="form.orderType === 'limit'">
<div class="cell">
<div class="label">
委托价格
</div>
<div class="value">
{{ form.price }} USDT
</div>
</div>
<div class="cell">
<div class="label">
数量
</div>
<div class="value">
{{ form.quantity }}
</div>
</div>
<div class="cell">
<div class="label">
金额
</div>
<div class="value">
{{ form.amount }} USDT
</div>
</div>
<div class="cell">
<div class="label">
类型
</div>
<div class="value">
{{ currentTradeWay?.name }}
</div>
</div>
</template>
<template v-else-if="form.orderType === 'market'">
<div class="cell">
<div class="label">
委托价格
</div>
<div class="value">
{{ form.price }} USDT
</div>
</div>
<div class="cell">
<div class="label">
数量
</div>
<div class="value">
{{ form.quantity }}
</div>
</div>
</template>
</div>
<div class="mt-6">
<ion-button expand="block" color="success" @click="onConfirm">
确认下单
</ion-button>
</div>
</div>
</template>
<style lang='css' scoped></style>
<style lang='css' scoped>
@reference "tailwindcss";
.cell {
@apply flex justify-between items-center py-1;
}
.label {
@apply text-sm text-(--ion-text-color-step-400);
}
.value {
@apply text-sm font-semibold;
}
</style>

View File

@@ -7,6 +7,9 @@ import { caretDownOutline } from "ionicons/icons";
import { tradeWayConfig } from "../config";
const model = defineModel({ type: Object as PropType<SpotOrderBody>, required: true });
const currentTradeWay = computed(() => {
return tradeWayConfig.find(item => item.value === model.value.orderType);
});
function onSelectTradeWay(item: TradeWayConfig) {
model.value.orderType = item.value;
@@ -17,7 +20,7 @@ function onSelectTradeWay(item: TradeWayConfig) {
<template>
<div id="open-modal" class="bg-faint flex items-center justify-between px-4 py-2 rounded-md">
<div class="text-xs font-medium text-text-300">
市场
{{ currentTradeWay?.name }}
</div>
<ion-icon :icon="caretDownOutline" />
</div>

View File

@@ -1,4 +1,4 @@
import * as yup from "yup";
import { z } from "zod";
export enum TradeWayValueEnum {
LIMIT = "limit",
@@ -21,28 +21,35 @@ export const tradeWayConfig: TradeWayConfig[] = [
description: "以指定价格买入或卖出",
icon: "hugeicons:trade-up",
},
{
name: "市价委托",
value: "market",
description: "以市场价格买入或卖出",
icon: "hugeicons:trade-down",
},
];
export const confirmOrderSchema = yup.object({
price: yup.number().when("way", {
is: TradeWayValueEnum.LIMIT !== undefined,
then: yup
.number()
.typeError("请输入有效的价格")
.required("价格为必填项")
.moreThan(0, "价格必须大于0"),
otherwise: yup.number().notRequired(),
}),
amount: yup
.number()
.typeError("请输入有效的数量")
.required("数量为必填项")
.moreThan(0, "数量必须大于0"),
way: yup
.mixed<TradeWayValue>()
.oneOf(
Object.values(TradeWayValueEnum),
"请选择有效的交易方式",
)
.required("交易方式为必填项"),
});
export const confirmOrderSchema = z.object({
quantity: z.coerce.number({ message: "请输入有效的数量" }).gt(0, "数量必须大于0"),
price: z.coerce.number({ message: "请输入有效的价格" }).gt(0, "价格必须大于0").optional().or(z.coerce.number().optional()),
orderType: z.enum([TradeWayValueEnum.LIMIT, TradeWayValueEnum.MARKET], {
message: "请选择有效的交易方式",
}) as z.ZodType<TradeWayValue>,
}).refine(
(data) => {
if (data.orderType === TradeWayValueEnum.LIMIT) {
return data.price !== undefined && data.price > 0;
}
return true;
},
{
message: "价格为必填项",
path: ["price"],
},
);
export const confirmOrderSubmitSchema = confirmOrderSchema.transform(data => ({
...data,
quantity: data.quantity.toString(),
price: data.price?.toString() ?? "",
}));

View File

@@ -2,6 +2,7 @@
import type { ChartingLibraryWidgetOptions } from "#/charting_library";
import type { SpotOrderBody } from "@/api/types";
import type { TradingViewInst } from "@/tradingview/index";
import type { ModalInstance } from "@/utils";
import { modalController } from "@ionic/vue";
import { useRouteQuery } from "@vueuse/router";
import { caretDownOutline, ellipsisHorizontal } from "ionicons/icons";
@@ -9,28 +10,32 @@ import MaterialSymbolsCandlestickChartOutline from "~icons/material-symbols/cand
import { client, safeClient } from "@/api";
import { TradeTypeEnum } from "@/api/enum";
import { TradingViewChart } from "@/tradingview/index";
import ConfirmOrder from "./components/confirm-order.vue";
import OrdersPanel from "./components/orders-panel.vue";
import TradePairsModal from "./components/trade-pairs-modal.vue";
import TradeSwitch from "./components/trade-switch.vue";
import TradeWay from "./components/trade-way.vue";
import { confirmOrderSchema, TradeWayValueEnum } from "./config";
import { confirmOrderSubmitSchema, TradeWayValueEnum } from "./config";
const { data } = await safeClient(client.api.trading_pairs.get({ query: { limit: 1 } }));
const mode = useRouteQuery<TradeTypeEnum>("mode", TradeTypeEnum.BUY);
const symbol = useRouteQuery<string>("symbol", "BTCUSD");
const symbol = useRouteQuery<string>("symbol", data.value?.data[0].symbol);
const tradingviewOptions: Partial<ChartingLibraryWidgetOptions> = {
disabled_features: [
"create_volume_indicator_by_default",
],
};
const tradingViewInst = useTemplateRef<TradingViewInst>("tradingViewInst");
const [form] = useResetRef<SpotOrderBody>({
const confirmModalInst = useTemplateRef<ModalInstance>("confirmModalInst");
const [form] = useResetRef<SpotOrderBody & { amount: string }>({
orderType: TradeWayValueEnum.LIMIT,
quantity: "",
side: mode.value,
symbol: symbol.value,
memo: "",
price: "",
amount: "",
});
async function openTradePairs() {
@@ -40,22 +45,38 @@ async function openTradePairs() {
initialBreakpoint: 0.95,
handle: true,
});
await modal.present();
const { data: result } = await modal.onWillDismiss<string>();
if (result) {
symbol.value = result;
result && (symbol.value = result);
}
function handleChangeQuantity(event) {
const val = (event.target as HTMLInputElement).value;
if (val && form.value.price) {
const amount = Number(val) * Number(form.value.price);
form.value.amount = amount.toString();
}
else {
form.value.amount = "";
}
}
function handleSubmit() {
confirmOrderSchema.validate(form.value).then(() => {
console.log("submit successfully");
}).catch((err) => {
console.log("submit failed:", err);
});
function handleChangeAmount(event) {
const val = (event.target as HTMLInputElement).value;
if (val && form.value.price) {
const quantity = Number(val) / Number(form.value.price);
form.value.quantity = quantity.toString();
}
else {
form.value.quantity = "";
}
}
async function handleSubmit() {
try {
await confirmOrderSubmitSchema.parseAsync(form.value);
confirmModalInst.value?.$el.present();
}
catch (err) {
console.error("订单验证失败:", err);
}
}
</script>
@@ -90,21 +111,37 @@ function handleSubmit() {
<TradeSwitch v-model:active="mode" @update:active="val => form.side = val" />
<TradeWay v-model="form" />
<template v-if="form.orderType === 'limit'">
<ion-input v-model="form.price" label="价格" class="count" inputmode="decimal" type="number" placeholder="请输入价格(USDT)" />
<ion-input v-model="form.price" label="价格" class="count" inputmode="decimal" type="number" placeholder="请输入价格">
<span slot="end">USDT</span>
</ion-input>
<ion-input v-model="form.quantity" label="数量" class="count" inputmode="decimal" type="number" placeholder="请输入交易数量" @ion-input="handleChangeQuantity">
<span slot="end">{{ symbol }}</span>
</ion-input>
<ion-input v-model="form.amount" label="金额" class="count" inputmode="decimal" type="number" placeholder="请输入交易金额" @ion-input="handleChangeAmount">
<span slot="end">USDT</span>
</ion-input>
</template>
<ion-input v-model="form.quantity" label="数量" class="count" inputmode="decimal" type="number" placeholder="请输入交易数量">
<span slot="end">{{ symbol }}</span>
</ion-input>
<ion-input v-model="form.price" label="金额" class="count" inputmode="decimal" type="number" placeholder="请输入交易金额">
<span slot="end">USDT</span>
</ion-input>
<template v-else-if="form.orderType === 'market'">
<ion-input v-model="form.price" label="价格" class="count" inputmode="decimal" type="number" placeholder="请输入价格">
<span slot="end">USDT</span>
</ion-input>
<ion-input v-model="form.quantity" label="数量" class="count" inputmode="decimal" type="number" placeholder="请输入交易数量" @ion-input="handleChangeQuantity">
<span slot="end">{{ symbol }}</span>
</ion-input>
</template>
<!-- <ion-range class="range" aria-label="Range with ticks" :pin="true" :ticks="true" :snaps="true" :min="0" :max="5" /> -->
<ion-button expand="block" size="small" shape="round" :color="mode === TradeTypeEnum.BUY ? 'success' : 'danger'" @click="handleSubmit">
{{ mode === TradeTypeEnum.BUY ? '买入' : '卖出' }}
</ion-button>
<ion-modal ref="confirmModalInst" class="confirm-modal" :breakpoints="[0, 1]" :initial-breakpoint="1" :handle="false">
<ConfirmOrder :form="form" />
</ion-modal>
</div>
<div class="col-span-2" />
</div>
<div class="mt-6 px-4 pb-4">
<OrdersPanel />
</div>
@@ -145,4 +182,7 @@ ion-range.range::part(tick-active) {
top: 18px;
border-radius: 100%;
}
.confirm-modal {
--height: auto;
}
</style>

View File

@@ -3,7 +3,7 @@ import type { GenericObject } from "vee-validate";
import { toastController } from "@ionic/vue";
import { arrowBackOutline } from "ionicons/icons";
import { Field, Form } from "vee-validate";
import * as yup from "yup";
import { z } from "zod";
import { safeClient } from "@/api";
import { authClient, emailSchema } from "@/auth";
@@ -17,7 +17,7 @@ const isSending = ref(false);
const canResend = computed(() => countdown.value === 0 && !isSending.value);
const emailError = ref("");
let timer: number | null = null;
let timer: NodeJS.Timeout | null = null;
function startCountdown() {
countdown.value = 60;
@@ -40,7 +40,7 @@ async function sendOtp() {
}
try {
await yup.string().email().validate(emailValue);
await z.string().email().parseAsync(emailValue);
}
catch {
emailError.value = t("auth.login.validation.emailInvalid");

View File

@@ -4,7 +4,7 @@ import type { FormInstance } from "@/utils";
import { loadingController, toastController } from "@ionic/vue";
import { swapVerticalOutline } from "ionicons/icons";
import { ErrorMessage, Field, Form } from "vee-validate";
import * as yup from "yup";
import { z } from "zod";
import { client, safeClient } from "@/api";
import { AssetCodeEnum } from "@/api/enum";
import { getCryptoIcon } from "@/config/crypto";
@@ -43,15 +43,15 @@ const availableBalance = computed(() => {
});
// 验证规则
const schema = computed(() => yup.object({
assetCode: yup.string().required(t("transfer.assetCodeRequired")),
amount: yup
.string()
.required(t("transfer.amountRequired"))
.test("min", t("transfer.amountMinError"), value => Number(value) > 0)
.test("max", t("transfer.amountMaxError", { amount: availableBalance.value }), value => Number(value) <= Number(availableBalance.value)),
fromAccount: yup.string().required(t("transfer.fromAccountRequired")),
toAccount: yup.string().required(t("transfer.toAccountRequired")),
const schema = computed(() => z.object({
assetCode: z.string({ message: t("transfer.assetCodeRequired") }).min(1, t("transfer.assetCodeRequired")),
amount: z
.string({ message: t("transfer.amountRequired") })
.min(1, t("transfer.amountRequired"))
.refine(value => Number(value) > 0, t("transfer.amountMinError"))
.refine(value => Number(value) <= Number(availableBalance.value), t("transfer.amountMaxError", { amount: availableBalance.value })),
fromAccount: z.string({ message: t("transfer.fromAccountRequired") }).min(1, t("transfer.fromAccountRequired")),
toAccount: z.string({ message: t("transfer.toAccountRequired") }).min(1, t("transfer.toAccountRequired")),
}));
// 交换账户

View File

@@ -1,43 +1,70 @@
import { toTypedSchema } from "@vee-validate/yup";
import * as yup from "yup";
import { toTypedSchema } from "@vee-validate/zod";
import { z } from "zod";
import { WithdrawMethodEnum } from "@/api/enum";
export function createWithdrawSchema(t: (key: string, params?: any) => string, maxAmount: string) {
return toTypedSchema(
yup.object({
assetCode: yup.string().required(t("withdraw.validation.assetCodeRequired")),
amount: yup
.string()
.required(t("withdraw.validation.amountRequired"))
.test("is-number", t("withdraw.validation.amountInvalid"), (value) => {
return /^\d+(?:\.\d+)?$/.test(value || "");
})
.test("max-amount", t("withdraw.validation.amountExceedsBalance"), (value) => {
if (!value || maxAmount === "0")
return false;
return Number.parseFloat(value) <= Number.parseFloat(maxAmount);
})
.test("min-amount", t("withdraw.validation.amountTooSmall"), (value) => {
if (!value)
return false;
return Number.parseFloat(value) > 0;
}),
withdrawMethod: yup.string().required(t("withdraw.validation.methodRequired")),
bankAccountId: yup.string().when("withdrawMethod", {
is: WithdrawMethodEnum.BANK,
then: schema => schema.required(t("withdraw.validation.bankAccountRequired")),
otherwise: schema => schema.optional(),
}),
chain: yup.string().when("withdrawMethod", {
is: WithdrawMethodEnum.CRYPTO,
then: schema => schema.required(t("withdraw.validation.chainRequired")),
otherwise: schema => schema.optional(),
}),
toAddress: yup.string().when("withdrawMethod", {
is: WithdrawMethodEnum.CRYPTO,
then: schema => schema.required(t("withdraw.validation.addressRequired")).min(10, t("withdraw.validation.addressTooShort")),
otherwise: schema => schema.optional(),
}),
}),
z.object({
assetCode: z.string({ message: t("withdraw.validation.assetCodeRequired") }).min(1, t("withdraw.validation.assetCodeRequired")),
amount: z
.string({ message: t("withdraw.validation.amountRequired") })
.min(1, t("withdraw.validation.amountRequired"))
.refine(
value => /^\d+(?:\.\d+)?$/.test(value),
t("withdraw.validation.amountInvalid"),
)
.refine(
(value) => {
if (maxAmount === "0")
return false;
return Number.parseFloat(value) <= Number.parseFloat(maxAmount);
},
t("withdraw.validation.amountExceedsBalance"),
)
.refine(
value => Number.parseFloat(value) > 0,
t("withdraw.validation.amountTooSmall"),
),
withdrawMethod: z.string({ message: t("withdraw.validation.methodRequired") }).min(1, t("withdraw.validation.methodRequired")),
bankAccountId: z.string().optional(),
chain: z.string().optional(),
toAddress: z.string().optional(),
})
.refine(
(data) => {
if (data.withdrawMethod === WithdrawMethodEnum.BANK) {
return !!data.bankAccountId;
}
return true;
},
{
message: t("withdraw.validation.bankAccountRequired"),
path: ["bankAccountId"],
},
)
.refine(
(data) => {
if (data.withdrawMethod === WithdrawMethodEnum.CRYPTO) {
return !!data.chain;
}
return true;
},
{
message: t("withdraw.validation.chainRequired"),
path: ["chain"],
},
)
.refine(
(data) => {
if (data.withdrawMethod === WithdrawMethodEnum.CRYPTO) {
return !!data.toAddress && data.toAddress.length >= 10;
}
return true;
},
{
message: t("withdraw.validation.addressRequired"),
path: ["toAddress"],
},
),
);
}