Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -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[] = [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
@import "tailwindcss";
|
||||
@config "../../tailwind.config.ts";
|
||||
@import "./common.css";
|
||||
@import "./common.css";
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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")),
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -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")),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() ?? "",
|
||||
}));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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")),
|
||||
}));
|
||||
|
||||
// 交换账户
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user