更新环境配置,修正服务地址;新增 RWA 产品管理相关组件及功能;优化表格数据展示及状态渲染逻辑

This commit is contained in:
2025-12-22 00:17:52 +07:00
parent 7423d210f4
commit 4c9cf042f0
10 changed files with 250 additions and 27 deletions

View File

@@ -108,6 +108,7 @@ defineExpose({
</TableHeaderOperation>
<NDataTable
:row-key="row => row.id"
:scroll-x="2000"
:columns="dataTableColumns"
:data="tableData"

View File

@@ -10,3 +10,9 @@ export enum DepositTypeEnum {
crypto = '链上充值',
fiat = '法币充值'
}
export enum RwaStatusEnum {
pending = '待审核',
approved = '已通过',
rejected = '已拒绝'
}

View File

@@ -232,7 +232,15 @@ const local: App.I18n.Schema = {
deposit: 'Deposit',
deposit_fiat: 'Fiat Deposit',
withdraw: 'Withdraw',
withdraw_fiat: 'Fiat Withdraw'
withdraw_fiat: 'Fiat Withdraw',
rwa_producttype: 'Product Type',
rwa: 'RWA Management',
rwa_product: 'RWA Product',
user: 'User Management',
user_bank: 'User Bank',
user_bankcard: 'User Bank Card',
user_list: 'User List',
user_transfer: 'User Transfer'
},
page: {
login: {

View File

@@ -23,7 +23,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
deposit_fiat: () => import("@/views/deposit/fiat/index.vue"),
home: () => import("@/views/home/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"),
user_bank: () => import("@/views/user/bank/index.vue"),
user_bankcard: () => import("@/views/user/bankcard/index.vue"),
user_list: () => import("@/views/user/list/index.vue"),

View File

@@ -1,8 +1,10 @@
import { ref } from 'vue';
import type { Ref, WatchSource } from 'vue';
import { ref, watch } from 'vue';
import { treaty } from '@elysiajs/eden';
import type { App } from '@riwa/api-types';
import { getServiceBaseURL } from '@/utils/service';
import { localStg } from '@/utils/storage';
import { $t } from '@/locales';
const isHttpProxy = import.meta.env.DEV && import.meta.env.VITE_HTTP_PROXY === 'Y';
const { baseURL } = getServiceBaseURL(import.meta.env, isHttpProxy);
@@ -14,37 +16,104 @@ const client = treaty<App>(baseURL, {
}
});
async function safeClient<T, E>(
requestFactory: () => Promise<{ data: T; error: E }>,
options: { silent?: boolean; immediate?: boolean } = {}
) {
const { immediate = true } = options;
const data = ref<T>();
const error = ref<E>();
export interface SafeClientOptions {
silent?: boolean;
immediate?: boolean;
watchSource?: WatchSource; // 用于监听的响应式数据源
}
const executeRequest = async () => {
const res = await requestFactory();
export interface SafeClientReturn<T, E> {
data: Ref<T | null>;
error: Ref<E | null>;
isPending: Ref<boolean>;
execute: () => Promise<void>;
onFetchResponse: (callback: (data: T, error: E) => void) => void;
stopWatching?: () => void;
}
if (res.error) {
export type RequestPromise<T, E> = Promise<{ data: T; error: E; status?: number; response?: Response }>;
export function safeClient<T, E>(
requestPromise: (() => RequestPromise<T, E>) | RequestPromise<T, E>,
options: SafeClientOptions = {}
): SafeClientReturn<T, E> & Promise<SafeClientReturn<T, E>> {
const { immediate = true, watchSource } = options;
const data = ref<T | null>(null);
const error = ref<E | null>(null);
const isPending = ref(false);
let responseCallback: ((data: T, error: E) => void) | null = null;
let stopWatcher: (() => void) | undefined;
const execute = async () => {
isPending.value = true;
let request: () => RequestPromise<T, E>;
if (typeof requestPromise !== 'function') {
request = () => Promise.resolve(requestPromise);
} else {
request = requestPromise;
}
const res = await request().finally(() => {
isPending.value = false;
});
if (res.error && res.status === 418) {
if (!options.silent) {
window.$message?.error('An error occurred while processing your request.');
const msg = $t((res.error as any).value.code, {
...(res.error as any).value.context
});
window.$message?.create(msg, {
type: 'error'
});
}
throw res.error;
}
data.value = res.data;
error.value = res.error;
// 调用注册的回调函数
if (responseCallback) {
responseCallback(res.data, res.error);
}
};
if (immediate) {
await executeRequest();
function onFetchResponse(callback: (data: T, error: E) => void) {
responseCallback = callback;
}
return {
data,
error,
refresh: executeRequest
function stopWatching() {
if (stopWatcher) {
stopWatcher();
stopWatcher = undefined;
}
}
// 如果提供了 watchSource则监听其变化
if (watchSource) {
stopWatcher = watch(
watchSource,
() => {
execute();
},
{ immediate: false } // 不立即执行,避免与 immediate 选项冲突
);
}
const result: SafeClientReturn<T, E> = {
data: data as Ref<T | null>,
error: error as Ref<E | null>,
isPending,
execute,
onFetchResponse,
stopWatching
};
const promise = immediate ? execute().then(() => result) : Promise.resolve(result);
Object.assign(promise, result);
return promise as SafeClientReturn<T, E> & Promise<SafeClientReturn<T, E>>;
}
export { client, safeClient };
export { client };

View File

@@ -22,4 +22,6 @@ declare namespace CommonType {
type RecordNullable<T> = {
[K in keyof T]?: T[K] | null;
};
type TreatyBody<T> = T extends (...args: any[]) => any ? NonNullable<Parameters<T>[0]> : never;
}

View File

@@ -47,6 +47,7 @@ declare module 'vue' {
NButton: typeof import('naive-ui')['NButton']
NCard: typeof import('naive-ui')['NCard']
NCheckbox: typeof import('naive-ui')['NCheckbox']
NCol: typeof import('naive-ui')['NCol']
NColorPicker: typeof import('naive-ui')['NColorPicker']
NDataTable: typeof import('naive-ui')['NDataTable']
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
@@ -71,6 +72,7 @@ declare module 'vue' {
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
NPopover: typeof import('naive-ui')['NPopover']
NRow: typeof import('naive-ui')['NRow']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSpace: typeof import('naive-ui')['NSpace']
@@ -81,6 +83,7 @@ declare module 'vue' {
NTabs: typeof import('naive-ui')['NTabs']
NThing: typeof import('naive-ui')['NThing']
NTooltip: typeof import('naive-ui')['NTooltip']
NUpload: typeof import('naive-ui')['NUpload']
NWatermark: typeof import('naive-ui')['NWatermark']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
@@ -135,6 +138,7 @@ declare global {
const NButton: typeof import('naive-ui')['NButton']
const NCard: typeof import('naive-ui')['NCard']
const NCheckbox: typeof import('naive-ui')['NCheckbox']
const NCol: typeof import('naive-ui')['NCol']
const NColorPicker: typeof import('naive-ui')['NColorPicker']
const NDataTable: typeof import('naive-ui')['NDataTable']
const NDialogProvider: typeof import('naive-ui')['NDialogProvider']
@@ -159,6 +163,7 @@ declare global {
const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
const NPopconfirm: typeof import('naive-ui')['NPopconfirm']
const NPopover: typeof import('naive-ui')['NPopover']
const NRow: typeof import('naive-ui')['NRow']
const NScrollbar: typeof import('naive-ui')['NScrollbar']
const NSelect: typeof import('naive-ui')['NSelect']
const NSpace: typeof import('naive-ui')['NSpace']
@@ -169,6 +174,7 @@ declare global {
const NTabs: typeof import('naive-ui')['NTabs']
const NThing: typeof import('naive-ui')['NThing']
const NTooltip: typeof import('naive-ui')['NTooltip']
const NUpload: typeof import('naive-ui')['NUpload']
const NWatermark: typeof import('naive-ui')['NWatermark']
const PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
const ReloadButton: typeof import('./../components/common/reload-button.vue')['default']

View File

@@ -0,0 +1,103 @@
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import { client, safeClient } from '@/service/api';
defineOptions({ name: 'RwaProductAdd' });
const emit = defineEmits<{
(e: 'close'): void;
}>();
type Body = CommonType.TreatyBody<typeof client.api.admin.rwa.issuance.products.post>;
const formInst = useTemplateRef<FormInst>('formInst');
const { data } = safeClient(
client.api.admin.rwa.category.categories.get({
query: {
pageIndex: 1,
pageSize: 9999
}
})
);
const form = ref<Body>({
name: '',
code: '',
categoryId: '',
description: ''
});
const rules: FormRules = {
name: [{ required: true, message: '请输入产品名称', trigger: ['blur', 'input'] }],
code: [{ required: true, message: '请输入产品编号', trigger: ['blur', 'input'] }],
categoryId: [{ required: true, message: '请输入产品类型', trigger: ['blur', 'input'] }],
estimatedValue: [{ required: true, message: '请输入产品估值', trigger: ['blur', 'input'] }],
totalSupplyLimit: [{ required: true, message: '请输入总发行量', trigger: ['blur', 'input'] }]
};
function handleSubmit() {
formInst.value?.validate(async errors => {
if (!errors) {
await safeClient(
client.api.admin.rwa.issuance.products.post({
...form.value
})
);
emit('close');
}
});
}
</script>
<template>
<div class="my-10">
<NForm
ref="formInst"
:model="form"
label-width="auto"
label-placement="left"
:rules="rules"
require-mark-placement="left"
>
<NFormItem path="name" label="产品名称">
<NInput v-model:value="form.name" />
</NFormItem>
<NFormItem path="code" label="产品编号">
<NInput v-model:value="form.code" />
</NFormItem>
<NFormItem path="categoryId" label="产品类型">
<NSelect
:value="form.categoryId || null"
:options="data?.data?.map(item => ({ label: item.name, value: item.id }))"
@update:value="val => (form.categoryId = val as string)"
/>
</NFormItem>
<NFormItem path="estimatedValue" label="产品估值">
<NInput v-model:value="form.estimatedValue" />
</NFormItem>
<NFormItem path="totalSupplyLimit" label="总发行量">
<NInput v-model:value="form.totalSupplyLimit" />
</NFormItem>
<NFormItem path="description" label="产品描述">
<NInput v-model:value="form.description" type="textarea" />
</NFormItem>
<NFormItem path="proofDocuments" label="资产证明 ">
<NUpload
action="https://naive-upload.free.beeceptor.com/"
:headers="{ 'naive-info': 'hello!' }"
:data="{
'naive-data': 'cool! naive!'
}"
>
<NButton>上传文件</NButton>
</NUpload>
</NFormItem>
<NSpace justify="end">
<NButton @click="$emit('close')"> </NButton>
<NButton type="primary" @click="handleSubmit"> </NButton>
</NSpace>
</NForm>
</div>
</template>
<style lang="css" scoped></style>

View File

@@ -1,8 +1,11 @@
<script lang="ts" setup>
import { useTemplateRef } from 'vue';
import { h, useTemplateRef } from 'vue';
import { useDateFormat } from '@vueuse/core';
import { NDatePicker, useDialog, useMessage } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
import { RwaStatusEnum } from '@/enum';
import Add from './components/add.vue';
const dialog = useDialog();
const message = useMessage();
@@ -21,8 +24,10 @@ const fetchData: TableFetchData = ({ pagination, filter }) => {
const columns: TableBaseColumns = [
{
title: 'ID',
key: 'id'
key: 'selection',
title: '序号',
type: 'selection',
width: 60
},
{
title: '产品代码',
@@ -48,12 +53,15 @@ const columns: TableBaseColumns = [
title: '创建时间',
key: 'createdAt',
render: (row: any) => {
return new Date(row.createdAt).toLocaleDateString();
return useDateFormat(row.createdAt, 'YYYY-MM-DD HH:mm').value;
}
},
{
title: '状态',
key: 'status'
key: 'status',
render: row => {
return RwaStatusEnum[row.status as keyof typeof RwaStatusEnum];
}
},
{
title: '描述',
@@ -119,6 +127,25 @@ const filterColumns: TableFilterColumns = [
component: NDatePicker
}
];
function handleAdd() {
const dialogInstance = dialog.create({
title: '添加产品',
content: () =>
h(Add, {
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
}),
style: { width: '600px' },
showIcon: false,
onPositiveClick: () => {
message.success('添加成功');
tableInst.value?.reload();
}
});
}
</script>
<template>
@@ -128,6 +155,7 @@ const filterColumns: TableFilterColumns = [
:columns="columns"
:filter-columns="filterColumns"
:fetch-data="fetchData"
@add="handleAdd"
/>
</template>