feat: 实现发行申请提交
This commit is contained in:
@@ -9,7 +9,7 @@ const client = treaty<App>(window.location.origin, {
|
||||
});
|
||||
|
||||
export async function safeClient<T, E>(
|
||||
requestPromise: Promise<{ data: T; error: E | null }>,
|
||||
requestPromise: Promise<{ data: T; error: E }>,
|
||||
options: { silent?: boolean } = {},
|
||||
) {
|
||||
const { data, error } = await requestPromise;
|
||||
|
||||
@@ -19,3 +19,9 @@ export type WithdrawBody = Omit<Parameters<typeof client.api.withdraw.post>[0],
|
||||
export type UserProfileData = Treaty.Data<typeof client.api.user.profile.get>["userProfile"];
|
||||
|
||||
export type UpdateUserProfileBody = NonNullable<Parameters<typeof client.api.user.profile.put>[0]>;
|
||||
|
||||
export type RwaIssuanceProductsData = Treaty.Data<typeof client.api.rwa.issuance.products.bundle.post>;
|
||||
|
||||
export type RwaIssuanceProductBody = NonNullable<Parameters<typeof client.api.rwa.issuance.products.bundle.post>[0]>;
|
||||
|
||||
export type RwaIssuanceCategoriesData = Treaty.Data<typeof client.api.rwa.issuance.categories.get>;
|
||||
|
||||
2
src/components/ui/collapse/exports.ts
Normal file
2
src/components/ui/collapse/exports.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Collapse } from './index.vue';
|
||||
export type * from './types';
|
||||
138
src/components/ui/collapse/index.vue
Normal file
138
src/components/ui/collapse/index.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<script lang='ts' setup>
|
||||
import type { CollapseEmits, CollapseProps } from "./types";
|
||||
import { IonIcon } from "@ionic/vue";
|
||||
import { chevronDownOutline } from "ionicons/icons";
|
||||
import { nextTick, ref } from "vue";
|
||||
|
||||
const props = withDefaults(defineProps<CollapseProps>(), {
|
||||
disabled: false,
|
||||
bordered: true,
|
||||
size: "medium",
|
||||
});
|
||||
|
||||
defineEmits<CollapseEmits>();
|
||||
|
||||
const active = defineModel<boolean>("active", { type: Boolean, default: true });
|
||||
|
||||
const contentRef = ref<HTMLElement>();
|
||||
|
||||
async function toggle() {
|
||||
if (props.disabled)
|
||||
return;
|
||||
|
||||
if (!active.value) {
|
||||
// 展开
|
||||
active.value = true;
|
||||
await nextTick();
|
||||
|
||||
if (contentRef.value) {
|
||||
const height = contentRef.value.scrollHeight;
|
||||
contentRef.value.style.height = "0px";
|
||||
requestAnimationFrame(() => {
|
||||
contentRef.value!.style.height = `${height}px`;
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
// 收起
|
||||
if (contentRef.value) {
|
||||
const height = contentRef.value.scrollHeight;
|
||||
contentRef.value.style.height = `${height}px`;
|
||||
requestAnimationFrame(() => {
|
||||
contentRef.value!.style.height = "0px";
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
active.value = false;
|
||||
}, 300);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (contentRef.value && active.value) {
|
||||
contentRef.value.style.height = "auto";
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function onTransitionEnd() {
|
||||
if (contentRef.value && !active.value) {
|
||||
contentRef.value.style.height = "0px";
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法和属性
|
||||
defineExpose({
|
||||
toggle,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="w-full overflow-hidden"
|
||||
:class="{
|
||||
'border border-gray-200 dark:border-gray-600 rounded-lg': bordered,
|
||||
'opacity-60': disabled,
|
||||
}"
|
||||
>
|
||||
<!-- 头部触发区域 -->
|
||||
<div
|
||||
class="flex items-center justify-between cursor-pointer transition-colors duration-200 select-none"
|
||||
:class="[
|
||||
{
|
||||
'border-b border-gray-200 dark:border-gray-600': bordered,
|
||||
},
|
||||
{
|
||||
'p-3 text-sm': size === 'small',
|
||||
'p-4 text-base': size === 'medium',
|
||||
'p-5 text-lg': size === 'large',
|
||||
},
|
||||
{
|
||||
'cursor-not-allowed': disabled,
|
||||
},
|
||||
]"
|
||||
@click="toggle"
|
||||
>
|
||||
<div class="font-medium text-gray-900 dark:text-white flex-1">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<IonIcon
|
||||
:icon="chevronDownOutline"
|
||||
class="text-gray-500 dark:text-gray-400 transition-transform duration-300"
|
||||
:class="[
|
||||
{
|
||||
'w-4 h-4': size === 'small',
|
||||
'w-5 h-5': size === 'medium',
|
||||
'w-6 h-6': size === 'large',
|
||||
},
|
||||
{
|
||||
'rotate-180': active,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div
|
||||
ref="contentRef"
|
||||
class="overflow-hidden transition-all duration-300 ease-in-out"
|
||||
:style="{ height: active ? 'auto' : '0' }"
|
||||
@transitionend="onTransitionEnd"
|
||||
>
|
||||
<div
|
||||
class="p-4 text-gray-600 dark:text-gray-300 space-y-5"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='css' scoped>
|
||||
.select-none {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
29
src/components/ui/collapse/types.ts
Normal file
29
src/components/ui/collapse/types.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export interface CollapseProps {
|
||||
/** 面板标题 */
|
||||
title?: string;
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean;
|
||||
/** 是否显示边框 */
|
||||
bordered?: boolean;
|
||||
/** 尺寸大小 */
|
||||
size?: "small" | "medium" | "large";
|
||||
/** 是否展开 (v-model:active) */
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface CollapseEmits {
|
||||
/** 展开状态变化时触发 */
|
||||
"update:active": [value: boolean];
|
||||
}
|
||||
|
||||
export interface CollapseSlots {
|
||||
/** 默认插槽 - 面板内容 */
|
||||
default: () => any;
|
||||
/** 标题插槽 - 自定义标题内容 */
|
||||
title: () => any;
|
||||
}
|
||||
|
||||
export interface CollapseInstance {
|
||||
/** 切换展开/收起状态 */
|
||||
toggle: () => void;
|
||||
}
|
||||
39
src/components/ui/datetime/index.vue
Normal file
39
src/components/ui/datetime/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang='ts' setup>
|
||||
import type { FieldBindingObject } from "vee-validate";
|
||||
|
||||
interface Props extends FieldBindingObject {
|
||||
label?: string;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
function handleChange(value: string) {
|
||||
const formattedValue = useDateFormat(value, "YYYY/MM/DD").value;
|
||||
props.onChange(formattedValue);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col items-start">
|
||||
<ion-label class="text-sm font-bold color-(--ion-text-color-secondary) mb-3.5">
|
||||
{{ props.label }}
|
||||
</ion-label>
|
||||
<ion-datetime-button datetime="datetime" color="primary">
|
||||
<div slot="date-target">
|
||||
{{ props.value }}
|
||||
</div>
|
||||
</ion-datetime-button>
|
||||
<ion-modal :keep-contents-mounted="true">
|
||||
<ion-datetime
|
||||
id="datetime"
|
||||
class="ui-datetime"
|
||||
done-text="Done"
|
||||
presentation="date"
|
||||
:show-default-buttons="true"
|
||||
@ion-change="handleChange($event.detail.value as string)"
|
||||
/>
|
||||
</ion-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='css' scoped></style>
|
||||
@@ -1,9 +0,0 @@
|
||||
<script lang='ts' setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
hello world
|
||||
</template>
|
||||
|
||||
<style lang='css' scoped></style>
|
||||
22
src/components/ui/result/index.vue
Normal file
22
src/components/ui/result/index.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang='ts' setup>
|
||||
defineProps<{
|
||||
icon?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex-col-center space-y-3">
|
||||
<ion-icon :icon="icon" class="text-5xl text-lime-500" />
|
||||
<div class="text-xl font-semibold">
|
||||
{{ title }}
|
||||
</div>
|
||||
<p class="text-center text-text-400 max-w-xs">
|
||||
{{ description }}
|
||||
</p>
|
||||
<slot name="extra" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='css' scoped></style>
|
||||
@@ -47,7 +47,63 @@
|
||||
"myCompany": "My Company",
|
||||
"issuingApply": "Issuing Apply",
|
||||
"audit": "Audit",
|
||||
"supervision": "Supervision"
|
||||
"supervision": "Supervision",
|
||||
"apply": {
|
||||
"title": "Issuing Apply",
|
||||
"base": "Base",
|
||||
"issuePeriod": "Issue Period",
|
||||
"productName": "Product Name",
|
||||
"enterProductName": "Please enter product name",
|
||||
"productCode": "Product Code",
|
||||
"enterProductCode": "Please enter product code",
|
||||
"productType": "Product Type",
|
||||
"chooseProductType": "Please choose product type",
|
||||
"productValue": "Product Valuation",
|
||||
"enterProductValue": "Please enter product valuation (USD)",
|
||||
"assetProof": "Asset Proof",
|
||||
"enterAssetProof": "Please enter asset proof",
|
||||
"totalSupplyLimit": "Total Supply Limit",
|
||||
"enterTotalSupplyLimit": "Please enter total supply limit",
|
||||
"editionName": "Edition Name",
|
||||
"enterEditionName": "Please enter edition name",
|
||||
"launchDate": "Launch Date",
|
||||
"enterLaunchDate": "Please enter launch date",
|
||||
"perUserLimit": "Per User Limit",
|
||||
"enterPerUserLimit": "Please enter per user limit",
|
||||
"totalSupply": "Total Supply",
|
||||
"enterTotalSupply": "Please enter total supply",
|
||||
"subscriptionDeadline": "Subscription Deadline",
|
||||
"enterSubscriptionDeadline": "Please enter subscription deadline",
|
||||
"unitPrice": "Unit Price",
|
||||
"enterUnitPrice": "Please enter unit price",
|
||||
"dividendRate": "Dividend Rate",
|
||||
"enterDividendRate": "Dividend rate (e.g., 0.01 for 1%)",
|
||||
"issuePeriodIndex": "Issue Period {index}",
|
||||
"realEstate": "Real Estate",
|
||||
"snickers": "Snickers",
|
||||
"next": "Next",
|
||||
"back": "Back",
|
||||
"submit": "Submit",
|
||||
"addStep": "Add Step",
|
||||
"removeItem": "Remove Item",
|
||||
"validation": {
|
||||
"nameRequired": "Name is required",
|
||||
"codeRequired": "Code is required",
|
||||
"categoryRequired": "Type is required",
|
||||
"editionNameRequired": "Edition name is required",
|
||||
"launchDateRequired": "Launch date is required",
|
||||
"perUserLimitRequired": "Per user limit is required",
|
||||
"totalSupplyRequired": "Total supply is required",
|
||||
"subscriptionDeadlineRequired": "Subscription deadline is required",
|
||||
"unitPriceRequired": "Unit price is required",
|
||||
"dividendRateRequired": "Dividend rate is required"
|
||||
},
|
||||
"done": {
|
||||
"title": "Application Submitted Successfully",
|
||||
"description": "Your issuance application has been submitted successfully. We will review your application within business days. Please wait patiently for the review results.",
|
||||
"viewProducts": "View My Products"
|
||||
}
|
||||
}
|
||||
},
|
||||
"purchase": {
|
||||
"purchaseAsset": "Purchase Assets",
|
||||
|
||||
@@ -47,7 +47,63 @@
|
||||
"myCompany": "我的企业",
|
||||
"issuingApply": "发行申请",
|
||||
"audit": "审核审计",
|
||||
"supervision": "委托监管"
|
||||
"supervision": "委托监管",
|
||||
"apply": {
|
||||
"title": "发行申请",
|
||||
"base": "基础信息",
|
||||
"issuePeriod": "发行期",
|
||||
"productName": "产品名称",
|
||||
"enterProductName": "请输入产品名称",
|
||||
"productCode": "产品编码",
|
||||
"enterProductCode": "请输入产品编码",
|
||||
"productType": "产品类型",
|
||||
"chooseProductType": "请选择产品类型",
|
||||
"productValue": "产品估值",
|
||||
"enterProductValue": "请输入产品估值(美元)",
|
||||
"assetProof": "资产证明",
|
||||
"enterAssetProof": "请输入资产证明",
|
||||
"totalSupplyLimit": "总发行量上限",
|
||||
"enterTotalSupplyLimit": "请输入总发行量上限",
|
||||
"editionName": "发行期名称",
|
||||
"enterEditionName": "请输入发行期名称",
|
||||
"launchDate": "发行日期",
|
||||
"enterLaunchDate": "请输入发行日期",
|
||||
"perUserLimit": "个人申购上限",
|
||||
"enterPerUserLimit": "请输入个人申购上限",
|
||||
"totalSupply": "发行总量",
|
||||
"enterTotalSupply": "请输入发行总量",
|
||||
"subscriptionDeadline": "申购截止日期",
|
||||
"enterSubscriptionDeadline": "请输入申购截止日期",
|
||||
"unitPrice": "单价",
|
||||
"enterUnitPrice": "请输入单价",
|
||||
"dividendRate": "分红率",
|
||||
"enterDividendRate": "分红率(如0.01代表1%)",
|
||||
"issuePeriodIndex": "发行期 {index}",
|
||||
"realEstate": "房地产",
|
||||
"snickers": "糖果",
|
||||
"next": "下一步",
|
||||
"back": "返回",
|
||||
"submit": "提交",
|
||||
"addStep": "添加发行期",
|
||||
"removeItem": "移除项目",
|
||||
"validation": {
|
||||
"nameRequired": "产品名称是必填项",
|
||||
"codeRequired": "产品编码是必填项",
|
||||
"categoryRequired": "产品类型是必填项",
|
||||
"editionNameRequired": "发行期名称是必填项",
|
||||
"launchDateRequired": "发行日期是必填项",
|
||||
"perUserLimitRequired": "个人申购上限是必填项",
|
||||
"totalSupplyRequired": "发行总量是必填项",
|
||||
"subscriptionDeadlineRequired": "申购截止日期是必填项",
|
||||
"unitPriceRequired": "单价是必填项",
|
||||
"dividendRateRequired": "分红率是必填项"
|
||||
},
|
||||
"done": {
|
||||
"title": "申请提交成功",
|
||||
"description": "您的发行申请已成功提交,我们将在工作日内对您的申请进行审核。请耐心等待审核结果。",
|
||||
"viewProducts": "查看我的产品"
|
||||
}
|
||||
}
|
||||
},
|
||||
"purchase": {
|
||||
"purchaseAsset": "购买资产",
|
||||
|
||||
@@ -54,6 +54,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
},
|
||||
{
|
||||
path: "/issue/issuing-apply",
|
||||
props: ({ query, params }) => ({ query, params }),
|
||||
component: () => import("@/views/issue/issuing-apply/index.vue"),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,3 +2,30 @@
|
||||
--background: var(--ion-color-primary-contrast);
|
||||
--min-height: 50px;
|
||||
}
|
||||
|
||||
.ui-select::part(label){
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ui-button {
|
||||
--border-radius: 8px;
|
||||
}
|
||||
|
||||
ion-datetime.ui-datetime {
|
||||
--background: rgb(255 255 255);
|
||||
--background-rgb: 255, 255, 255;
|
||||
--wheel-highlight-background: rgb(194 194 194);
|
||||
--wheel-fade-background-rgb: 255, 255, 255;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
ion-datetime.ui-datetime {
|
||||
--background: rgb(15, 15, 15);
|
||||
--background-rgb: 15, 15, 15;
|
||||
--wheel-highlight-background: rgb(50, 50, 50);
|
||||
--wheel-highlight-border-radius: 48px;
|
||||
--wheel-fade-background-rgb: 15, 15, 15;
|
||||
}
|
||||
}
|
||||
127
src/views/issue/issuing-apply/base.vue
Normal file
127
src/views/issue/issuing-apply/base.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<script lang='ts' setup>
|
||||
import type { GenericObject } from "vee-validate";
|
||||
import type { RwaIssuanceCategoriesData, RwaIssuanceProductBody } from "@/api/types";
|
||||
import { toTypedSchema } from "@vee-validate/yup";
|
||||
import { ErrorMessage, Field, FieldArray, Form } from "vee-validate";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import * as yup from "yup";
|
||||
import { client, safeClient } from "@/api";
|
||||
|
||||
const props = defineProps<{
|
||||
initialData: RwaIssuanceProductBody["product"];
|
||||
categories: RwaIssuanceCategoriesData;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: "next", values: GenericObject): void;
|
||||
}>();
|
||||
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")),
|
||||
}),
|
||||
);
|
||||
|
||||
function handleSubmit(values: GenericObject) {
|
||||
emit("next", values);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form
|
||||
:validation-schema="schema" :initial-values="initialData"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<Field name="name" type="text">
|
||||
<template #default="{ field }">
|
||||
<ui-input-label
|
||||
v-bind="field"
|
||||
:label="t('asset.issue.apply.productName')"
|
||||
:placeholder="t('asset.issue.apply.enterProductName')"
|
||||
/>
|
||||
</template>
|
||||
</Field>
|
||||
<ErrorMessage name="name" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="code" type="text">
|
||||
<template #default="{ field }">
|
||||
<ui-input-label
|
||||
v-bind="field"
|
||||
:label="t('asset.issue.apply.productCode')"
|
||||
:placeholder="t('asset.issue.apply.enterProductCode')"
|
||||
/>
|
||||
</template>
|
||||
</Field>
|
||||
<ErrorMessage name="code" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="categoryId" type="text">
|
||||
<template #default="{ field }">
|
||||
<ion-select class="ui-select" interface="action-sheet" toggle-icon="" v-bind="field" :label="t('asset.issue.apply.productType')" :placeholder="t('asset.issue.apply.chooseProductType')">
|
||||
<ion-select-option v-for="item in categories" :key="item.id" :value="item.id">
|
||||
{{ item.name }}
|
||||
</ion-select-option>
|
||||
</ion-select>
|
||||
</template>
|
||||
</Field>
|
||||
<ErrorMessage name="categoryId" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="estimatedValue" type="text">
|
||||
<template #default="{ field }">
|
||||
<ui-input-label
|
||||
v-bind="field"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
:label="t('asset.issue.apply.productValue')"
|
||||
:placeholder="t('asset.issue.apply.enterProductValue')"
|
||||
/>
|
||||
</template>
|
||||
</Field>
|
||||
<ErrorMessage name="estimatedValue" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="proofDocuments" type="text">
|
||||
<template #default="{ field }">
|
||||
<ui-input-label
|
||||
v-bind="field"
|
||||
:label="t('asset.issue.apply.assetProof')"
|
||||
:placeholder="t('asset.issue.apply.enterAssetProof')"
|
||||
/>
|
||||
</template>
|
||||
</Field>
|
||||
<ErrorMessage name="proofDocuments" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="totalSupplyLimit" type="text">
|
||||
<template #default="{ field }">
|
||||
<ui-input-label
|
||||
v-bind="field"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
:label="t('asset.issue.apply.totalSupplyLimit')"
|
||||
:placeholder="t('asset.issue.apply.enterTotalSupplyLimit')"
|
||||
/>
|
||||
</template>
|
||||
</Field>
|
||||
<ErrorMessage name="totalSupplyLimit" />
|
||||
</div>
|
||||
|
||||
<ion-button type="submit" expand="block">
|
||||
{{ t('asset.issue.apply.next') }}
|
||||
</ion-button>
|
||||
</div>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<style lang='css' scoped></style>
|
||||
27
src/views/issue/issuing-apply/done.vue
Normal file
27
src/views/issue/issuing-apply/done.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang='ts' setup>
|
||||
import { ribbonOutline } from "ionicons/icons";
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ui-result
|
||||
:icon="ribbonOutline"
|
||||
:title="t('asset.issue.apply.done.title')"
|
||||
:description="t('asset.issue.apply.done.description')"
|
||||
>
|
||||
<template #extra>
|
||||
<ion-button
|
||||
expand="block"
|
||||
color="primary"
|
||||
router-link="/issue/issuance-products"
|
||||
>
|
||||
{{ t('asset.issue.apply.done.viewProducts') }}
|
||||
</ion-button>
|
||||
</template>
|
||||
</ui-result>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang='css' scoped></style>
|
||||
@@ -1,14 +1,62 @@
|
||||
<script lang='ts' setup>
|
||||
import type { GenericObject } from "vee-validate";
|
||||
import type { RwaIssuanceProductBody } from "@/api/types";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { client, safeClient } from "@/api";
|
||||
import Base from "./base.vue";
|
||||
import Done from "./done.vue";
|
||||
import IssuePeriod from "./issue-period.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
const now = useNow();
|
||||
const { data: categories = [] } = await safeClient(client.api.rwa.issuance.categories.get());
|
||||
|
||||
const step = useRouteQuery<number>("step", 1, { transform: v => Number(v), mode: "push" });
|
||||
const initialData: RwaIssuanceProductBody = {
|
||||
product: {
|
||||
name: "",
|
||||
code: "",
|
||||
categoryId: categories!.length > 0 ? categories![0].id : "",
|
||||
},
|
||||
editions: [
|
||||
{
|
||||
editionName: "",
|
||||
launchDate: useDateFormat(now.value, "YYYY/MM/DD").value,
|
||||
perUserLimit: "",
|
||||
subscriptionDeadline: useDateFormat(now.value, "YYYY/MM/DD").value,
|
||||
totalSupply: "",
|
||||
unitPrice: "",
|
||||
dividendRate: "",
|
||||
},
|
||||
],
|
||||
};
|
||||
const form = useStorage<RwaIssuanceProductBody>("issuing-apply-form", { ...initialData });
|
||||
|
||||
function handleNext(values: GenericObject) {
|
||||
form.value.product = { ...values as any };
|
||||
step.value = 2;
|
||||
}
|
||||
|
||||
async function handleSubmit(editions: RwaIssuanceProductBody["editions"]) {
|
||||
form.value.editions = editions;
|
||||
await client.api.rwa.issuance.products.bundle.post(form.value);
|
||||
form.value = { ...initialData };
|
||||
step.value = 3;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<IonPage>
|
||||
<ion-header>
|
||||
<ion-toolbar class="ui-toolbar">
|
||||
<ion-title>Issuing Apply</ion-title>
|
||||
<ion-title>{{ t('asset.issue.apply.title') }}</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<IonContent :fullscreen="true" class="ion-padding" />
|
||||
<IonContent :fullscreen="true" class="ion-padding">
|
||||
<Base v-if="step === 1" :initial-data="form.product" :categories="categories || []" @next="handleNext" />
|
||||
<IssuePeriod v-else-if="step === 2" :initial-data="form.editions" @submit="handleSubmit" />
|
||||
<Done v-else />
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
</template>
|
||||
|
||||
173
src/views/issue/issuing-apply/issue-period.vue
Normal file
173
src/views/issue/issuing-apply/issue-period.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<script lang='ts' setup>
|
||||
import type { GenericObject } from "vee-validate";
|
||||
import type { RwaIssuanceProductBody } from "@/api/types";
|
||||
import { toTypedSchema } from "@vee-validate/yup";
|
||||
import { addOutline, removeOutline } from "ionicons/icons";
|
||||
import { ErrorMessage, Field, FieldArray, Form } from "vee-validate";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import * as yup from "yup";
|
||||
|
||||
const props = defineProps<{
|
||||
initialData: RwaIssuanceProductBody["editions"];
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: "submit", values: RwaIssuanceProductBody["editions"]): void;
|
||||
}>();
|
||||
const { t } = useI18n();
|
||||
const now = useNow();
|
||||
|
||||
const initialIssuePeriod: RwaIssuanceProductBody["editions"][0] = {
|
||||
editionName: "",
|
||||
launchDate: useDateFormat(now.value, "YYYY/MM/DD").value,
|
||||
perUserLimit: "",
|
||||
subscriptionDeadline: useDateFormat(now.value, "YYYY/MM/DD").value,
|
||||
totalSupply: "",
|
||||
unitPrice: "",
|
||||
dividendRate: "",
|
||||
};
|
||||
|
||||
const initialValues = ref({
|
||||
editions: props.initialData || [{ ...initialIssuePeriod }],
|
||||
});
|
||||
|
||||
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")),
|
||||
perUserLimit: yup.string().required(t("asset.issue.apply.validation.perUserLimitRequired")),
|
||||
totalSupply: yup.string().required(t("asset.issue.apply.validation.totalSupplyRequired")),
|
||||
subscriptionDeadline: yup.string().required(t("asset.issue.apply.validation.subscriptionDeadlineRequired")),
|
||||
unitPrice: yup.string().required(t("asset.issue.apply.validation.unitPriceRequired")),
|
||||
dividendRate: yup.string().required(t("asset.issue.apply.validation.dividendRateRequired")),
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
function handleSubmit(values: GenericObject) {
|
||||
emit("submit", values.editions);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form :validation-schema="schema" :initial-values="initialValues" class="space-y-5" @submit="handleSubmit">
|
||||
<FieldArray v-slot="{ fields, push, remove }" name="editions">
|
||||
<ui-collapse v-for="(entry, idx) in fields" :key="entry.key" :title="t('asset.issue.apply.issuePeriodIndex', { index: idx + 1 })" size="small">
|
||||
<div>
|
||||
<Field :name="`editions[${idx}].editionName`">
|
||||
<template #default="{ field }">
|
||||
<ui-input-label
|
||||
v-bind="field"
|
||||
:label="t('asset.issue.apply.editionName')"
|
||||
:placeholder="t('asset.issue.apply.enterEditionName')"
|
||||
/>
|
||||
</template>
|
||||
</Field>
|
||||
<ErrorMessage :name="`editions[${idx}].editionName`" />
|
||||
</div>
|
||||
<div>
|
||||
<Field :name="`editions[${idx}].launchDate`">
|
||||
<template #default="{ field }">
|
||||
<ui-datetime
|
||||
v-bind="field"
|
||||
:label="t('asset.issue.apply.launchDate')"
|
||||
/>
|
||||
</template>
|
||||
</Field>
|
||||
<ErrorMessage :name="`editions[${idx}].launchDate`" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field :name="`editions[${idx}].perUserLimit`" type="text">
|
||||
<template #default="{ field }">
|
||||
<ui-input-label
|
||||
v-bind="field"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
:label="t('asset.issue.apply.perUserLimit')"
|
||||
:placeholder="t('asset.issue.apply.enterPerUserLimit')"
|
||||
/>
|
||||
</template>
|
||||
</Field>
|
||||
<ErrorMessage :name="`editions[${idx}].perUserLimit`" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field :name="`editions[${idx}].totalSupply`" type="text">
|
||||
<template #default="{ field }">
|
||||
<ui-input-label
|
||||
v-bind="field"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
:label="t('asset.issue.apply.totalSupply')"
|
||||
:placeholder="t('asset.issue.apply.enterTotalSupply')"
|
||||
/>
|
||||
</template>
|
||||
</Field>
|
||||
<ErrorMessage :name="`editions[${idx}].totalSupply`" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field :name="`editions[${idx}].subscriptionDeadline`">
|
||||
<template #default="{ field }">
|
||||
<ui-datetime
|
||||
v-bind="field"
|
||||
:label="t('asset.issue.apply.subscriptionDeadline')"
|
||||
/>
|
||||
</template>
|
||||
</Field>
|
||||
<ErrorMessage :name="`editions[${idx}].subscriptionDeadline`" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field :name="`editions[${idx}].unitPrice`" type="text">
|
||||
<template #default="{ field }">
|
||||
<ui-input-label
|
||||
v-bind="field"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
:label="t('asset.issue.apply.unitPrice')"
|
||||
:placeholder="t('asset.issue.apply.enterUnitPrice')"
|
||||
/>
|
||||
</template>
|
||||
</Field>
|
||||
<ErrorMessage :name="`editions[${idx}].unitPrice`" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field :name="`editions[${idx}].dividendRate`" type="number">
|
||||
<template #default="{ field }">
|
||||
<ui-input-label
|
||||
v-bind="field"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
:label="t('asset.issue.apply.dividendRate')"
|
||||
:placeholder="t('asset.issue.apply.enterDividendRate')"
|
||||
/>
|
||||
</template>
|
||||
</Field>
|
||||
<ErrorMessage :name="`editions[${idx}].dividendRate`" />
|
||||
</div>
|
||||
|
||||
<ion-button color="tertiary" size="small" class="ui-button" @click="remove(idx)">
|
||||
<ion-icon slot="start" :icon="removeOutline" />
|
||||
<span>{{ t('asset.issue.apply.removeItem') }}</span>
|
||||
</ion-button>
|
||||
</ui-collapse>
|
||||
|
||||
<ion-button expand="block" color="tertiary" fill="outline" @click="push({ ...initialIssuePeriod })">
|
||||
<ion-icon slot="icon-only" :icon="addOutline" />
|
||||
<span>{{ t('asset.issue.apply.addStep') }}</span>
|
||||
</ion-button>
|
||||
</FieldArray>
|
||||
|
||||
<ion-button expand="block" @click="$router.back()">
|
||||
{{ t('asset.issue.apply.back') }}
|
||||
</ion-button>
|
||||
<ion-button expand="block" type="submit">
|
||||
{{ t('asset.issue.apply.submit') }}
|
||||
</ion-button>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<style lang='css' scoped></style>
|
||||
@@ -30,7 +30,6 @@ import WalletCard from "./components/wallet-card.vue";
|
||||
<WalletCard />
|
||||
<IssuingAsset />
|
||||
<PurchaseAsset />
|
||||
<AssetBalance />
|
||||
<MyRevenue />
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
|
||||
@@ -160,6 +160,7 @@ onMounted(() => {
|
||||
<ion-modal :keep-contents-mounted="true">
|
||||
<ion-datetime
|
||||
id="datetime"
|
||||
class="ui-datetime"
|
||||
done-text="Done"
|
||||
presentation="date"
|
||||
:value="userProfile?.birthday"
|
||||
@@ -187,21 +188,4 @@ ion-avatar {
|
||||
ion-item {
|
||||
--min-height: 60px;
|
||||
}
|
||||
ion-datetime {
|
||||
--background: rgb(255 255 255);
|
||||
--background-rgb: 255, 255, 255;
|
||||
--wheel-highlight-background: rgb(194 194 194);
|
||||
--wheel-fade-background-rgb: 255, 255, 255;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
ion-datetime {
|
||||
--background: rgb(15, 15, 15);
|
||||
--background-rgb: 15, 15, 15;
|
||||
--wheel-highlight-background: rgb(50, 50, 50);
|
||||
--wheel-highlight-border-radius: 48px;
|
||||
--wheel-fade-background-rgb: 15, 15, 15;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user