feat: 新增代币化管理功能,包括代币化产品的添加、编辑和列表展示

This commit is contained in:
2026-01-07 16:16:55 +07:00
parent 5f9d639599
commit 0e20480565
12 changed files with 412 additions and 96 deletions

View File

@@ -1,5 +1,5 @@
# backend service base url, test environment # backend service base url, test environment
VITE_SERVICE_BASE_URL=http://192.168.1.6:9528 VITE_SERVICE_BASE_URL=http://192.168.1.17:9528
# other backend service base url, test environment # other backend service base url, test environment
VITE_OTHER_SERVICE_BASE_URL= `{}` VITE_OTHER_SERVICE_BASE_URL= `{}`

View File

@@ -50,7 +50,7 @@
"@better-scroll/core": "2.5.1", "@better-scroll/core": "2.5.1",
"@elysiajs/eden": "^1.4.5", "@elysiajs/eden": "^1.4.5",
"@iconify/vue": "5.0.0", "@iconify/vue": "5.0.0",
"@riwa/api-types": "http://192.168.1.8:9527/api/riwa-api-types-0.0.74.tgz", "@riwa/api-types": "http://192.168.1.17:9527/api/riwa-eden-0.0.81.tgz",
"@sa/axios": "workspace:*", "@sa/axios": "workspace:*",
"@sa/color": "workspace:*", "@sa/color": "workspace:*",
"@sa/hooks": "workspace:*", "@sa/hooks": "workspace:*",

12
pnpm-lock.yaml generated
View File

@@ -18,8 +18,8 @@ importers:
specifier: 5.0.0 specifier: 5.0.0
version: 5.0.0(vue@3.5.25(typescript@5.9.3)) version: 5.0.0(vue@3.5.25(typescript@5.9.3))
'@riwa/api-types': '@riwa/api-types':
specifier: http://192.168.1.8:9527/api/riwa-api-types-0.0.74.tgz specifier: http://192.168.1.17:9527/api/riwa-eden-0.0.81.tgz
version: http://192.168.1.8:9527/api/riwa-api-types-0.0.74.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3))) version: '@riwa/eden@http://192.168.1.17:9527/api/riwa-eden-0.0.81.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))'
'@sa/axios': '@sa/axios':
specifier: workspace:* specifier: workspace:*
version: link:packages/axios version: link:packages/axios
@@ -1083,9 +1083,9 @@ packages:
'@quansync/fs@0.1.6': '@quansync/fs@0.1.6':
resolution: {integrity: sha512-zoA8SqQO11qH9H8FCBR7NIbowYARIPmBz3nKjgAaOUDi/xPAAu1uAgebtV7KXHTc6CDZJVRZ1u4wIGvY5CWYaw==} resolution: {integrity: sha512-zoA8SqQO11qH9H8FCBR7NIbowYARIPmBz3nKjgAaOUDi/xPAAu1uAgebtV7KXHTc6CDZJVRZ1u4wIGvY5CWYaw==}
'@riwa/api-types@http://192.168.1.8:9527/api/riwa-api-types-0.0.74.tgz': '@riwa/eden@http://192.168.1.17:9527/api/riwa-eden-0.0.81.tgz':
resolution: {tarball: http://192.168.1.8:9527/api/riwa-api-types-0.0.74.tgz} resolution: {tarball: http://192.168.1.17:9527/api/riwa-eden-0.0.81.tgz}
version: 0.0.74 version: 0.0.81
peerDependencies: peerDependencies:
'@elysiajs/eden': ^1.4.5 '@elysiajs/eden': ^1.4.5
@@ -5080,7 +5080,7 @@ snapshots:
dependencies: dependencies:
quansync: 0.3.0 quansync: 0.3.0
'@riwa/api-types@http://192.168.1.8:9527/api/riwa-api-types-0.0.74.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))': '@riwa/eden@http://192.168.1.17:9527/api/riwa-eden-0.0.81.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))':
dependencies: dependencies:
'@elysiajs/eden': 1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)) '@elysiajs/eden': 1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3))

View File

@@ -245,7 +245,9 @@ const local: App.I18n.Schema = {
transfer: 'Transfer', transfer: 'Transfer',
withdraw_approved: 'Approved Withdraw', withdraw_approved: 'Approved Withdraw',
asset: 'Asset Management', asset: 'Asset Management',
tradingpairs: 'Trading Pairs Management' tradingpairs: 'Trading Pairs Management',
tokenization: 'Tokenization Management',
tokenization_product: 'Tokenization Product'
}, },
page: { page: {
login: { login: {

View File

@@ -241,7 +241,9 @@ const local: App.I18n.Schema = {
user_transfer: '用户转账记录', user_transfer: '用户转账记录',
transfer: '转账管理', transfer: '转账管理',
asset: '资产管理', asset: '资产管理',
tradingpairs: '交易对管理' tradingpairs: '交易对管理',
tokenization: '代币化管理',
tokenization_product: '代币化产品'
}, },
page: { page: {
login: { login: {

View File

@@ -26,6 +26,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
rwa_product: () => import("@/views/rwa/product/index.vue"), rwa_product: () => import("@/views/rwa/product/index.vue"),
rwa_producttype: () => import("@/views/rwa/productType/index.vue"), rwa_producttype: () => import("@/views/rwa/productType/index.vue"),
rwa_subscribe: () => import("@/views/rwa/subscribe/index.vue"), rwa_subscribe: () => import("@/views/rwa/subscribe/index.vue"),
tokenization_product: () => import("@/views/tokenization/product/index.vue"),
tradingpairs: () => import("@/views/tradingPairs/index.vue"), tradingpairs: () => import("@/views/tradingPairs/index.vue"),
transfer: () => import("@/views/transfer/index.vue"), transfer: () => import("@/views/transfer/index.vue"),
user_bank: () => import("@/views/user/bank/index.vue"), user_bank: () => import("@/views/user/bank/index.vue"),

View File

@@ -39,6 +39,17 @@ export const generatedRoutes: GeneratedRoute[] = [
hideInMenu: true hideInMenu: true
} }
}, },
{
name: 'home',
path: '/home',
component: 'layout.base$view.home',
meta: {
title: 'home',
i18nKey: 'route.home',
icon: 'mdi:monitor-dashboard',
order: 1
}
},
{ {
name: 'asset', name: 'asset',
path: '/asset', path: '/asset',
@@ -46,8 +57,66 @@ export const generatedRoutes: GeneratedRoute[] = [
meta: { meta: {
title: 'asset', title: 'asset',
i18nKey: 'route.asset', i18nKey: 'route.asset',
order: 5 order: 2
} }
},
{
name: 'transfer',
path: '/transfer',
component: 'layout.base$view.transfer',
meta: {
title: 'transfer',
i18nKey: 'route.transfer',
order: 3
}
},
{
name: 'user',
path: '/user',
component: 'layout.base',
meta: {
title: 'user',
i18nKey: 'route.user',
order: 4
},
children: [
{
name: 'user_bank',
path: '/user/bank',
component: 'view.user_bank',
meta: {
title: 'user_bank',
i18nKey: 'route.user_bank'
}
},
{
name: 'user_bankcard',
path: '/user/bankcard',
component: 'view.user_bankcard',
meta: {
title: 'user_bankcard',
i18nKey: 'route.user_bankcard'
}
},
{
name: 'user_list',
path: '/user/list',
component: 'view.user_list',
meta: {
title: 'user_list',
i18nKey: 'route.user_list'
}
},
{
name: 'user_transfer',
path: '/user/transfer',
component: 'view.user_transfer',
meta: {
title: 'user_transfer',
i18nKey: 'route.user_transfer'
}
}
]
}, },
{ {
name: 'deposit', name: 'deposit',
@@ -56,7 +125,7 @@ export const generatedRoutes: GeneratedRoute[] = [
meta: { meta: {
title: 'deposit', title: 'deposit',
i18nKey: 'route.deposit', i18nKey: 'route.deposit',
order: 2 order: 5
}, },
children: [ children: [
{ {
@@ -70,17 +139,6 @@ export const generatedRoutes: GeneratedRoute[] = [
} }
] ]
}, },
{
name: 'home',
path: '/home',
component: 'layout.base$view.home',
meta: {
title: 'home',
i18nKey: 'route.home',
icon: 'mdi:monitor-dashboard',
order: 1
}
},
{ {
name: 'iframe-page', name: 'iframe-page',
path: '/iframe-page/:url', path: '/iframe-page/:url',
@@ -145,6 +203,27 @@ export const generatedRoutes: GeneratedRoute[] = [
} }
] ]
}, },
{
name: 'tokenization',
path: '/tokenization',
component: 'layout.base',
meta: {
title: '代币化管理',
i18nKey: 'route.tokenization',
order: 6
},
children: [
{
name: 'tokenization_product',
path: '/tokenization/product',
component: 'view.tokenization_product',
meta: {
title: '代币化产品',
i18nKey: 'route.tokenization_product'
}
}
]
},
{ {
name: 'tradingpairs', name: 'tradingpairs',
path: '/tradingpairs', path: '/tradingpairs',
@@ -152,67 +231,11 @@ export const generatedRoutes: GeneratedRoute[] = [
meta: { meta: {
title: 'tradingpairs', title: 'tradingpairs',
i18nKey: 'route.tradingpairs', i18nKey: 'route.tradingpairs',
order: 6 order: 7
} }
}, },
{
name: 'transfer',
path: '/transfer',
component: 'layout.base$view.transfer',
meta: {
title: 'transfer',
i18nKey: 'route.transfer',
order: 5
}
},
{
name: 'user',
path: '/user',
component: 'layout.base',
meta: {
title: 'user',
i18nKey: 'route.user',
order: 2
},
children: [
{
name: 'user_bank',
path: '/user/bank',
component: 'view.user_bank',
meta: {
title: 'user_bank',
i18nKey: 'route.user_bank'
}
},
{
name: 'user_bankcard',
path: '/user/bankcard',
component: 'view.user_bankcard',
meta: {
title: 'user_bankcard',
i18nKey: 'route.user_bankcard'
}
},
{
name: 'user_list',
path: '/user/list',
component: 'view.user_list',
meta: {
title: 'user_list',
i18nKey: 'route.user_list'
}
},
{
name: 'user_transfer',
path: '/user/transfer',
component: 'view.user_transfer',
meta: {
title: 'user_transfer',
i18nKey: 'route.user_transfer'
}
}
]
},
{ {
name: 'withdraw', name: 'withdraw',
path: '/withdraw', path: '/withdraw',

View File

@@ -176,6 +176,8 @@ const routeMap: RouteMap = {
"rwa_product": "/rwa/product", "rwa_product": "/rwa/product",
"rwa_producttype": "/rwa/producttype", "rwa_producttype": "/rwa/producttype",
"rwa_subscribe": "/rwa/subscribe", "rwa_subscribe": "/rwa/subscribe",
"tokenization": "/tokenization",
"tokenization_product": "/tokenization/product",
"tradingpairs": "/tradingpairs", "tradingpairs": "/tradingpairs",
"transfer": "/transfer", "transfer": "/transfer",
"user": "/user", "user": "/user",

View File

@@ -30,6 +30,8 @@ declare module "@elegant-router/types" {
"rwa_product": "/rwa/product"; "rwa_product": "/rwa/product";
"rwa_producttype": "/rwa/producttype"; "rwa_producttype": "/rwa/producttype";
"rwa_subscribe": "/rwa/subscribe"; "rwa_subscribe": "/rwa/subscribe";
"tokenization": "/tokenization";
"tokenization_product": "/tokenization/product";
"tradingpairs": "/tradingpairs"; "tradingpairs": "/tradingpairs";
"transfer": "/transfer"; "transfer": "/transfer";
"user": "/user"; "user": "/user";
@@ -80,6 +82,7 @@ declare module "@elegant-router/types" {
| "iframe-page" | "iframe-page"
| "login" | "login"
| "rwa" | "rwa"
| "tokenization"
| "tradingpairs" | "tradingpairs"
| "transfer" | "transfer"
| "user" | "user"
@@ -111,6 +114,7 @@ declare module "@elegant-router/types" {
| "rwa_product" | "rwa_product"
| "rwa_producttype" | "rwa_producttype"
| "rwa_subscribe" | "rwa_subscribe"
| "tokenization_product"
| "tradingpairs" | "tradingpairs"
| "transfer" | "transfer"
| "user_bank" | "user_bank"

View File

@@ -0,0 +1,214 @@
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import { type FormInst, type FormRules, NInput, useDialog } from 'naive-ui';
import type { Treaty } from '@elysiajs/eden';
import { client, safeClient } from '@/service/api';
defineOptions({ name: 'RwaProductTokenization' });
type Data = Treaty.Data<typeof client.api.admin.rwa.issuance.products.get>['data'][number];
type Body = CommonType.TreatyBody<typeof client.api.admin.rwa.tokenization_schema.issue.post>;
const props = defineProps<{
data: Data;
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
const formRef = useTemplateRef<FormInst | null>('formRef');
const form = ref<Body>({
productId: props.data.id,
assetCode: props.data.code,
totalSupply: '100',
lockOptions: [
{
months: 6,
rewardRate: '0.05' // 奖励率(如 0.05 = 5%
}
]
});
const rules: FormRules = {
assetCode: [
{
required: true,
message: '请选择资产代码',
trigger: 'change'
}
],
totalSupply: [
{
required: true,
message: '请输入发行总量',
trigger: ['blur', 'change']
},
{
validator: (_rule, value) => {
if (Number(value) < 1) {
return new Error('发行总量必须大于0');
}
return true;
},
trigger: ['blur', 'change']
}
]
};
function addLockOption() {
(form.value.lockOptions ||= []).push({
months: 12,
rewardRate: '0.1'
});
}
function removeLockOption(index: number) {
if (form.value.lockOptions && form.value.lockOptions.length > 1) {
form.value.lockOptions.splice(index, 1);
}
}
function handleSubmit() {
formRef.value?.validate(async errors => {
if (!errors) {
await safeClient(() =>
client.api.admin.rwa.tokenization_schema.issue.post({
...form.value
})
);
window.$message?.success('代币化产品发行请求已提交。');
emit('close');
}
});
}
</script>
<template>
<NForm
ref="formRef"
:model="form"
:rules="rules"
label-width="auto"
label-placement="left"
require-mark-placement="left"
>
<NGrid :cols="1">
<NFormItemGi label="资产代码" path="assetCode">
<NInput v-model:value="form.assetCode" placeholder="请输入资产代码" />
</NFormItemGi>
<NFormItemGi label="发行总量" path="totalSupply">
<NInputNumber
:min="1"
:step="100"
:value="Number(form.totalSupply)"
@update:value="val => (form.totalSupply = String(val))"
/>
</NFormItemGi>
</NGrid>
<NDivider />
<div class="mb-4 flex items-center justify-between">
<span class="text-base font-medium">锁仓选项</span>
<NButton secondary type="primary" @click="addLockOption">
<template #icon>
<icon-ic-round-plus class="text-icon" />
</template>
添加选项
</NButton>
</div>
<NSpace vertical :size="16">
<NCard
v-for="(option, index) in form.lockOptions"
:key="index"
size="small"
:segmented="{ content: true }"
class="rounded-8px"
>
<template #header>
<div class="flex items-center justify-between">
<span>选项 {{ index + 1 }}</span>
<NButton
v-if="form.lockOptions && form.lockOptions.length > 1"
text
type="error"
@click="removeLockOption(index)"
>
<template #icon>
<icon-ic-round-delete class="text-icon" />
</template>
</NButton>
</div>
</template>
<NGrid :cols="2" :x-gap="16">
<NFormItemGi
label="锁仓月数"
:path="`lockOptions[${index}].months`"
:rule="{
required: true,
type: 'number',
message: '请输入锁仓月数',
trigger: ['blur', 'change']
}"
>
<NInputNumber
v-model:value="option.months"
:min="1"
:max="120"
:step="1"
placeholder="请输入锁仓月数"
class="w-full"
>
<template #suffix></template>
</NInputNumber>
</NFormItemGi>
<NFormItemGi
label="奖励率"
:path="`lockOptions[${index}].rewardRate`"
:rule="{
required: true,
validator: (_rule: any, value: string) => {
const num = Number(value);
if (!value || value === '') {
return new Error('请输入奖励率');
}
if (Number.isNaN(num)) {
return new Error('请输入有效的数字');
}
if (num < 0 || num > 1) {
return new Error('奖励率必须在0-1之间');
}
return true;
},
trigger: ['blur', 'change']
}"
>
<NInputNumber
:value="Number(option.rewardRate)"
:min="0"
:max="1"
:step="0.01"
:precision="4"
placeholder="请输入奖励率"
class="w-full"
@update:value="val => (option.rewardRate = String(val))"
>
<template #suffix>
<span class="text-12px">{{ (Number(option.rewardRate) * 100).toFixed(2) }}%</span>
</template>
</NInputNumber>
</NFormItemGi>
</NGrid>
</NCard>
</NSpace>
<NSpace justify="end" class="mt-4">
<NButton @click="$emit('close')">取消</NButton>
<NButton type="primary" @click="handleSubmit">立即代币化</NButton>
</NSpace>
</NForm>
</template>
<style lang="css" scoped></style>

View File

@@ -8,6 +8,7 @@ import { RwaStatusEnum } from '@/enum';
import Add from './components/add.vue'; import Add from './components/add.vue';
import Edit from './components/edit.vue'; import Edit from './components/edit.vue';
import Editions from './components/editions.vue'; import Editions from './components/editions.vue';
import Tokenization from './components/tokenization.vue';
const dialog = useDialog(); const dialog = useDialog();
const message = useMessage(); const message = useMessage();
@@ -214,21 +215,18 @@ const filterColumns: TableFilterColumns = [
]; ];
function handleTokenization(row: any) { function handleTokenization(row: any) {
dialog.create({ const dialogInstance = dialog.create({
title: '代币化产品', title: '代币化产品',
content: '确认将该产品代币化吗?', content: () =>
positiveText: '确认', h(Tokenization, {
negativeText: '取消', data: row,
onPositiveClick: async () => { onClose: () => {
await safeClient(() => dialogInstance.destroy();
client.api.admin.rwa.tokenization.issue.post({
assetCode: row.code,
productId: row.id,
totalSupply: row.estimatedValue
})
);
tableInst.value?.reload(); tableInst.value?.reload();
} }
}),
style: { width: '600px' },
showIcon: false
}); });
} }

View File

@@ -0,0 +1,70 @@
<script lang="ts" setup>
import { useTemplateRef } from 'vue';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.rwa.tokenization_schema.configs.get({
query: {
...pagination,
...filter
}
})
);
};
const columns: TableBaseColumns = [
{
key: 'selection',
title: '序号',
type: 'selection',
width: 60
},
{
title: '资产代码',
key: 'assetCode'
},
{
title: '发行总量',
key: 'totalSupply'
},
{
title: '锁定选项',
key: 'lockOptions',
render: (row: any) => {
return row.lockOptions
.map((option: any) => `锁定${option.months}个月,奖励率${(Number(option.rewardRate) * 100).toFixed(2)}%`)
.join(' ');
}
},
{
title: '关联RWA产品',
key: 'productId'
},
{
title: '创建时间',
key: 'createdAt'
}
];
const filterColumns: TableFilterColumns = [];
</script>
<template>
<TableBase
ref="tableInst"
:columns="columns"
:filter-columns="filterColumns"
:fetch-data="fetchData"
:header-operations="{
add: false,
refresh: true,
columns: true
}"
/>
</template>
<style lang="css" scoped></style>