feat: init

This commit is contained in:
2026-01-18 18:17:18 +07:00
parent dfd4074ff4
commit 724f0a47e9
48 changed files with 6 additions and 6946 deletions

View File

@@ -1,213 +0,0 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import type { FormInst, FormItemRule } from 'naive-ui';
import { NButton, NForm, NFormItem, NFormItemGi, NGrid, NInput, NInputNumber, NSpace, NSwitch } from 'naive-ui';
import { client, safeClient } from '@/service/api';
type Asset = CommonType.TreatyBody<typeof client.api.admin.assets.post>;
defineOptions({
name: 'AssetEdit'
});
const emit = defineEmits<{
close: [];
}>();
const formRef = ref<FormInst | null>(null);
const loading = ref(false);
const formModel = reactive<Asset>({
code: '',
name: '',
iconUrl: '',
maxWithdrawPerDay: '',
maxWithdrawPerTx: '',
minDeposit: '',
minWithdraw: '',
withdrawFeeFixed: '',
withdrawFeeRate: '',
tradeEnabled: false,
transferEnabled: false,
withdrawEnabled: false,
depositEnabled: false,
isActive: false,
isRwaAsset: false,
sortOrder: 1
});
// 用于表单输入的临时数字值
const tempValues = ref({
maxWithdrawPerDay: 0,
maxWithdrawPerTx: 0,
minDeposit: 0,
minWithdraw: 0,
withdrawFeeFixed: 0,
withdrawFeeRate: 0, // 转换为百分比
sortOrder: 0
});
const rules: Record<string, FormItemRule | FormItemRule[]> = {
code: { required: true, message: '请输入资产代码', trigger: 'blur' },
name: { required: true, message: '请输入资产名称', trigger: 'blur' },
iconUrl: { required: true, message: '请输入图标地址', trigger: 'blur' },
minDeposit: { required: true, message: '请输入最小充值金额', trigger: 'blur' },
minWithdraw: { required: true, message: '请输入最小提现金额', trigger: 'blur' },
withdrawFeeFixed: { required: true, message: '请输入提现固定手续费', trigger: 'blur' },
withdrawFeeRate: { required: true, message: '请输入提现手续费率', trigger: 'blur' }
};
// 同步数字输入到表单字符串
function syncNumberToString() {
formModel.maxWithdrawPerDay = tempValues.value.maxWithdrawPerDay.toString();
formModel.maxWithdrawPerTx = tempValues.value.maxWithdrawPerTx.toString();
formModel.minDeposit = tempValues.value.minDeposit.toString();
formModel.minWithdraw = tempValues.value.minWithdraw.toString();
formModel.withdrawFeeFixed = tempValues.value.withdrawFeeFixed.toString();
formModel.withdrawFeeRate = (tempValues.value.withdrawFeeRate / 100).toString(); // 转回小数
formModel.sortOrder = tempValues.value.sortOrder;
}
async function handleSubmit() {
syncNumberToString();
await formRef.value?.validate();
loading.value = true;
await safeClient(() =>
client.api.admin.assets.post({
...formModel
})
);
loading.value = false;
window.$message?.success('更新成功');
emit('close');
}
function handleCancel() {
emit('close');
}
</script>
<template>
<div class="my-10">
<NForm ref="formRef" :model="formModel" :rules="rules" label-placement="left" :label-width="140">
<NGrid :cols="2" :x-gap="12">
<NFormItemGi label="资产代码" path="code">
<NInput v-model:value="formModel.code" placeholder="请输入资产代码" />
</NFormItemGi>
<NFormItemGi label="资产名称" path="name">
<NInput v-model:value="formModel.name" placeholder="请输入资产名称" />
</NFormItemGi>
</NGrid>
<NFormItem label="图标地址" path="iconUrl">
<IconPicker v-model="formModel.iconUrl" :collections="['cryptocurrency-color']" />
</NFormItem>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi label="每日最大提现金额" path="maxWithdrawPerDay">
<NInputNumber
v-model:value="tempValues.maxWithdrawPerDay"
placeholder="请输入每日最大提现金额"
:min="0"
:precision="2"
class="w-full"
/>
</NFormItemGi>
<NFormItemGi label="单笔最大提现金额" path="maxWithdrawPerTx">
<NInputNumber
v-model:value="tempValues.maxWithdrawPerTx"
placeholder="请输入单笔最大提现金额"
:min="0"
:precision="2"
class="w-full"
/>
</NFormItemGi>
</NGrid>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi label="最小充值金额" path="minDeposit">
<NInputNumber
v-model:value="tempValues.minDeposit"
placeholder="请输入最小充值金额"
:min="0"
:precision="2"
class="w-full"
/>
</NFormItemGi>
<NFormItemGi label="最小提现金额" path="minWithdraw">
<NInputNumber
v-model:value="tempValues.minWithdraw"
placeholder="请输入最小提现金额"
:min="0"
:precision="2"
class="w-full"
/>
</NFormItemGi>
</NGrid>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi label="提现固定手续费" path="withdrawFeeFixed">
<NInputNumber
v-model:value="tempValues.withdrawFeeFixed"
placeholder="请输入提现固定手续费"
:min="0"
:precision="2"
class="w-full"
/>
</NFormItemGi>
<NFormItemGi label="提现手续费率" path="withdrawFeeRate">
<NInputNumber
v-model:value="tempValues.withdrawFeeRate"
placeholder="请输入提现手续费率"
:min="0"
:max="100"
:precision="4"
class="w-full"
>
<template #suffix>%</template>
</NInputNumber>
</NFormItemGi>
</NGrid>
<NFormItem label="排序" path="sortOrder">
<NInputNumber v-model:value="tempValues.sortOrder" placeholder="请输入排序" :min="0" class="w-full" />
</NFormItem>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi label="是否可交易">
<NSwitch v-model:value="formModel.tradeEnabled" />
</NFormItemGi>
<NFormItemGi label="是否可转账">
<NSwitch v-model:value="formModel.transferEnabled" />
</NFormItemGi>
<NFormItemGi label="是否可提现">
<NSwitch v-model:value="formModel.withdrawEnabled" />
</NFormItemGi>
<NFormItemGi label="是否可充值">
<NSwitch v-model:value="formModel.depositEnabled" />
</NFormItemGi>
<NFormItemGi label="是否启用">
<NSwitch v-model:value="formModel.isActive" />
</NFormItemGi>
<NFormItemGi label="是否为 RWA 资产">
<NSwitch v-model:value="formModel.isRwaAsset" />
</NFormItemGi>
</NGrid>
<NSpace justify="end">
<NButton type="primary" ghost @click="handleCancel">取消</NButton>
<NButton type="primary" :loading="loading" @click="handleSubmit">创建</NButton>
</NSpace>
</NForm>
</div>
</template>
<style lang="css" scoped></style>

View File

@@ -1,218 +0,0 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import type { FormInst, FormItemRule } from 'naive-ui';
import { NButton, NForm, NFormItem, NFormItemGi, NGrid, NInput, NInputNumber, NSpace, NSwitch } from 'naive-ui';
import { client, safeClient } from '@/service/api';
type Asset = CommonType.TreatyBody<typeof client.api.admin.assets.post>;
defineOptions({
name: 'AssetEdit'
});
const props = defineProps<{
data: Asset;
}>();
const emit = defineEmits<{
close: [];
}>();
const formRef = ref<FormInst | null>(null);
const loading = ref(false);
const formModel = reactive<Partial<Asset>>({
code: props.data.code,
name: props.data.name,
iconUrl: props.data.iconUrl,
maxWithdrawPerDay: props.data.maxWithdrawPerDay,
maxWithdrawPerTx: props.data.maxWithdrawPerTx,
minDeposit: props.data.minDeposit,
minWithdraw: props.data.minWithdraw,
withdrawFeeFixed: props.data.withdrawFeeFixed,
withdrawFeeRate: props.data.withdrawFeeRate,
tradeEnabled: props.data.tradeEnabled,
transferEnabled: props.data.transferEnabled,
withdrawEnabled: props.data.withdrawEnabled,
depositEnabled: props.data.depositEnabled,
isActive: props.data.isActive,
isRwaAsset: props.data.isRwaAsset,
sortOrder: props.data.sortOrder
});
// 用于表单输入的临时数字值
const tempValues = ref({
maxWithdrawPerDay: Number(props.data.maxWithdrawPerDay) || 0,
maxWithdrawPerTx: Number(props.data.maxWithdrawPerTx) || 0,
minDeposit: Number(props.data.minDeposit) || 0,
minWithdraw: Number(props.data.minWithdraw) || 0,
withdrawFeeFixed: Number(props.data.withdrawFeeFixed) || 0,
withdrawFeeRate: Number(props.data.withdrawFeeRate) * 100 || 0, // 转换为百分比
sortOrder: Number(props.data.sortOrder) || 0
});
const rules: Record<string, FormItemRule | FormItemRule[]> = {
code: { required: true, message: '请输入资产代码', trigger: 'blur' },
name: { required: true, message: '请输入资产名称', trigger: 'blur' },
iconUrl: { required: true, message: '请输入图标地址', trigger: 'blur' },
minDeposit: { required: true, message: '请输入最小充值金额', trigger: 'blur' },
minWithdraw: { required: true, message: '请输入最小提现金额', trigger: 'blur' },
withdrawFeeFixed: { required: true, message: '请输入提现固定手续费', trigger: 'blur' },
withdrawFeeRate: { required: true, message: '请输入提现手续费率', trigger: 'blur' }
};
// 同步数字输入到表单字符串
function syncNumberToString() {
formModel.maxWithdrawPerDay = tempValues.value.maxWithdrawPerDay.toString();
formModel.maxWithdrawPerTx = tempValues.value.maxWithdrawPerTx.toString();
formModel.minDeposit = tempValues.value.minDeposit.toString();
formModel.minWithdraw = tempValues.value.minWithdraw.toString();
formModel.withdrawFeeFixed = tempValues.value.withdrawFeeFixed.toString();
formModel.withdrawFeeRate = (tempValues.value.withdrawFeeRate / 100).toString(); // 转回小数
formModel.sortOrder = tempValues.value.sortOrder;
}
async function handleSubmit() {
syncNumberToString();
await formRef.value?.validate();
loading.value = true;
await safeClient(() =>
client.api.admin.assets({ code: props.data.code }).patch({
...formModel
})
);
loading.value = false;
window.$message?.success('更新成功');
emit('close');
}
function handleCancel() {
emit('close');
}
</script>
<template>
<div class="my-10">
<NForm ref="formRef" :model="formModel" :rules="rules" label-placement="left" :label-width="140">
<NGrid :cols="2" :x-gap="12">
<NFormItemGi label="资产代码" path="code">
<NInput v-model:value="formModel.code" placeholder="请输入资产代码" />
</NFormItemGi>
<NFormItemGi label="资产名称" path="name">
<NInput v-model:value="formModel.name" placeholder="请输入资产名称" />
</NFormItemGi>
</NGrid>
<NFormItem label="图标地址" path="iconUrl">
<IconPicker v-model="formModel.iconUrl" :collections="['cryptocurrency-color']" />
</NFormItem>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi label="每日最大提现金额" path="maxWithdrawPerDay">
<NInputNumber
v-model:value="tempValues.maxWithdrawPerDay"
placeholder="请输入每日最大提现金额"
:min="0"
:precision="2"
class="w-full"
/>
</NFormItemGi>
<NFormItemGi label="单笔最大提现金额" path="maxWithdrawPerTx">
<NInputNumber
v-model:value="tempValues.maxWithdrawPerTx"
placeholder="请输入单笔最大提现金额"
:min="0"
:precision="2"
class="w-full"
/>
</NFormItemGi>
</NGrid>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi label="最小充值金额" path="minDeposit">
<NInputNumber
v-model:value="tempValues.minDeposit"
placeholder="请输入最小充值金额"
:min="0"
:precision="2"
class="w-full"
/>
</NFormItemGi>
<NFormItemGi label="最小提现金额" path="minWithdraw">
<NInputNumber
v-model:value="tempValues.minWithdraw"
placeholder="请输入最小提现金额"
:min="0"
:precision="2"
class="w-full"
/>
</NFormItemGi>
</NGrid>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi label="提现固定手续费" path="withdrawFeeFixed">
<NInputNumber
v-model:value="tempValues.withdrawFeeFixed"
placeholder="请输入提现固定手续费"
:min="0"
:precision="2"
class="w-full"
/>
</NFormItemGi>
<NFormItemGi label="提现手续费率" path="withdrawFeeRate">
<NInputNumber
v-model:value="tempValues.withdrawFeeRate"
placeholder="请输入提现手续费率"
:min="0"
:max="100"
:precision="4"
class="w-full"
>
<template #suffix>%</template>
</NInputNumber>
</NFormItemGi>
</NGrid>
<NFormItem label="排序" path="sortOrder">
<NInputNumber v-model:value="tempValues.sortOrder" placeholder="请输入排序" :min="0" class="w-full" />
</NFormItem>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi label="是否可交易">
<NSwitch v-model:value="formModel.tradeEnabled" />
</NFormItemGi>
<NFormItemGi label="是否可转账">
<NSwitch v-model:value="formModel.transferEnabled" />
</NFormItemGi>
<NFormItemGi label="是否可提现">
<NSwitch v-model:value="formModel.withdrawEnabled" />
</NFormItemGi>
<NFormItemGi label="是否可充值">
<NSwitch v-model:value="formModel.depositEnabled" />
</NFormItemGi>
<NFormItemGi label="是否启用">
<NSwitch v-model:value="formModel.isActive" />
</NFormItemGi>
<NFormItemGi label="是否为 RWA 资产">
<NSwitch v-model:value="formModel.isRwaAsset" />
</NFormItemGi>
</NGrid>
<NSpace justify="end">
<NButton type="primary" ghost @click="handleCancel">取消</NButton>
<NButton type="primary" :loading="loading" @click="handleSubmit">提交</NButton>
</NSpace>
</NForm>
</div>
</template>
<style lang="css" scoped></style>

View File

@@ -1,242 +0,0 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { useDateFormat } from '@vueuse/core';
import { NSelect, useDialog } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
import SvgIcon from '@/components/custom/svg-icon.vue';
import Edit from './components/edit.vue';
import Add from './components/add.vue';
const dialog = useDialog();
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.assets.get({
query: { ...pagination, ...filter }
})
);
};
const columns: TableBaseColumns = [
{
title: '代码',
key: 'code',
fixed: 'left'
},
{
title: '名称',
key: 'name'
},
{
title: '图标',
key: 'iconUrl',
width: 80,
render: (row: any) => {
return h(SvgIcon, { icon: row.iconUrl, class: 'w-6 h-6' });
}
},
{
title: '每日最大提现金额',
key: 'maxWithdrawPerDay',
render: (row: any) => {
return Number(row.maxWithdrawPerDay).toFixed(2) ?? '无限制';
}
},
{
title: '单笔最大提现金额',
key: 'maxWithdrawPerTx',
render: (row: any) => {
return Number(row.maxWithdrawPerTx).toFixed(2) ?? '无限制';
}
},
{
title: '最小充值金额',
key: 'minDeposit',
render: (row: any) => {
return Number(row.minDeposit).toFixed(2);
}
},
{
title: '最小提现金额',
key: 'minWithdraw',
render: (row: any) => {
return Number(row.minWithdraw).toFixed(2);
}
},
{
title: '提现固定手续费',
key: 'withdrawFeeFixed',
render: (row: any) => {
return Number(row.withdrawFeeFixed).toFixed(2);
}
},
{
title: '提现手续费率',
key: 'withdrawFeeRate',
render: (row: any) => {
return `${Number(row.withdrawFeeRate).toFixed(4)}%`;
}
},
{
title: '是否可交易',
key: 'tradeEnabled',
render: (row: any) => {
return row.tradeEnabled ? '是' : '否';
}
},
{
title: '是否可转账',
key: 'transferEnabled',
render: (row: any) => {
return row.transferEnabled ? '是' : '否';
}
},
{
title: '是否可提现',
key: 'withdrawEnabled',
render: (row: any) => {
return row.withdrawEnabled ? '是' : '否';
}
},
{
title: '是否可充值',
key: 'depositEnabled',
render: (row: any) => {
return row.depositEnabled ? '是' : '否';
}
},
{
title: '是否启用',
key: 'isActive',
render: (row: any) => {
return row.isActive ? '是' : '否';
}
},
{
title: '是否为 RWA 资产',
key: 'isRwaAsset',
render: (row: any) => {
return row.isRwaAsset ? '是' : '否';
}
},
{
title: '排序',
key: 'sortOrder'
},
{
title: '创建时间',
key: 'createdAt',
render: (row: any) => {
return useDateFormat(row.createdAt, 'YYYY-MM-DD HH:mm').value;
}
},
{
title: '操作',
fixed: 'right',
key: 'operation',
width: 140,
operations: (row: any) => [
{
contentText: '编辑',
ghost: true,
size: 'small',
onClick: () => {
handleEdit(row);
}
},
{
contentText: '删除',
ghost: true,
type: 'error',
size: 'small',
onClick: () => {
dialog.create({
title: '删除确认',
content: '确认删除该资产吗,删除后不可恢复。',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
await safeClient(() => client.api.admin.assets({ code: row.code }).delete());
tableInst.value?.reload();
}
});
}
}
]
}
];
const filterColumns: TableFilterColumns = [
{
title: '资产代码',
key: 'code'
},
{
title: '资产名称',
key: 'name'
},
{
title: '是否启用',
key: 'isActive',
component: NSelect,
componentProps: {
placeholder: '请选择状态',
clearable: true,
options: [
{ label: '启用', value: true },
{ label: '未启用', value: false }
]
}
}
];
function handleAdd() {
const dialogInstance = dialog.create({
title: '新建资产',
showIcon: false,
content: () =>
h(Add, {
onClose: () => {
dialogInstance?.destroy();
tableInst.value?.reload();
}
}),
style: { width: '800px' },
closable: true
});
}
function handleEdit(row: any) {
const dialogInstance = dialog.create({
title: '编辑资产',
showIcon: false,
content: () =>
h(Edit, {
data: row,
onClose: () => {
dialogInstance?.destroy();
tableInst.value?.reload();
}
}),
style: { width: '800px' },
closable: true
});
}
</script>
<template>
<TableBase
ref="tableInst"
:columns="columns"
:fetch-data="fetchData"
show-header-operation
:filter-columns="filterColumns"
:scroll-x="3000"
@add="handleAdd"
/>
</template>
<style lang="css" scoped></style>

View File

@@ -1,121 +0,0 @@
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import { client, safeClient } from '@/service/api';
defineOptions({ name: 'AddBank' });
type Body = CommonType.TreatyBody<typeof client.api.admin.bank_account.banks.post>;
const emit = defineEmits<{
(e: 'close'): void;
}>();
const formRef = useTemplateRef<FormInst | null>('formRef');
const form = ref<Body>({
bankCode: '',
nameCn: '',
nameEn: '',
isActive: true,
sortOrder: 0
});
const rules: FormRules = {
bankCode: [
{
required: true,
message: '请输入银行代码',
trigger: ['blur', 'input']
},
{
pattern: /^[A-Z0-9_]+$/,
message: '银行代码只能包含大写字母、数字和下划线',
trigger: ['blur', 'input']
}
],
nameCn: [
{
required: true,
message: '请输入银行中文名称',
trigger: ['blur', 'input']
},
{
min: 2,
max: 100,
message: '银行名称长度应在2-100个字符之间',
trigger: ['blur', 'input']
}
],
sortOrder: [
{
required: true,
type: 'number',
message: '请输入排序顺序',
trigger: ['blur', 'change']
}
]
};
async function handleSubmit() {
formRef.value?.validate(async errors => {
if (!errors) {
const { data } = await safeClient(() =>
client.api.admin.bank_account.banks.post({
...form.value
})
);
if (data) {
window.$message?.success('银行创建成功');
emit('close');
}
}
});
}
</script>
<template>
<NForm
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
label-placement="left"
require-mark-placement="left"
>
<NFormItem label="银行代码" path="bankCode">
<NInput
v-model:value="form.bankCode"
placeholder="请输入银行代码ICBC"
maxlength="50"
:input-props="{ style: 'text-transform: uppercase' }"
/>
</NFormItem>
<NFormItem label="银行名称" path="nameCn">
<NInput v-model:value="form.nameCn" placeholder="请输入银行中文名称" maxlength="100" show-count />
</NFormItem>
<NFormItem label="排序顺序" path="sortOrder">
<NInputNumber v-model:value="form.sortOrder" :min="0" :step="1" placeholder="请输入排序顺序" class="w-full">
<template #suffix>
<span class="text-12px text-gray-400">数字越小越靠前</span>
</template>
</NInputNumber>
</NFormItem>
<NFormItem label="是否启用" path="isActive">
<NSwitch :value="form.isActive || false" @update:value="val => (form.isActive = val)">
<template #checked>启用</template>
<template #unchecked>禁用</template>
</NSwitch>
</NFormItem>
<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

@@ -1,106 +0,0 @@
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import type { Treaty } from '@elysiajs/eden';
import { client, safeClient } from '@/service/api';
defineOptions({ name: 'EditBank' });
type Data = Treaty.Data<typeof client.api.admin.bank_account.banks.get>['data'][number];
type Body = CommonType.TreatyBody<typeof client.api.admin.bank_account.banks.post>;
const props = defineProps<{
data: Data;
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
const formRef = useTemplateRef<FormInst | null>('formRef');
const form = ref<Body>({
...props.data
});
const rules: FormRules = {
nameCn: [
{
required: true,
message: '请输入银行中文名称',
trigger: ['blur', 'input']
},
{
min: 2,
max: 100,
message: '银行名称长度应在2-100个字符之间',
trigger: ['blur', 'input']
}
],
sortOrder: [
{
required: true,
type: 'number',
message: '请输入排序顺序',
trigger: ['blur', 'change']
}
]
};
async function handleSubmit() {
formRef.value?.validate(async errors => {
if (!errors) {
const { data } = await safeClient(() =>
client.api.admin.bank_account.banks({ id: props.data.id }).patch({
...form.value
})
);
if (data) {
window.$message?.success('银行更新成功');
emit('close');
}
}
});
}
</script>
<template>
<NForm
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
label-placement="left"
require-mark-placement="left"
>
<NFormItem label="银行代码">
<NInput :value="data.bankCode" disabled placeholder="银行代码不可修改" />
</NFormItem>
<NFormItem label="银行名称" path="nameCn">
<NInput v-model:value="form.nameCn" placeholder="请输入银行中文名称" maxlength="100" show-count />
</NFormItem>
<NFormItem label="排序顺序" path="sortOrder">
<NInputNumber v-model:value="form.sortOrder" :min="0" :step="1" placeholder="请输入排序顺序" class="w-full">
<template #suffix>
<span class="text-12px text-gray-400">数字越小越靠前</span>
</template>
</NInputNumber>
</NFormItem>
<NFormItem label="是否启用" path="isActive">
<NSwitch :value="form.isActive || false" @update:value="val => (form.isActive = val)">
<template #checked>启用</template>
<template #unchecked>禁用</template>
</NSwitch>
</NFormItem>
<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

@@ -1,146 +0,0 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { useDateFormat } from '@vueuse/core';
import { useDialog, useMessage } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
import Add from './components/add.vue';
import Edit from './components/edit.vue';
const dialog = useDialog();
const message = useMessage();
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.bank_account.banks.get({
query: {
...pagination,
...filter
}
})
);
};
const columns: TableBaseColumns = [
{
title: '银行名称',
key: 'nameCn'
},
{
title: '银行编号',
key: 'bankCode'
},
{
title: '是否启用',
key: 'isActive',
width: 100,
render(row: any) {
return row.isActive ? '是' : '否';
}
},
{
title: '排序',
key: 'sortOrder',
width: 80
},
{
title: '创建时间',
key: 'createdAt',
render(row: any) {
return useDateFormat(row.createdAt, 'YYYY-MM-DD HH:mm:ss').value;
}
},
{
title: '操作',
fixed: 'right',
key: 'operation',
width: 160,
operations: (row: any) => [
{
contentText: '编辑',
type: 'primary',
ghost: true,
size: 'small',
onClick: () => {
handleEdit(row);
}
},
{
contentText: '删除',
type: 'error',
ghost: true,
size: 'small',
onClick: async () => {
dialog.create({
title: '提示',
positiveText: '是',
negativeText: '否',
content: '确认删除该银行信息?',
onPositiveClick: async () => {
await safeClient(() => client.api.admin.bank_account.banks({ id: row.id }).delete());
message.success('删除成功');
tableInst.value?.reload();
}
});
}
}
]
}
];
const filterColumns: TableFilterColumns = [
{
title: '银行名称',
key: 'nameCn'
},
{
title: '银行编号',
key: 'bankCode'
}
];
function handleAdd() {
const dialogInstance = dialog.create({
title: '创建银行',
content: () =>
h(Add, {
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
}),
style: { width: '600px' },
showIcon: false
});
}
function handleEdit(row) {
const dialogInstance = dialog.create({
title: '编辑银行',
content: () =>
h(Edit, {
data: row,
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
}),
style: { width: '600px' },
showIcon: false
});
}
</script>
<template>
<TableBase
ref="tableInst"
show-header-operation
:columns="columns"
:filter-columns="filterColumns"
:fetch-data="fetchData"
:scroll-x="800"
@add="handleAdd"
/>
</template>
<style lang="css" scoped></style>

View File

@@ -1,140 +0,0 @@
<script lang="ts" setup>
import { useTemplateRef } from 'vue';
import { NSelect, useDialog, useMessage } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import { DepositTypeEnum } from '@/enum';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
const dialog = useDialog();
const message = useMessage();
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.deposit.pending.get({
query: { ...pagination, ...filter }
})
);
};
const columns: TableBaseColumns = [
{
title: 'ID',
key: 'id'
},
{
title: '用户ID',
key: 'userId'
},
{
title: '用户名',
key: 'user.email'
},
{
title: '资产代码',
key: 'assetCode'
},
{
title: '充值类型',
key: 'depositType',
render(row) {
return DepositTypeEnum[row.depositType as keyof typeof DepositTypeEnum];
}
},
{
title: '金额',
key: 'amount',
render: row => {
return Number(row.amount).toFixed(2);
}
},
{
title: '操作',
fixed: 'right',
key: 'operation',
width: 160,
operations: (row: any) => [
{
contentText: '通过',
type: 'primary',
strong: true,
secondary: true,
visible: row.status !== 'approved',
onClick: async () => {
dialog.create({
title: '提示',
positiveText: '是',
negativeText: '否',
content: '确认通过该充值请求吗?',
onPositiveClick: async () => {
await safeClient(() =>
client.api.admin.deposit.approve({ orderId: row.id as string }).post({
actualAmount: row.amount
})
);
tableInst.value?.reload();
message.success('充值通过成功');
}
});
}
},
{
contentText: '拒绝',
type: 'error',
ghost: true,
onClick: async () => {
dialog.create({
title: '提示',
positiveText: '是',
negativeText: '否',
content: '确认拒绝该充值请求吗?',
onPositiveClick: async () => {
safeClient(() =>
client.api.admin.deposit.reject({ orderId: row.id as string }).post({
reviewNote: '管理员拒绝充值'
})
);
tableInst.value?.reload();
message.success('充值拒绝成功');
}
});
}
}
]
}
];
const filterColumns: TableFilterColumns = [
{
title: '用户ID',
key: 'userId'
},
{
title: '资产代码',
key: 'assetCode'
},
{
title: '充值类型',
key: 'depositType',
component: NSelect,
componentProps: {
options: [
{ label: '法币充值', value: 'fiat' },
{ label: '加密货币充值', value: 'crypto' }
]
}
}
];
</script>
<template>
<TableBase
ref="tableInst"
:columns="columns"
:fetch-data="fetchData"
show-header-operation
:filter-columns="filterColumns"
/>
</template>
<style lang="css" scoped></style>

View File

@@ -1,236 +0,0 @@
<script lang="ts" setup>
import { computed, h, ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import { NInput, useDialog } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import UploadS3 from '@/components/upload/index.vue';
defineOptions({ name: 'AddNews' });
type Body = CommonType.TreatyBody<typeof client.api.admin.news.post>;
const emit = defineEmits<{
(e: 'close'): void;
}>();
const formRef = useTemplateRef<FormInst | null>('formRef');
const dialog = useDialog();
const form = ref<Body>({
title: '',
content: '',
categoryId: '',
attachmentIds: [],
isPinned: false
});
// 获取分类列表
const { data: categories, execute: refreshCategories } = safeClient(() =>
client.api.admin.news_categories.get({
query: {
pageIndex: 1,
pageSize: 100
}
})
);
const categoryOptions = computed(
() =>
categories.value?.data.map(cat => ({
label: cat.name,
value: cat.id
})) || []
);
// 新增分类
function handleAddCategory() {
const categoryName = ref('');
dialog.create({
title: '新增分类',
content: () =>
h('div', { class: 'py-4' }, [
h('div', { class: 'mb-2 text-14px' }, '分类名称'),
h(NInput, {
value: categoryName.value,
placeholder: '请输入分类名称',
'onUpdate:value': (val: string) => {
categoryName.value = val;
}
})
]),
positiveText: '创建',
negativeText: '取消',
onPositiveClick: async () => {
if (!categoryName.value.trim()) {
window.$message?.error('请输入分类名称');
return false;
}
const { data } = await safeClient(() =>
client.api.admin.news_categories.post({
name: categoryName.value
})
);
if (data) {
window.$message?.success('分类创建成功');
await refreshCategories();
if (data.value?.id) {
form.value.categoryId = data.value.id;
}
}
}
});
}
const rules: FormRules = {
title: [
{
required: true,
message: '请输入新闻标题',
trigger: ['blur', 'input']
},
{
min: 2,
max: 200,
message: '标题长度应在2-200个字符之间',
trigger: ['blur', 'input']
}
],
summary: [
{
required: true,
message: '请输入新闻摘要',
trigger: ['blur', 'input']
},
{
min: 10,
max: 200,
message: '摘要长度应在10-200个字符之间',
trigger: ['blur', 'input']
}
],
content: [
{
required: true,
message: '请输入新闻内容',
trigger: ['blur', 'input']
},
{
min: 10,
message: '内容至少10个字符',
trigger: ['blur', 'input']
}
],
categoryId: [
{
required: true,
message: '请选择新闻分类',
trigger: 'change'
}
]
};
async function handleSubmit() {
formRef.value?.validate(async errors => {
if (!errors) {
const { data } = await safeClient(() =>
client.api.admin.news.post({
...form.value
})
);
if (data) {
window.$message?.success('新闻创建成功');
emit('close');
}
}
});
}
</script>
<template>
<NForm
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
label-placement="left"
require-mark-placement="left"
>
<NFormItem label="新闻标题" path="title">
<NInput v-model:value="form.title" placeholder="请输入新闻标题" maxlength="50" show-count />
</NFormItem>
<NFormItem label="缩略图" path="thumbnailId">
<UploadS3
:model-value="form.thumbnailId ? [form.thumbnailId] : undefined"
:max-size="20"
:max-files="1"
accept="image/*"
placeholder="上传图片"
:fetch-options="{ businessType: 'news_attachment' }"
@update:model-value="evt => (form.thumbnailId = evt.length > 0 ? evt[0] : undefined)"
/>
</NFormItem>
<NGrid :cols="2">
<NFormItemGi label="新闻分类" path="categoryId">
<NSelect v-model:value="form.categoryId" :options="categoryOptions" placeholder="请选择新闻分类" class="flex-1">
<template #action>
<div class="px-3 py-2">
<NButton size="small" type="primary" block @click.stop="handleAddCategory">
<template #icon>
<icon-ic-round-plus class="text-icon" />
</template>
新增分类
</NButton>
</div>
</template>
</NSelect>
</NFormItemGi>
<NFormItemGi label="新闻摘要" path="summary">
<NInput v-model:value="form.summary" placeholder="请输入新闻摘要" maxlength="100" show-count />
</NFormItemGi>
</NGrid>
<NFormItem label="新闻内容" path="content">
<MarkdownEditor v-model:value="form.content" placeholder="请输入新闻内容" :preview="false" />
</NFormItem>
<NFormItem label="附件上传" path="attachmentIds">
<UploadS3
:model-value="form.attachmentIds || []"
:max-size="20"
:max-files="10"
accept="*/*"
placeholder="上传附件"
:fetch-options="{ businessType: 'other' }"
@update:model-value="evt => (form.attachmentIds = evt)"
/>
</NFormItem>
<NGrid :cols="2">
<NFormItemGi label="置顶显示" path="isPinned">
<NSwitch v-model:value="form.isPinned">
<template #checked></template>
<template #unchecked></template>
</NSwitch>
</NFormItemGi>
<NFormItemGi label="排序顺序" path="sortOrder">
<NInputNumber v-model:value="form.sortOrder" :min="0" :step="1" placeholder="请输入排序顺序" class="w-full">
<template #suffix>
<span class="text-12px text-gray-400">数字越小越靠前</span>
</template>
</NInputNumber>
</NFormItemGi>
</NGrid>
<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

@@ -1,238 +0,0 @@
<script lang="ts" setup>
import { computed, h, ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import { NInput, useDialog } from 'naive-ui';
import type { Treaty } from '@elysiajs/eden';
import { client, safeClient } from '@/service/api';
import UploadS3 from '@/components/upload/index.vue';
defineOptions({ name: 'EditNews' });
type Data = Treaty.Data<typeof client.api.admin.news.get>['data'][number];
type Body = CommonType.TreatyBody<typeof client.api.admin.news.post>;
const props = defineProps<{
data: Data;
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
const formRef = useTemplateRef<FormInst | null>('formRef');
const dialog = useDialog();
const form = ref<Body>({
...props.data
});
// 获取分类列表
const { data: categories, execute: refreshCategories } = safeClient(() =>
client.api.admin.news_categories.get({
query: {
pageIndex: 1,
pageSize: 100
}
})
);
const categoryOptions = computed(
() =>
categories.value?.data.map(cat => ({
label: cat.name,
value: cat.id
})) || []
);
// 新增分类
function handleAddCategory() {
const categoryName = ref('');
dialog.create({
title: '新增分类',
content: () =>
h('div', { class: 'py-4' }, [
h('div', { class: 'mb-2 text-14px' }, '分类名称'),
h(NInput, {
value: categoryName.value,
placeholder: '请输入分类名称',
'onUpdate:value': (val: string) => {
categoryName.value = val;
}
})
]),
positiveText: '创建',
negativeText: '取消',
onPositiveClick: async () => {
if (!categoryName.value.trim()) {
window.$message?.error('请输入分类名称');
return false;
}
const { data } = await safeClient(() =>
client.api.admin.news_categories.post({
name: categoryName.value
})
);
if (data) {
window.$message?.success('分类创建成功');
await refreshCategories();
if (data.value?.id) {
form.value.categoryId = data.value.id;
}
}
}
});
}
const rules: FormRules = {
title: [
{
required: true,
message: '请输入新闻标题',
trigger: ['blur', 'input']
},
{
min: 2,
max: 200,
message: '标题长度应在2-200个字符之间',
trigger: ['blur', 'input']
}
],
summary: [
{
required: true,
message: '请输入新闻摘要',
trigger: ['blur', 'input']
},
{
min: 10,
max: 200,
message: '摘要长度应在10-200个字符之间',
trigger: ['blur', 'input']
}
],
content: [
{
required: true,
message: '请输入新闻内容',
trigger: ['blur', 'input']
},
{
min: 10,
message: '内容至少10个字符',
trigger: ['blur', 'input']
}
],
categoryId: [
{
required: true,
message: '请选择新闻分类',
trigger: 'change'
}
]
};
async function handleSubmit() {
formRef.value?.validate(async errors => {
if (!errors) {
const { data } = await safeClient(() =>
client.api.admin.news({ id: props.data.id }).patch({
...form.value
})
);
if (data) {
window.$message?.success('新闻更新成功');
emit('close');
}
}
});
}
</script>
<template>
<NForm
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
label-placement="left"
require-mark-placement="left"
>
<NFormItem label="新闻标题" path="title">
<NInput v-model:value="form.title" placeholder="请输入新闻标题" maxlength="200" show-count />
</NFormItem>
<NFormItem label="缩略图" path="thumbnailId">
<UploadS3
:model-value="[form.thumbnailId || '']"
:max-size="20"
:max-files="1"
accept="image/*"
placeholder="上传图片"
:fetch-options="{ businessType: 'news_attachment' }"
@update:model-value="evt => (form.thumbnailId = evt[0] || '')"
/>
</NFormItem>
<NGrid :cols="2">
<NFormItemGi label="新闻分类" path="categoryId">
<NSelect v-model:value="form.categoryId" :options="categoryOptions" placeholder="请选择新闻分类" class="flex-1">
<template #action>
<div class="px-3 py-2">
<NButton size="small" type="primary" block @click.stop="handleAddCategory">
<template #icon>
<icon-ic-round-plus class="text-icon" />
</template>
新增分类
</NButton>
</div>
</template>
</NSelect>
</NFormItemGi>
<NFormItemGi label="新闻摘要" path="summary">
<NInput v-model:value="form.summary" placeholder="请输入新闻摘要" maxlength="100" show-count />
</NFormItemGi>
</NGrid>
<NFormItem label="新闻内容" path="content">
<MarkdownEditor v-model:value="form.content" placeholder="请输入新闻内容" :preview="false" class="h-400px!" />
</NFormItem>
<NFormItem label="附件上传" path="attachmentIds">
<UploadS3
:model-value="form.attachmentIds || []"
:max-size="20"
:max-files="10"
accept="*/*"
placeholder="上传附件"
:fetch-options="{ businessType: 'news_attachment' }"
@update:model-value="evt => (form.attachmentIds = evt)"
/>
</NFormItem>
<NGrid :cols="2">
<NFormItemGi label="置顶显示" path="isPinned">
<NSwitch v-model:value="form.isPinned">
<template #checked></template>
<template #unchecked></template>
</NSwitch>
</NFormItemGi>
<NFormItemGi label="排序顺序" path="sortOrder">
<NInputNumber v-model:value="form.sortOrder" :min="0" :step="1" placeholder="请输入排序顺序" class="w-full">
<template #suffix>
<span class="text-12px text-gray-400">数字越小越靠前</span>
</template>
</NInputNumber>
</NFormItemGi>
</NGrid>
<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

@@ -1,202 +0,0 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { useDateFormat } from '@vueuse/core';
import { NButton, NSelect, NSpace, NTag, useDialog } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
import Add from './components/add.vue';
import Edit from './components/edit.vue';
const dialog = useDialog();
const tableInst = useTemplateRef<TableInst>('tableInst');
const { data: categories } = safeClient(
client.api.admin.news_categories.get({
query: {
pageIndex: 1,
pageSize: 100
}
})
);
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.news.get({
query: {
...pagination,
...filter
}
})
);
};
const columns: TableBaseColumns = [
{
title: '新闻标题',
key: 'title',
width: 250,
ellipsis: { tooltip: true }
},
{
title: '分类',
key: 'category.name',
width: 120
},
{
title: '摘要',
key: 'summary',
width: 300,
ellipsis: { tooltip: true }
},
{
title: '内容预览',
key: 'content',
width: 300,
ellipsis: { tooltip: true }
},
{
title: '附件数',
key: 'attachmentIds',
width: 100,
render: (row: any) => row.attachmentIds?.length || 0
},
{
title: '置顶',
key: 'isPinned',
width: 80,
render: row =>
h(
NTag,
{ type: row.isPinned ? 'warning' : 'default', size: 'small' },
{ default: () => (row.isPinned ? '是' : '否') }
)
},
{
title: '排序顺序',
key: 'sortOrder',
width: 80
},
{
title: '创建时间',
key: 'createdAt',
width: 180,
render: (row: any) => {
return useDateFormat(row.createdAt, 'YYYY-MM-DD HH:mm:ss').value;
}
},
{
title: '操作',
key: 'operations',
width: 150,
fixed: 'right',
render: (row: any) =>
h(
NSpace,
{ size: 'small' },
{
default: () => [
h(
NButton,
{
size: 'small',
type: 'primary',
text: true,
onClick: () => handleEdit(row)
},
{ default: () => '编辑' }
),
h(
NButton,
{
size: 'small',
type: 'error',
text: true,
onClick: () => handleDelete(row.id)
},
{ default: () => '删除' }
)
]
}
)
}
];
const filterColumns: TableFilterColumns = [
{
key: 'isPinned',
title: '置顶状态',
component: NSelect,
componentProps: {
options: [
{ label: '全部', value: '' },
{ label: '已置顶', value: 'true' },
{ label: '未置顶', value: 'false' }
]
}
}
];
function handleAdd() {
const dialogInstance = dialog.create({
title: '创建新闻',
content: () =>
h(Add, {
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
}),
style: { width: '1200px' },
showIcon: false
});
}
function handleEdit(row: any) {
const dialogInstance = dialog.create({
title: '编辑新闻',
content: () =>
h(Edit, {
data: row,
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
}),
style: { width: '1200px' },
showIcon: false
});
}
function handleDelete(id: string) {
dialog.warning({
title: '确认删除',
content: '确定要删除这条新闻吗?此操作不可恢复。',
positiveText: '删除',
negativeText: '取消',
onPositiveClick: async () => {
const { data } = await safeClient(() =>
client.api.admin.news({ id }).delete({
params: { id }
})
);
if (data) {
window.$message?.success('删除成功');
tableInst.value?.reload();
}
}
});
}
</script>
<template>
<TableBase
ref="tableInst"
show-header-operation
:columns="columns"
:filter-columns="filterColumns"
:fetch-data="fetchData"
:scroll-x="800"
@add="handleAdd"
/>
</template>
<style lang="css" scoped></style>

View File

@@ -1,215 +0,0 @@
<script lang="ts" setup>
import { computed, ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import { client, safeClient } from '@/service/api';
defineOptions({ name: 'AddNotification' });
type Body = CommonType.TreatyBody<typeof client.api.admin.notifications.post>;
const emit = defineEmits<{
(e: 'close'): void;
}>();
const formRef = useTemplateRef<FormInst | null>('formRef');
const form = ref<Body>({
title: '',
content: '',
category: 'GENERAL',
type: 'system',
isBroadcast: true,
priority: 'normal',
userIds: []
});
const categoryOptions = [
{ label: '通用', value: 'GENERAL' },
{ label: '系统', value: 'SYSTEM' },
{ label: '交易', value: 'TRADING' },
{ label: '安全', value: 'SECURITY' },
{ label: '营销', value: 'MARKETING' }
];
const typeOptions = [
{ label: '系统通知', value: 'system' },
{ label: '安全通知', value: 'security' },
{ label: '交易通知', value: 'transaction' },
{ label: '活动通知', value: 'activity' }
];
const priorityOptions = [
{ label: '低', value: 'low' },
{ label: '普通', value: 'normal' },
{ label: '高', value: 'high' },
{ label: '紧急', value: 'urgent' }
];
// 用户选择相关
const { data: users } = safeClient(() =>
client.api.admin.users.get({
query: {
pageIndex: 1,
pageSize: 100
}
})
);
const userOptions = computed(
() =>
users.value?.data.map(user => ({
label: `${user.username} (${user.email})`,
value: user.id
})) || []
);
const showUserSelect = computed(() => !form.value.isBroadcast);
const rules: FormRules = {
title: [
{
required: true,
message: '请输入通知标题',
trigger: ['blur', 'input']
},
{
min: 1,
max: 200,
message: '标题长度应在1-200个字符之间',
trigger: ['blur', 'input']
}
],
content: [
{
required: true,
message: '请输入通知内容',
trigger: ['blur', 'input']
},
{
min: 1,
message: '内容不能为空',
trigger: ['blur', 'input']
}
],
category: [
{
required: true,
message: '请选择通知分类',
trigger: 'change'
}
],
type: [
{
required: true,
message: '请选择通知类型',
trigger: 'change'
}
],
priority: [
{
required: true,
message: '请选择优先级',
trigger: 'change'
}
],
userIds: [
{
type: 'array',
validator: (_rule, value: string[]) => {
if (!form.value.isBroadcast && (!value || value.length === 0)) {
return new Error('非广播通知必须选择接收用户');
}
return true;
},
trigger: 'change'
}
]
};
function handleBroadcastChange(value: boolean) {
if (value) {
form.value.userIds = [];
}
}
async function handleSubmit() {
formRef.value?.validate(async errors => {
if (!errors) {
const { data } = await safeClient(() =>
client.api.admin.notifications.post({
...form.value
})
);
if (data) {
window.$message?.success('通知创建成功');
emit('close');
}
}
});
}
</script>
<template>
<NForm
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
label-placement="left"
require-mark-placement="left"
>
<NFormItem label="通知标题" path="title">
<NInput v-model:value="form.title" placeholder="请输入通知标题" maxlength="200" show-count />
</NFormItem>
<NFormItem label="通知内容" path="content">
<NInput
v-model:value="form.content"
type="textarea"
placeholder="请输入通知内容"
:rows="5"
maxlength="2000"
show-count
/>
</NFormItem>
<NGrid :cols="2" :x-gap="16">
<NFormItemGi label="通知类型" path="type">
<NSelect v-model:value="form.type" :options="typeOptions" placeholder="请选择通知类型" />
</NFormItemGi>
<NFormItemGi label="通知分类" path="category">
<NSelect v-model:value="form.category" :options="categoryOptions" placeholder="请选择通知分类" />
</NFormItemGi>
<NFormItemGi label="优先级" path="priority">
<NSelect v-model:value="form.priority" :options="priorityOptions" placeholder="请选择优先级" />
</NFormItemGi>
<NFormItemGi label="广播通知" path="isBroadcast">
<NSwitch v-model:value="form.isBroadcast" @update:value="handleBroadcastChange">
<template #checked></template>
<template #unchecked></template>
</NSwitch>
</NFormItemGi>
</NGrid>
<NFormItem v-if="showUserSelect" label="接收用户" path="userIds">
<NSelect
v-model:value="form.userIds"
:options="userOptions"
placeholder="请选择接收用户"
multiple
filterable
clearable
/>
</NFormItem>
<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

@@ -1,147 +0,0 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { useDateFormat } from '@vueuse/core';
import { NTag, useDialog } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableInst } from '@/components/table';
import Add from './components/add.vue';
const dialog = useDialog();
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.notifications.get({
query: {
...pagination,
...filter
}
})
);
};
const typeMap: Record<string, { label: string; type: 'info' | 'success' | 'warning' | 'error' }> = {
system: { label: '系统', type: 'info' },
security: { label: '安全', type: 'error' },
transaction: { label: '交易', type: 'success' },
activity: { label: '活动', type: 'warning' }
};
const categoryMap: Record<string, string> = {
GENERAL: '通用',
SYSTEM: '系统',
TRADING: '交易',
SECURITY: '安全',
MARKETING: '营销'
};
const priorityMap: Record<string, { label: string; type: 'default' | 'info' | 'success' | 'warning' | 'error' }> = {
low: { label: '低', type: 'default' },
normal: { label: '普通', type: 'info' },
high: { label: '高', type: 'warning' },
urgent: { label: '紧急', type: 'error' }
};
const columns: TableBaseColumns = [
{
key: 'selection',
title: '序号',
type: 'selection',
width: 60
},
{
title: '通知标题',
key: 'title',
width: 200,
ellipsis: { tooltip: true }
},
{
title: '通知类型',
key: 'type',
width: 100,
render: row => {
const type = typeMap[row.type as keyof typeof typeMap];
return h(NTag, { type: type?.type || 'default', size: 'small' }, { default: () => type?.label || row.type });
}
},
{
title: '分类',
key: 'category',
width: 100,
render: (row: any) => {
return categoryMap[row.category] || row.category;
}
},
{
title: '优先级',
key: 'priority',
width: 100,
render: row => {
const priority = priorityMap[row.priority as keyof typeof priorityMap];
return h(
NTag,
{ type: priority?.type || 'default', size: 'small' },
{ default: () => priority?.label || row.priority }
);
}
},
{
title: '广播',
key: 'isBroadcast',
width: 80,
render: row => (row.isBroadcast ? '是' : '否')
},
{
title: '通知内容',
key: 'content',
ellipsis: { tooltip: true },
width: 250
},
{
title: '创建时间',
key: 'createdAt',
width: 180,
render: (row: any) => {
return useDateFormat(row.createdAt, 'YYYY-MM-DD HH:mm:ss').value;
}
}
// {
// title: '操作',
// key: 'operations',
// fixed: 'right',
// width: 120
// }
];
function handleAdd() {
const dialogInstance = dialog.create({
title: '创建通知',
content: () =>
h(Add, {
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
}),
style: { width: '600px' },
showIcon: false,
onPositiveClick: () => {
window.$message?.success('创建通知成功');
tableInst.value?.reload();
}
});
}
</script>
<template>
<TableBase
ref="tableInst"
show-header-operation
:columns="columns"
:fetch-data="fetchData"
:scroll-x="1000"
@add="handleAdd"
/>
</template>
<style lang="css" scoped></style>

View File

@@ -1,230 +0,0 @@
<script lang="ts" setup>
import { computed, ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import type { Treaty } from '@elysiajs/eden';
import { client, safeClient } from '@/service/api';
defineOptions({ name: 'EditSpotRobotConfig' });
type Data = Treaty.Data<typeof client.api.admin.spot_robot_trader.configs.get>['data'][number];
const props = defineProps<{
data: Data;
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
const formRef = useTemplateRef<FormInst | null>('formRef');
const { data: assets } = safeClient(client.api.admin.assets.get({ query: { pageIndex: 1, pageSize: 100 } }));
const { data: users } = safeClient(client.api.admin.users.get({ query: { pageIndex: 1, pageSize: 200 } }));
const { data: tradingPairs } = safeClient(
client.api.admin.trading_pairs.get({ query: { pageIndex: 1, pageSize: 100 } })
);
const assetOptions = computed(
() =>
assets.value?.data.map(asset => ({
label: `${asset.code} - ${asset.name}`,
value: asset.code
})) || []
);
const form = ref({
baseAsset: props.data.baseAsset,
quoteAsset: props.data.quoteAsset,
robotUserId: props.data.robotUserId,
symbol: props.data.symbol,
baseTopUpAmount: props.data.baseTopUpAmount ? Number(props.data.baseTopUpAmount) : 0,
enabled: props.data.enabled,
operatorUserId: props.data.operatorUserId || '',
buyPriceMax: props.data.buyPriceMax ? Number(props.data.buyPriceMax) : 0,
buyPriceMin: props.data.buyPriceMin ? Number(props.data.buyPriceMin) : 0,
buyPriceStep: props.data.buyPriceStep ? Number(props.data.buyPriceStep) : 0
});
const rules: FormRules = {
baseAsset: [
{
required: true,
message: '请输入基础资产',
trigger: ['blur', 'input']
}
],
quoteAsset: [
{
required: true,
message: '请输入计价资产',
trigger: ['blur', 'input']
}
],
robotUserId: [
{
required: true,
message: '请输入机器人用户ID',
trigger: ['blur', 'input']
}
],
symbol: [
{
required: true,
message: '请输入交易对',
trigger: ['blur', 'input']
}
],
baseTopUpAmount: [
{
required: true,
type: 'number',
message: '请输入基础资产单次充值金额',
trigger: ['blur', 'change']
}
],
buyPriceMax: [
{
required: true,
type: 'number',
message: '请输入买入价格上限',
trigger: ['blur', 'change']
}
],
buyPriceMin: [
{
required: true,
type: 'number',
message: '请输入买入价格下限',
trigger: ['blur', 'change']
}
],
buyPriceStep: [
{
required: true,
type: 'number',
message: '请输入买入价格步长',
trigger: ['blur', 'change']
}
]
};
async function handleSubmit() {
formRef.value?.validate(async errors => {
if (!errors) {
const { data } = await safeClient(() =>
client.api.admin.spot_robot_trader.config.put({
id: props.data.id,
baseAsset: form.value.baseAsset,
operatorUserId: form.value.operatorUserId,
quoteAsset: form.value.quoteAsset,
robotUserId: form.value.robotUserId,
symbol: form.value.symbol,
baseTopUpAmount: String(form.value.baseTopUpAmount),
enabled: form.value.enabled,
buyPriceMax: String(form.value.buyPriceMax),
buyPriceMin: String(form.value.buyPriceMin),
buyPriceStep: String(form.value.buyPriceStep)
})
);
if (data) {
window.$message?.success('现货机器人配置更新成功');
emit('close');
}
}
});
}
</script>
<template>
<NForm
ref="formRef"
:model="form"
:rules="rules"
label-width="140px"
label-placement="left"
require-mark-placement="left"
>
<NFormItem label="基础资产" path="baseAsset">
<NSelect v-model:value="form.baseAsset" :options="assetOptions" placeholder="请选择基础资产" filterable />
</NFormItem>
<NFormItem label="计价资产" path="quoteAsset">
<NSelect v-model:value="form.quoteAsset" :options="assetOptions" placeholder="请选择计价资产" filterable />
</NFormItem>
<NFormItem label="机器人用户ID" path="robotUserId">
<NSelect
v-model:value="form.robotUserId"
:options="users?.data.map(user => ({ label: `${user.nickname || user.user.name}`, value: user.userId })) || []"
placeholder="请选择机器人用户"
filterable
/>
</NFormItem>
<NFormItem label="交易对" path="symbol">
<NSelect
v-model:value="form.symbol"
:options="tradingPairs?.data.map(tp => ({ label: tp.symbol, value: tp.symbol })) || []"
placeholder="请选择交易对"
filterable
/>
</NFormItem>
<NFormItem label="基础资产单次充值金额" path="baseTopUpAmount">
<NInputNumber
v-model:value="form.baseTopUpAmount"
:min="0"
:step="0.01"
:precision="8"
placeholder="请输入充值金额"
class="w-full"
/>
</NFormItem>
<NFormItem label="买入价格上限" path="buyPriceMax">
<NInputNumber
v-model:value="form.buyPriceMax"
:min="0"
:step="0.01"
:precision="8"
placeholder="请输入买入价格上限"
class="w-full"
/>
</NFormItem>
<NFormItem label="买入价格下限" path="buyPriceMin">
<NInputNumber
v-model:value="form.buyPriceMin"
:min="0"
:step="0.01"
:precision="8"
placeholder="请输入买入价格下限"
class="w-full"
/>
</NFormItem>
<NFormItem label="买入价格步长" path="buyPriceStep">
<NInputNumber
v-model:value="form.buyPriceStep"
:min="0"
:step="0.01"
:precision="8"
placeholder="请输入买入价格步长"
class="w-full"
/>
</NFormItem>
<NFormItem label="是否启用" path="enabled">
<NSwitch v-model:value="form.enabled">
<template #checked>启用</template>
<template #unchecked>禁用</template>
</NSwitch>
</NFormItem>
<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

@@ -1,143 +0,0 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { useDateFormat } from '@vueuse/core';
import { useDialog } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableInst } from '@/components/table';
import Edit from './components/edit.vue';
const dialog = useDialog();
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.spot_robot_trader.configs.get({
query: {
...pagination,
...filter
}
})
);
};
const columns: TableBaseColumns = [
{
key: 'selection',
title: '序号',
type: 'selection',
width: 60
},
{
key: 'id',
title: 'ID',
width: 100
},
{
key: 'baseAsset',
title: '基础资产',
width: 120
},
{
key: 'quoteAsset',
title: '计价资产',
width: 120
},
{
key: 'robotUserId',
title: '机器人用户ID',
width: 150
},
{
key: 'symbol',
title: '交易对',
width: 100
},
{
key: 'baseTopUpAmount',
title: '基础资产单次充值金额',
width: 160
},
{
key: 'buyPriceMax',
title: '买入价格上限',
width: 140
},
{
key: 'buyPriceMin',
title: '买入价格下限',
width: 140
},
{
key: 'buyPriceStep',
title: '买入价格步长',
width: 140
},
{
key: 'createdAt',
title: '创建时间',
width: 180,
render: (row: any) => {
return useDateFormat(row.createdAt, 'YYYY-MM-DD HH:mm:ss').value;
}
},
{
key: 'enabled',
title: '是否启用',
width: 100,
render: (row: any) => (row.enabled ? '是' : '否')
},
{
key: 'operations',
title: '操作',
fixed: 'right',
width: 150,
operations: (row: any) => [
{
contentText: row.enabled ? '禁用' : '启用',
type: 'primary',
ghost: true,
size: 'small',
onClick: async () => {
await safeClient(
client.api.admin.spot_robot_trader.config.enable.post({
id: row.id
})
);
tableInst.value?.reload();
}
},
{
contentText: '编辑',
type: 'primary',
ghost: true,
size: 'small',
onClick: () => {
handleEdit(row);
}
}
]
}
];
function handleEdit(row: any) {
const dialogInstance = dialog.create({
title: '编辑现货机器人配置',
content: () =>
h(Edit, {
data: row,
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
}),
style: { width: '600px' },
showIcon: false
});
}
</script>
<template>
<TableBase ref="tableInst" show-header-operation :columns="columns" :fetch-data="fetchData" :scroll-x="1000" />
</template>
<style lang="css" scoped></style>

View File

@@ -1,192 +0,0 @@
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import dayjs from 'dayjs';
import { client, safeClient } from '@/service/api';
defineOptions({ name: 'RwaEditionAdd' });
const emit = defineEmits<{
(e: 'close'): void;
}>();
const props = defineProps<{
productId: string;
}>();
type Body = CommonType.TreatyBody<typeof client.api.admin.rwa.issuance.editions.post>;
const formInst = useTemplateRef<FormInst>('formInst');
const form = ref({
productId: props.productId,
editionName: '',
perUserLimit: '1',
totalSupply: '1',
unitPrice: '1',
dividendRate: '0.01',
launchDate: dayjs().valueOf() as number | null,
subscriptionStartDate: dayjs().valueOf() as number | null,
subscriptionEndDate: dayjs().valueOf() as number | null
});
const rules: FormRules = {
editionName: [{ required: true, message: '请输入发行期名称', trigger: ['blur', 'input'] }],
perUserLimit: [{ required: true, message: '请输入个人申购上限', trigger: ['blur', 'input'] }],
totalSupply: [{ required: true, message: '请输入发行总量', trigger: ['blur', 'input'] }],
unitPrice: [{ required: true, message: '请输入单价', trigger: ['blur', 'input'] }],
dividendRate: [{ required: true, message: '请输入分红率', trigger: ['blur', 'input'] }],
launchDate: [{ required: true, type: 'number', message: '请选择预热时间', trigger: ['blur', 'change'] }],
subscriptionStartDate: [
{ required: true, type: 'number', message: '请选择申购开始时间', trigger: ['blur', 'change'] }
],
subscriptionEndDate: [{ required: true, type: 'number', message: '请选择申购结束时间', trigger: ['blur', 'change'] }]
};
// 手动验证时间逻辑
function validateTimes() {
const now = dayjs();
if (form.value.launchDate && dayjs(form.value.launchDate).isBefore(now)) {
return '预热时间必须在当前时间之后';
}
if (form.value.subscriptionStartDate && form.value.launchDate) {
if (dayjs(form.value.subscriptionStartDate).isBefore(dayjs(form.value.launchDate))) {
return '申购开始时间必须在预热时间之后';
}
}
if (form.value.subscriptionEndDate && form.value.subscriptionStartDate) {
if (dayjs(form.value.subscriptionEndDate).isBefore(dayjs(form.value.subscriptionStartDate))) {
return '申购结束时间必须在开始时间之后';
}
}
return null;
}
function handleCreateDraft() {
formInst.value?.validate(async errors => {
if (!errors) {
await safeClient(
client.api.admin.rwa.issuance.editions.post({
...form.value,
launchDate: new Date(form.value.launchDate!),
subscriptionStartDate: new Date(form.value.subscriptionStartDate!),
subscriptionEndDate: new Date(form.value.subscriptionEndDate!)
} as Body)
);
emit('close');
}
});
}
function handleCreateDraftAndSubmit() {
formInst.value?.validate(async errors => {
if (!errors) {
const timeError = validateTimes();
if (timeError) {
window.$message?.error(timeError);
return;
}
await safeClient(
client.api.admin.rwa.issuance.editions.post({
...form.value,
launchDate: new Date(form.value.launchDate!),
subscriptionStartDate: new Date(form.value.subscriptionStartDate!),
subscriptionEndDate: new Date(form.value.subscriptionEndDate!)
} as Body)
);
emit('close');
}
});
}
</script>
<template>
<div class="my-10">
<NForm
ref="formInst"
:model="form"
label-width="150"
label-placement="left"
label-align="left"
:rules="rules"
require-mark-placement="left"
>
<NFormItem path="editionName" label="发行期名称">
<NInput v-model:value="form.editionName" />
</NFormItem>
<NFormItem path="perUserLimit" label="个人申购上限(份)">
<NInputNumber
:min="1"
:step="10"
:value="Number(form.perUserLimit)"
@update:value="val => (form.perUserLimit = String(val))"
/>
</NFormItem>
<NFormItem path="totalSupply" label="发行总量(份)">
<NInputNumber
:min="1"
:step="100"
:value="Number(form.totalSupply)"
@update:value="val => (form.totalSupply = String(val))"
/>
</NFormItem>
<NFormItem path="unitPrice" label="单价($)">
<NInputNumber :min="0" :value="Number(form.unitPrice)" @update:value="val => (form.unitPrice = String(val))" />
</NFormItem>
<NFormItem path="dividendRate" label="分红率(%)">
<NInputNumber
:max="100"
:min="0"
:step="0.1"
:precision="2"
:value="Number(form.dividendRate) * 100"
@update:value="val => (form.dividendRate = String((val || 0) / 100))"
>
<template #suffix>%</template>
</NInputNumber>
</NFormItem>
<NFormItem path="launchDate" label="预热时间">
<NDatePicker
v-model:value="form.launchDate"
type="datetime"
format="yyyy-MM-dd HH:mm"
:is-date-disabled="(ts: number) => ts < Date.now()"
value-format="x"
/>
</NFormItem>
<NFormItem>
<NSpace vertical class="w-full">
<NFormItem path="subscriptionStartDate" label="申购开始时间" label-width="150">
<NDatePicker
v-model:value="form.subscriptionStartDate"
type="datetime"
format="yyyy-MM-dd HH:mm"
:is-date-disabled="(ts: number) => (form.launchDate ? ts < Number(form.launchDate) : false)"
value-format="x"
class="w-full"
/>
</NFormItem>
<NFormItem path="subscriptionEndDate" label="申购结束时间" label-width="150">
<NDatePicker
v-model:value="form.subscriptionEndDate"
type="datetime"
format="yyyy-MM-dd HH:mm"
:is-date-disabled="
(ts: number) => (form.subscriptionStartDate ? ts < Number(form.subscriptionStartDate) : false)
"
value-format="x"
class="w-full"
/>
</NFormItem>
</NSpace>
</NFormItem>
<NSpace justify="end" class="mt-5">
<NButton type="primary" ghost @click="handleCreateDraftAndSubmit">创建并提交审核</NButton>
<NButton type="primary" @click="handleCreateDraft">创建</NButton>
</NSpace>
</NForm>
</div>
</template>
<style lang="css" scoped></style>

View File

@@ -1,153 +0,0 @@
<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: 200
}
})
);
const form = ref<Body>({
iconifyIcon: '',
name: '',
code: '',
categoryId: '',
description: '',
estimatedValue: '1',
totalSupplyLimit: '100',
proofDocumentIds: []
});
const rules: FormRules = {
iconifyIcon: [{ required: true, message: '请选择产品图标', trigger: ['blur', 'input'] }],
name: [{ required: true, message: '请输入产品名称', trigger: ['blur', 'input'] }],
code: [
{ required: true, message: '请输入产品编号', trigger: ['blur', 'input'] },
{
trigger: ['blur'],
asyncValidator: async (rule, value, callback) => {
if (!value) return;
const { data: check } = await safeClient(() =>
client.api.admin.rwa.issuance.products.check_code.get({
query: {
code: value
}
})
);
if (check.value?.exists) {
callback(new Error('产品编号已存在'));
}
callback();
}
}
],
categoryId: [{ required: true, message: '请输入产品类型', trigger: ['blur', 'input'] }],
estimatedValue: [{ required: true, message: '请输入产品估值', trigger: ['blur', 'input'] }],
totalSupplyLimit: [{ required: true, message: '请输入总发行量', trigger: ['blur', 'input'] }]
};
function handleCreateDraft() {
formInst.value?.validate(async errors => {
if (!errors) {
await safeClient(
client.api.admin.rwa.issuance.products.post({
...form.value
})
);
emit('close');
}
});
}
function handleCreateDraftAndSubmit() {
formInst.value?.validate(async errors => {
if (!errors) {
await safeClient(
client.api.admin.rwa.issuance.products.submit.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="iconifyIcon" label="产品图标 ">
<IconPicker v-model="form.iconifyIcon" />
</NFormItem>
<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="产品估值">
<NInputNumber
:min="1"
:step="100"
:precision="2"
:value="Number(form.estimatedValue)"
@update:value="val => (form.estimatedValue = String(val || 0))"
/>
</NFormItem>
<NFormItem path="totalSupplyLimit" label="总发行量">
<NInputNumber
:min="1"
:step="100"
:precision="0"
:value="Number(form.totalSupplyLimit)"
@update:value="val => (form.totalSupplyLimit = String(val || 0))"
/>
</NFormItem>
<NFormItem path="description" label="产品描述">
<NInput v-model:value="form.description" type="textarea" />
</NFormItem>
<NFormItem path="proofDocuments" label="资产证明 ">
<Upload
:model-value="form.proofDocumentIds || []"
:fetch-options="{
businessType: 'rwa_proof'
}"
accept="application/pdf,image/*,.doc,.docx"
@update:model-value="val => (form.proofDocumentIds = val)"
/>
</NFormItem>
<NSpace justify="end">
<NButton type="primary" @click="handleCreateDraftAndSubmit">创建草稿并提交审核</NButton>
<NButton type="primary" @click="handleCreateDraft">创建草稿</NButton>
</NSpace>
</NForm>
</div>
</template>
<style lang="css" scoped></style>

View File

@@ -1,93 +0,0 @@
<script lang="ts" setup>
import { useTemplateRef } from 'vue';
import { useDateFormat } from '@vueuse/core';
import { useDialog } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableInst } from '@/components/table';
const props = defineProps<{
id: string;
}>();
const tableInst = useTemplateRef<TableInst>('tableInst');
const dialog = useDialog();
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.rwa.subscription.orders.get({
query: {
editionId: props.id,
...pagination,
...filter
}
})
);
};
const columns: TableBaseColumns = [
{
title: '分配用户ID',
key: 'userId',
width: 150
},
{
title: '分配数量',
key: 'quantity',
width: 120,
render: (row: any) => {
return Number(row.quantity).toFixed(2);
}
},
{
title: '单价',
key: 'unitPrice',
width: 120,
render: (row: any) => {
return Number(row.unitPrice).toFixed(2);
}
},
{
title: '分配时间',
key: 'createdAt',
render: (row: any) => {
return useDateFormat(row.createdAt, 'YYYY-MM-DD HH:mm').value;
}
},
{
title: '操作',
fixed: 'right',
key: 'operation',
width: 'auto',
operations: row => [
{
contentText: '执行分配',
type: 'primary',
ghost: true,
size: 'small',
onClick: () => {
dialog.create({
title: '执行分配',
content: '确认执行该分配操作?',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
await safeClient(() =>
client.api.admin.rwa.subscription.allocate.post({
editionId: props.id
})
);
tableInst.value?.reload();
}
});
}
}
]
}
];
</script>
<template>
<TableBase ref="tableInst" title="申购记录" :columns="columns" :fetch-data="fetchData" :scroll-x="undefined" />
</template>
<style lang="css" scoped></style>

View File

@@ -1,175 +0,0 @@
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import dayjs from 'dayjs';
import { client, safeClient } from '@/service/api';
type Body = CommonType.TreatyBody<typeof client.api.admin.rwa.issuance.editions.post>;
defineOptions({ name: 'RwaEditionAdd' });
const emit = defineEmits<{
(e: 'close'): void;
}>();
const props = defineProps<{
data: Body;
}>();
const formInst = useTemplateRef<FormInst>('formInst');
const form = ref<Body>({
productId: props.data.productId,
editionName: props.data.editionName,
perUserLimit: props.data.perUserLimit,
totalSupply: props.data.totalSupply,
unitPrice: props.data.unitPrice,
dividendRate: props.data.dividendRate,
launchDate: dayjs().toDate(),
subscriptionStartDate: dayjs().toDate(),
subscriptionEndDate: dayjs().toDate()
});
const rules: FormRules = {
editionName: [{ required: true, message: '请输入发行期名称', trigger: ['blur', 'input'] }],
perUserLimit: [{ required: true, message: '请输入个人申购上限', trigger: ['blur', 'input'] }],
totalSupply: [{ required: true, message: '请输入发行总量', trigger: ['blur', 'input'] }],
unitPrice: [{ required: true, message: '请输入单价', trigger: ['blur', 'input'] }],
dividendRate: [{ required: true, message: '请输入分红率', trigger: ['blur', 'input'] }],
launchDate: [{ required: true, type: 'number', message: '请选择预热时间', trigger: ['blur', 'change'] }],
subscriptionStartDate: [
{ required: true, type: 'number', message: '请选择申购开始时间', trigger: ['blur', 'change'] }
],
subscriptionEndDate: [{ required: true, type: 'number', message: '请选择申购结束时间', trigger: ['blur', 'change'] }]
};
// 手动验证时间逻辑
function validateTimes() {
const now = dayjs();
if (form.value.launchDate && dayjs(form.value.launchDate).isBefore(now)) {
return '预热时间必须在当前时间之后';
}
if (form.value.subscriptionStartDate && form.value.launchDate) {
if (dayjs(form.value.subscriptionStartDate).isBefore(dayjs(form.value.launchDate))) {
return '申购开始时间必须在预热时间之后';
}
}
if (form.value.subscriptionEndDate && form.value.subscriptionStartDate) {
if (dayjs(form.value.subscriptionEndDate).isBefore(dayjs(form.value.subscriptionStartDate))) {
return '申购结束时间必须在开始时间之后';
}
}
return null;
}
function handleSubmit() {
formInst.value?.validate(async errors => {
if (!errors) {
await safeClient(
client.api.admin.rwa.issuance.editions.post({
...form.value,
launchDate: new Date(form.value.launchDate!),
subscriptionStartDate: new Date(form.value.subscriptionStartDate!),
subscriptionEndDate: new Date(form.value.subscriptionEndDate!)
} as Body)
);
emit('close');
}
});
}
</script>
<template>
<div class="my-10">
<NForm
ref="formInst"
:model="form"
label-width="150"
label-placement="left"
label-align="left"
:rules="rules"
require-mark-placement="left"
>
<NFormItem path="editionName" label="发行期名称">
<NInput v-model:value="form.editionName" />
</NFormItem>
<NFormItem path="perUserLimit" label="个人申购上限(份)">
<NInputNumber
:min="1"
:step="10"
:value="Number(form.perUserLimit)"
@update:value="val => (form.perUserLimit = String(val))"
/>
</NFormItem>
<NFormItem path="totalSupply" label="发行总量(份)">
<NInputNumber
:min="1"
:step="100"
:value="Number(form.totalSupply)"
@update:value="val => (form.totalSupply = String(val))"
/>
</NFormItem>
<NFormItem path="unitPrice" label="单价($)">
<NInputNumber :min="0" :value="Number(form.unitPrice)" @update:value="val => (form.unitPrice = String(val))" />
</NFormItem>
<NFormItem path="dividendRate" label="分红率(%)">
<NInputNumber
:max="100"
:min="0"
:step="0.1"
:precision="2"
:value="Number(form.dividendRate) * 100"
@update:value="val => (form.dividendRate = String((val || 0) / 100))"
>
<template #suffix>%</template>
</NInputNumber>
</NFormItem>
<NFormItem path="launchDate" label="预热时间">
<NDatePicker
:value="dayjs(form.launchDate).valueOf()"
type="datetime"
format="yyyy-MM-dd HH:mm"
:is-date-disabled="(ts: number) => ts < Date.now()"
value-format="x"
@update:value="val => (form.launchDate = new Date(val))"
/>
</NFormItem>
<NFormItem>
<NSpace vertical class="w-full">
<NFormItem path="subscriptionStartDate" label="申购开始时间" label-width="150">
<NDatePicker
:value="dayjs(form.subscriptionStartDate).valueOf()"
type="datetime"
format="yyyy-MM-dd HH:mm"
:is-date-disabled="(ts: number) => (form.launchDate ? ts < Number(form.launchDate) : false)"
value-format="x"
class="w-full"
@update:value="val => (form.subscriptionStartDate = new Date(val))"
/>
</NFormItem>
<NFormItem path="subscriptionEndDate" label="申购结束时间" label-width="150">
<NDatePicker
:value="dayjs(form.subscriptionEndDate).valueOf()"
type="datetime"
format="yyyy-MM-dd HH:mm"
:is-date-disabled="
(ts: number) => (form.subscriptionStartDate ? ts < Number(form.subscriptionStartDate) : false)
"
value-format="x"
class="w-full"
@update:value="val => (form.subscriptionEndDate = new Date(val))"
/>
</NFormItem>
</NSpace>
</NFormItem>
<NSpace justify="end" class="mt-5">
<NButton type="primary" @click="handleSubmit">修 改</NButton>
</NSpace>
</NForm>
</div>
</template>
<style lang="css" scoped></style>

View File

@@ -1,148 +0,0 @@
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import type { Treaty } from '@elysiajs/eden';
import { client, safeClient } from '@/service/api';
defineOptions({ name: 'RwaProductEdit' });
const props = defineProps<{
data: Treaty.Data<typeof client.api.admin.rwa.issuance.products.post>;
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
type Body = CommonType.TreatyBody<typeof client.api.admin.rwa.issuance.products.put>;
const formInst = useTemplateRef<FormInst>('formInst');
const { data: categories } = safeClient(
client.api.admin.rwa.category.categories.get({
query: {
pageIndex: 1,
pageSize: 200
}
})
);
const form = ref<Body>({
id: props.data.id,
iconifyIcon: props.data.iconifyIcon,
name: props.data.name,
code: props.data.code,
categoryId: props.data.categoryId,
description: props.data.description,
estimatedValue: props.data.estimatedValue,
totalSupplyLimit: props.data.totalSupplyLimit,
proofDocumentIds: props.data.proofDocumentIds
});
const rules: FormRules = {
iconifyIcon: [{ required: true, message: '请选择产品图标', trigger: ['blur', 'input'] }],
name: [{ required: true, message: '请输入产品名称', trigger: ['blur', 'input'] }],
code: [
{ required: true, message: '请输入产品编号', trigger: ['blur', 'input'] },
{
trigger: ['blur'],
asyncValidator: async (rule, value, callback) => {
if (!value) return;
const { data: check } = await safeClient(() =>
client.api.admin.rwa.issuance.products.check_code.get({
query: {
code: value,
excludeId: props.data.id
}
})
);
if (check.value?.exists) {
callback(new Error('产品编号已存在'));
}
callback();
}
}
],
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.put({
...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="iconifyIcon" label="产品图标 ">
<IconPicker v-model="form.iconifyIcon" />
</NFormItem>
<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="categories?.data?.map(item => ({ label: item.name, value: item.id }))"
@update:value="val => (form.categoryId = val as string)"
/>
</NFormItem>
<NFormItem path="estimatedValue" label="产品估值">
<NInputNumber
:min="1"
:step="100"
:precision="2"
:value="Number(form.estimatedValue)"
@update:value="val => (form.estimatedValue = String(val || 0))"
/>
</NFormItem>
<NFormItem path="totalSupplyLimit" label="总发行量">
<NInputNumber
:min="1"
:step="100"
:precision="0"
:value="Number(form.totalSupplyLimit)"
@update:value="val => (form.totalSupplyLimit = String(val || 0))"
/>
</NFormItem>
<NFormItem path="description" label="产品描述">
<NInput v-model:value="form.description" type="textarea" />
</NFormItem>
<NFormItem path="proofDocumentIds" label="资产证明 ">
<Upload
:model-value="form.proofDocumentIds || []"
:fetch-options="{
businessType: 'rwa_proof'
}"
accept="application/pdf,image/*,.doc,.docx"
@update:model-value="val => (form.proofDocumentIds = val)"
/>
</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,257 +0,0 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { useDateFormat } from '@vueuse/core';
import { useDialog } from 'naive-ui';
import type { Treaty } from '@elysiajs/eden';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableInst } from '@/components/table';
import { RwaEditionStatusEnum } from '@/enum';
import AddEdition from './add-edition.vue';
import EditEdition from './edit-edition.vue';
import Allocations from './allocations.vue';
defineOptions({ name: 'RwaProductEditions' });
const props = defineProps<{
data: Treaty.Data<typeof client.api.admin.rwa.issuance.products.get>['data'][number];
}>();
const tableInst = useTemplateRef<TableInst>('tableInst');
const dialog = useDialog();
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.rwa.issuance.editions.get({
query: {
productId: props.data.id,
...pagination,
...filter
}
})
);
};
const columns: TableBaseColumns = [
{
title: '发行期名称',
key: 'editionName',
editComponent: 'NInput'
},
{
title: '个人申购上限',
key: 'perUserLimit',
render: (row: any) => {
return Number(row.perUserLimit).toFixed(2);
}
},
{
title: '发行总量',
key: 'totalSupply',
render: (row: any) => {
return Number(row.totalSupply).toFixed(2);
}
},
{
title: '单价',
key: 'unitPrice',
render: (row: any) => {
return Number(row.unitPrice).toFixed(2);
}
},
{
title: '分红率',
key: 'dividendRate',
render: (row: any) => {
return `${(Number(row.dividendRate) * 100).toFixed(2)}%`;
}
},
{
title: '预热时间',
key: 'launchDate',
render: (row: any) => {
return useDateFormat(row.launchDate, 'YYYY-MM-DD HH:mm').value;
}
},
{
title: '订阅开始时间',
key: 'subscriptionStartDate',
render: (row: any) => {
return useDateFormat(row.subscriptionStartDate, 'YYYY-MM-DD HH:mm').value;
}
},
{
title: '认购截止时间',
key: 'subscriptionEndDate',
render: (row: any) => {
return useDateFormat(row.subscriptionEndDate, 'YYYY-MM-DD HH:mm').value;
}
},
{
title: '状态',
key: 'status',
render: row => {
return RwaEditionStatusEnum[row.status as keyof typeof RwaEditionStatusEnum];
}
},
{
title: '操作',
fixed: 'right',
key: 'operation',
width: 300,
operations: (row: any) => [
{
contentText: '排期',
type: 'primary',
ghost: true,
size: 'small',
visible: row.status === 'draft' || row.status === 'cancelled',
text: true,
onClick: async () => {
dialog.create({
title: '确认排期吗?',
content: '排期后将对投资者可见,且不可修改。',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
await safeClient(client.api.admin.rwa.issuance.editions({ id: row.id }).publish.post());
tableInst.value?.reload();
}
});
}
},
{
contentText: '取消排期',
type: 'primary',
ghost: true,
size: 'small',
text: true,
visible: row.status === 'scheduled',
onClick: async () => {
dialog.create({
title: '确认取消发布该排期吗?',
content: '取消排期将对投资者不可见,且不可修改。',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
await safeClient(client.api.admin.rwa.issuance.editions({ id: row.id }).cancel.post());
tableInst.value?.reload();
}
});
}
},
{
contentText: '执行分红',
type: 'primary',
ghost: true,
size: 'small',
text: true,
// visible: row.status === 'scheduled',
onClick: async () => {
dialog.create({
title: '确认执行分红吗?',
content: '执行分红后将按照分红率向持有者分配收益。',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
await safeClient(
client.api.admin.rwa.issuance.dividend.distribute.post({
editionId: row.id as string
})
);
tableInst.value?.reload();
}
});
}
},
{
contentText: '申购记录',
type: 'primary',
ghost: true,
size: 'small',
text: true,
visible: row.status === 'scheduled',
onClick: () => handleAllocations(row)
},
{
contentText: '编辑',
type: 'primary',
ghost: true,
size: 'small',
text: true,
// visible: row.status === 'draft',
onClick: () => handleEdit(row)
},
{
contentText: '删除',
type: 'error',
ghost: true,
size: 'small',
text: true,
// visible: row.status === 'draft' || row.status === 'cancelled',
onClick: () => {
dialog.create({
title: '确认删除该排期吗?',
content: '删除后该排期将不可恢复,请谨慎操作。',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
await safeClient(client.api.admin.rwa.issuance.editions({ id: row.id }).delete());
tableInst.value?.reload();
}
});
}
}
]
}
];
function handleAdd() {
const dialogInstance = dialog.create({
title: '添加阶段',
content: () =>
h(AddEdition, {
productId: props.data.id,
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
}),
style: { width: '600px' },
showIcon: false,
onPositiveClick: () => {
tableInst.value?.reload();
}
});
}
function handleEdit(row: any) {
const dialogInstance = dialog.create({
title: '编辑阶段',
content: () =>
h(EditEdition, {
data: row,
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
}),
style: { width: '600px' },
showIcon: false,
onPositiveClick: () => {
tableInst.value?.reload();
}
});
}
function handleAllocations(row: any) {
const dialogInstance = dialog.create({
title: '分配记录',
content: () => h(Allocations, { id: row.id }),
style: { width: '800px', height: '600px' },
showIcon: false
});
}
</script>
<template>
<TableBase ref="tableInst" title="RWA阶段" :columns="columns" :fetch-data="fetchData" @add="handleAdd" />
</template>
<style lang="css" scoped></style>

View File

@@ -1,214 +0,0 @@
<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

@@ -1,311 +0,0 @@
<script lang="ts" setup>
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 SvgIcon from '@/components/custom/svg-icon.vue';
import Add from './components/add.vue';
import Edit from './components/edit.vue';
import Editions from './components/editions.vue';
import Tokenization from './components/tokenization.vue';
const dialog = useDialog();
const message = useMessage();
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.rwa.issuance.products.get({
query: {
...pagination,
...filter
}
})
);
};
const columns: TableBaseColumns = [
{
key: 'selection',
title: '序号',
type: 'selection',
width: 60
},
{
title: '产品图标',
key: 'iconifyIcon',
render: (row: any) => {
return h(SvgIcon, { icon: row.iconifyIcon });
}
},
{
title: '产品代码',
key: 'code'
},
{
title: '产品名称',
key: 'name'
},
{
title: '产品估值',
key: 'estimatedValue',
render: (row: any) => {
return Number(row.estimatedValue).toFixed(2);
}
},
{
title: '产品分类',
key: 'category.name'
},
{
title: '创建人ID',
key: 'creator.username'
},
{
title: '创建时间',
key: 'createdAt',
render: (row: any) => {
return useDateFormat(row.createdAt, 'YYYY-MM-DD HH:mm').value;
}
},
{
title: '状态',
key: 'status',
render: row => {
return RwaStatusEnum[row.status as keyof typeof RwaStatusEnum];
}
},
{
title: '描述',
key: 'description'
},
{
title: '操作',
fixed: 'right',
key: 'operation',
width: 300,
operations: (row: any) => [
{
contentText: '提交到审核',
ghost: true,
visible: row.status === 'draft',
size: 'small',
onClick: async () => {
dialog.create({
title: '提交到审核流程',
content: '确认将该产品提交到审核流程吗?',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
await safeClient(() =>
client.api.admin.rwa.issuance.products({ id: row.id }).submit.post({
submissionNote: '提交审核'
})
);
tableInst.value?.reload();
}
});
}
},
{
contentText: '批准',
ghost: true,
visible: row.status === 'under_review',
size: 'small',
onClick: async () => {
dialog.create({
title: '批准产品',
content: '确认批准该产品吗?',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
await safeClient(() =>
client.api.admin.rwa.issuance.approve.post({
productId: row.id as string,
publishFirstEdition: true
})
);
tableInst.value?.reload();
}
});
}
},
{
contentText: '拒绝',
ghost: true,
visible: row.status === 'under_review',
size: 'small',
onClick: async () => {
dialog.create({
title: '批准产品',
content: '确认批准该产品吗?',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
await safeClient(() =>
client.api.admin.rwa.issuance.reject.post({
productId: row.id as string,
rejectionReason: '不符合要求'
})
);
tableInst.value?.reload();
}
});
}
},
{
contentText: '编辑',
type: 'primary',
ghost: true,
size: 'small',
// visible: row.status === 'draft' || row.status === 'rejected',
onClick: () => {
handleEdit(row);
}
},
{
contentText: '删除',
type: 'error',
ghost: true,
size: 'small',
// visible: row.status === 'draft' || row.status === 'rejected',
onClick: () => {
dialog.create({
title: '删除产品',
content: '确认删除该产品吗,删除后不可恢复。',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
await safeClient(() => client.api.admin.rwa.issuance.products({ id: row.id }).delete());
tableInst.value?.reload();
}
});
}
},
{
contentText: '发行期',
type: 'tertiary',
ghost: true,
size: 'small',
onClick: () => {
handleViewEditions(row);
}
},
{
contentText: '代币化',
type: 'tertiary',
ghost: true,
size: 'small',
onClick: () => {
handleTokenization(row);
}
}
]
}
];
const filterColumns: TableFilterColumns = [
{
title: '产品代码',
key: 'code'
},
{
title: '产品名称',
key: 'name'
},
{
title: '创建时间',
key: 'createdAt',
component: NDatePicker
}
];
function handleTokenization(row: any) {
const dialogInstance = dialog.create({
title: '代币化产品',
content: () =>
h(Tokenization, {
data: row,
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
}),
style: { width: '600px' },
showIcon: false
});
}
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();
}
});
}
function handleEdit(row: any) {
const dialogInstance = dialog.create({
title: '编辑产品',
content: () =>
h(Edit, {
data: row,
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
}),
style: { width: '600px' },
showIcon: false,
onPositiveClick: () => {
message.success('更新成功');
tableInst.value?.reload();
}
});
}
function handleViewEditions(row: any) {
const dialogInstance = dialog.create({
title: '发行期列表',
content: () =>
h(Editions, {
data: row,
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
}),
style: { width: '80vw', minWidth: '1000px', height: '80vh' },
showIcon: false,
onPositiveClick: () => {
message.success('添加成功');
tableInst.value?.reload();
}
});
}
</script>
<template>
<TableBase
ref="tableInst"
show-header-operation
:columns="columns"
:filter-columns="filterColumns"
:fetch-data="fetchData"
@add="handleAdd"
/>
</template>
<style lang="css" scoped></style>

View File

@@ -1,68 +0,0 @@
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import { client, safeClient } from '@/service/api';
type Body = CommonType.TreatyBody<typeof client.api.admin.rwa.category.categories.post>;
const emit = defineEmits<{
(e: 'close'): void;
}>();
const formInst = useTemplateRef<FormInst>('formInst');
const form = ref<Body>({
name: '',
code: '',
description: '',
displayOrder: 0
});
const rules: FormRules = {
name: [{ required: true, message: '请输入分类名称', trigger: ['blur', 'input'] }],
code: [{ required: true, message: '请输入分类编号', trigger: ['blur', 'input'] }]
};
function handleSubmit() {
formInst.value?.validate(async errors => {
if (!errors) {
await safeClient(() =>
client.api.admin.rwa.category.categories.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="description" label="描述">
<NInput v-model:value="form.description" type="textarea" />
</NFormItem>
<NFormItem path="displayOrder" label="排序">
<NInputNumber v-model:value="form.displayOrder" />
</NFormItem>
<NSpace justify="end">
<NButton type="primary" ghost @click="$emit('close')"> </NButton>
<NButton type="primary" @click="handleSubmit"> </NButton>
</NSpace>
</NForm>
</div>
</template>
<style lang="css" scoped></style>

View File

@@ -1,73 +0,0 @@
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import type { Treaty } from '@elysiajs/eden';
import { client, safeClient } from '@/service/api';
type Body = CommonType.TreatyBody<typeof client.api.admin.rwa.category.categories.post>;
const props = defineProps<{
data: Treaty.Data<typeof client.api.admin.rwa.category.categories.post>;
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
const formInst = useTemplateRef<FormInst>('formInst');
const form = ref<Body>({
id: props.data.id,
name: props.data.name,
code: props.data.code,
description: props.data.description,
displayOrder: props.data.displayOrder
});
const rules: FormRules = {
name: [{ required: true, message: '请输入分类名称', trigger: ['blur', 'input'] }],
code: [{ required: true, message: '请输入分类编号', trigger: ['blur', 'input'] }]
};
function handleSubmit() {
formInst.value?.validate(async errors => {
if (!errors) {
await safeClient(() =>
client.api.admin.rwa.category.categories.put({
...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="description" label="描述">
<NInput v-model:value="form.description" type="textarea" />
</NFormItem>
<NFormItem path="displayOrder" label="排序">
<NInputNumber v-model:value="form.displayOrder" />
</NFormItem>
<NSpace justify="end">
<NButton type="primary" ghost @click="$emit('close')"> </NButton>
<NButton type="primary" @click="handleSubmit"> </NButton>
</NSpace>
</NForm>
</div>
</template>
<style lang="css" scoped></style>

View File

@@ -1,121 +0,0 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { useDateFormat } from '@vueuse/core';
import { useDialog, useMessage } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
import Add from './components/add.vue';
import Edit from './components/edit.vue';
const dialog = useDialog();
const message = useMessage();
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.rwa.category.categories.get({
query: {
...pagination,
...filter
}
})
);
};
const columns: TableBaseColumns = [
{
title: 'ID',
key: 'id'
},
{
title: '编号',
key: 'code'
},
{
title: '名称',
key: 'name'
},
{
title: '描述',
key: 'description'
},
{
title: '创建时间',
key: 'createdAt',
render: (row: any) => {
return useDateFormat(row.createdAt, 'YYYY-MM-DD HH:mm').value;
}
},
{
title: '操作',
fixed: 'right',
key: 'operation',
width: 160,
operations: (row: any) => [
{
contentText: '编辑',
type: 'primary',
ghost: true,
size: 'small',
onClick: () => handleEdit(row)
},
{
contentText: '删除',
type: 'error',
ghost: true,
size: 'small',
onClick: async () => {
dialog.create({
title: '提示',
positiveText: '是',
negativeText: '否',
content: '确认删除该信息?',
onPositiveClick: async () => {
await safeClient(() => client.api.admin.rwa.category.categories({ id: row.id as string }).delete());
message.success('删除成功');
tableInst.value?.reload();
}
});
}
}
]
}
];
function handleAdd() {
const dialogInstance = dialog.create({
title: '添加产品类型',
content: () =>
h(Add, {
onClose: () => {
dialogInstance?.destroy();
tableInst.value?.reload();
}
}),
style: { width: '600px' },
closable: true
});
}
function handleEdit(data: any) {
const dialogInstance = dialog.create({
title: '编辑产品类型',
content: () =>
h(Edit, {
data,
onClose: () => {
dialogInstance?.destroy();
tableInst.value?.reload();
}
}),
style: { width: '600px' },
closable: true
});
}
</script>
<template>
<TableBase ref="tableInst" :columns="columns" :fetch-data="fetchData" @add="handleAdd" />
</template>
<style lang="css" scoped></style>

View File

@@ -1,95 +0,0 @@
<script lang="ts" setup>
import { useTemplateRef } from 'vue';
import { useDateFormat } from '@vueuse/core';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableInst } from '@/components/table';
import { RwaSubscribeStatusEnum } from '@/enum';
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.rwa.subscription.orders.get({
query: {
...pagination,
...filter
}
})
);
};
const columns: TableBaseColumns = [
{
key: 'selection',
title: '序号',
type: 'selection',
width: 60
},
{
title: '用户ID',
key: 'userId'
},
{
title: '产品名称',
key: 'product.name'
},
{
title: '申购数量',
key: 'quantity',
render: (row: any) => {
return Number(row.quantity).toFixed(2);
}
},
{
title: '单价',
key: 'unitPrice',
render: (row: any) => {
return Number(row.unitPrice).toFixed(2);
}
},
{
title: '总价',
key: 'totalAmount',
render: (row: any) => {
return Number(row.totalAmount).toFixed(2);
}
},
{
title: '申购时间',
key: 'createdAt',
render: (row: any) => {
return useDateFormat(new Date(row.createdAt), 'YYYY-MM-DD HH:mm:ss').value;
}
},
{
title: '状态',
key: 'status',
render: (row: any) => {
return RwaSubscribeStatusEnum[row.status as keyof typeof RwaSubscribeStatusEnum];
}
},
{
title: '操作',
fixed: 'right',
key: 'operation',
width: 200,
operations: (row: any) => []
}
];
</script>
<template>
<TableBase
ref="tableInst"
show-header-operation
:header-operations="{
add: false,
refresh: true,
columns: true
}"
:columns="columns"
:fetch-data="fetchData"
/>
</template>
<style lang="css" scoped></style>

View File

@@ -1,166 +0,0 @@
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { client, safeClient } from '@/service/api';
defineOptions({ name: 'LockOptions' });
interface LockOption {
months: number;
rewardRate: string;
}
const props = defineProps<{
options: LockOption[];
id: string;
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
const formRef = useTemplateRef<FormInst | null>('formRef');
const lockOptions = ref<LockOption[]>(cloneDeep(props.options));
const rules: FormRules = {};
function addLockOption() {
const newOptions = [...lockOptions.value, { months: 12, rewardRate: '0.1' }];
lockOptions.value = newOptions;
}
function removeLockOption(index: number) {
if (lockOptions.value.length > 1) {
const newOptions = lockOptions.value.filter((_, i) => i !== index);
lockOptions.value = newOptions;
}
}
function updateOption<K extends keyof LockOption>(index: number, field: K, value: LockOption[K]) {
lockOptions.value[index][field] = value;
}
function handleSubmit() {
formRef.value?.validate(async errors => {
if (!errors) {
await safeClient(() =>
client.api.admin.rwa.tokenization_schema({ id: props.id }).lock_options.patch({
lockOptions: lockOptions.value
})
);
window.$message?.success('锁仓选项更新成功');
emit('close');
}
});
}
</script>
<template>
<NForm
ref="formRef"
:model="{ lockOptions }"
:rules="rules"
label-width="auto"
label-placement="left"
require-mark-placement="left"
>
<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 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="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
:value="option.months"
:min="1"
:max="120"
:step="1"
placeholder="请输入锁仓月数"
class="w-full"
@update:value="val => updateOption(index, 'months', val!)"
>
<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 => updateOption(index, '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

@@ -1,162 +0,0 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { useDateFormat } from '@vueuse/core';
import { useDialog } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
import { TokenizationStatusEnum } from '@/enum';
import LockOptions from './components/lock-options.vue';
const dialog = useDialog();
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.rwa.tokenization_schema.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: '链类型',
key: 'chainType'
},
{
title: '合约地址',
key: 'contractAddress'
},
{
title: '交易哈希',
key: 'deployTxHash'
},
{
title: '关联RWA产品',
key: 'productId'
},
{
title: '发行状态',
key: 'status',
width: 120,
render: (row: any) => {
return TokenizationStatusEnum[row.status as keyof typeof TokenizationStatusEnum];
}
},
{
title: '创建时间',
key: 'createdAt',
render: (row: any) => {
return useDateFormat(row.createdAt, 'YYYY-MM-DD HH:mm:ss').value;
}
},
{
title: '操作',
fixed: 'right',
key: 'operation',
width: 300,
operations: (row: any) => [
{
contentText: '启用资产化',
ghost: true,
size: 'small',
visible: row.enabled === false,
onClick: async () => {
await safeClient(
client.api.admin.rwa.tokenization_schema.enable.post({
productId: row.productId,
createTradingPair: true
})
);
window.$message?.success('资产化已启用。');
tableInst.value?.reload();
}
},
{
contentText: '关闭资产化',
ghost: true,
size: 'small',
visible: row.enabled === true,
onClick: async () => {
await safeClient(client.api.admin.rwa.tokenization_schema({ id: row.id }).disable.patch());
window.$message?.success('资产化已关闭。');
tableInst.value?.reload();
}
},
{
contentText: '更新锁仓选项',
ghost: true,
size: 'small',
onClick: async () => {
handleUpdateLockOptions(row);
}
}
]
}
];
const filterColumns: TableFilterColumns = [
{
title: '资产代码',
key: 'assetCode'
}
];
function handleUpdateLockOptions(row: any) {
const dialogInstance = dialog.create({
title: '更新锁仓选项',
content: () =>
h(LockOptions, {
options: row.lockOptions,
id: row.id,
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
}),
style: { width: '600px' }
});
}
</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>

View File

@@ -1,178 +0,0 @@
<script lang="ts" setup>
import { computed, ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import { client, safeClient } from '@/service/api';
type Body = CommonType.TreatyBody<typeof client.api.admin.trading_pairs.post>;
defineOptions({
name: 'TradingPairAdd'
});
const emit = defineEmits<{
(e: 'close'): void;
}>();
const formInst = useTemplateRef<FormInst>('formInst');
const form = ref<Body>({
symbol: '',
name: '',
baseAsset: '',
quoteAsset: 'USDT',
description: '',
minOrderAmount: '0',
minOrderQuantity: '0',
makerFeeRate: '0',
takerFeeRate: '0',
iconUrl: '',
isActive: true,
marginEnabled: false,
maxLeverage: null,
spotEnabled: true
});
// 用于表单输入的临时数字值
const tempValues = ref({
minOrderAmount: 0,
minOrderQuantity: 0,
makerFeeRate: 0,
takerFeeRate: 0,
maxLeverage: 1
});
const rules: FormRules = {
symbol: [{ required: true, message: '请输入交易对标识', trigger: ['blur', 'input'] }],
name: [{ required: true, message: '请输入交易对显示名称', trigger: ['blur', 'input'] }],
baseAsset: [{ required: true, message: '请输入基础资产代码', trigger: ['blur', 'input'] }],
quoteAsset: [{ required: true, message: '请输入计价资产代码', trigger: ['blur', 'input'] }],
minOrderAmount: [{ required: true, message: '请输入最小下单金额', trigger: ['blur', 'change'] }],
minOrderQuantity: [{ required: true, message: '请输入最小下单数量', trigger: ['blur', 'change'] }],
makerFeeRate: [{ required: true, message: '请输入挂单手续费率', trigger: ['blur', 'change'] }],
takerFeeRate: [{ required: true, message: '请输入成交手续费率', trigger: ['blur', 'change'] }]
};
const showMaxLeverage = computed(() => form.value.marginEnabled);
const { data: assets } = safeClient(client.api.admin.assets.get());
const assetsOption = computed(() => {
return assets.value?.data
.filter(item => item.code !== 'USDT')
.map(item => ({
label: item.code,
value: item.code
}));
});
// 同步数字输入到表单字符串
function syncNumberToString() {
form.value.minOrderAmount = tempValues.value.minOrderAmount.toString();
form.value.minOrderQuantity = tempValues.value.minOrderQuantity.toString();
form.value.makerFeeRate = (tempValues.value.makerFeeRate / 100).toString();
form.value.takerFeeRate = (tempValues.value.takerFeeRate / 100).toString();
if (form.value.marginEnabled) {
form.value.maxLeverage = tempValues.value.maxLeverage;
} else {
form.value.maxLeverage = null;
}
}
function handleSubmit() {
formInst.value?.validate(async errors => {
if (!errors) {
syncNumberToString();
await safeClient(() => client.api.admin.trading_pairs.post(form.value));
emit('close');
}
});
}
</script>
<template>
<div class="my-10">
<NForm
ref="formInst"
:model="form"
label-width="140px"
label-placement="left"
:rules="rules"
require-mark-placement="left"
>
<NFormItem path="name" label="交易对显示名称">
<NInput v-model:value="form.name" placeholder="如:比特币/泰达币" />
</NFormItem>
<NFormItem path="iconUrl" label="交易对图标">
<IconPicker v-model="form.iconUrl" :collections="['cryptocurrency-color']" />
</NFormItem>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi path="baseAsset" label="基础资产代码">
<NSelect
:value="form.baseAsset || undefined"
:options="assetsOption"
placeholder="如BTC"
@update:value="val => (form.baseAsset = val)"
/>
</NFormItemGi>
<NFormItemGi path="quoteAsset" label="计价资产代码">
<NInput v-model:value="form.quoteAsset" placeholder="如USDT" readonly disabled />
</NFormItemGi>
</NGrid>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi path="minOrderAmount" label="最小下单金额USDT">
<NInputNumber v-model:value="tempValues.minOrderAmount" :min="0" :precision="2" class="w-full" />
</NFormItemGi>
<NFormItemGi path="minOrderQuantity" label="最小下单数量">
<NInputNumber v-model:value="tempValues.minOrderQuantity" :min="0" :precision="8" class="w-full" />
</NFormItemGi>
</NGrid>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi path="makerFeeRate" label="挂单手续费率">
<NInputNumber v-model:value="tempValues.makerFeeRate" :min="0" :max="100" :step="0.01" class="w-full">
<template #suffix>%</template>
</NInputNumber>
</NFormItemGi>
<NFormItemGi path="takerFeeRate" label="成交手续费率">
<NInputNumber v-model:value="tempValues.takerFeeRate" :min="0" :max="100" :step="0.01" class="w-full">
<template #suffix>%</template>
</NInputNumber>
</NFormItemGi>
</NGrid>
<NFormItem path="description" label="交易对描述">
<NInput v-model:value="form.description" type="textarea" placeholder="请输入交易对描述" />
</NFormItem>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi path="spotEnabled" label="是否支持现货交易">
<NSwitch v-model:value="form.spotEnabled" />
</NFormItemGi>
<NFormItemGi path="isActive" label="是否启用">
<NSwitch v-model:value="form.isActive" />
</NFormItemGi>
<NFormItemGi path="marginEnabled" label="是否支持杠杆交易">
<NSwitch v-model:value="form.marginEnabled" />
</NFormItemGi>
<NFormItemGi v-if="showMaxLeverage" path="maxLeverage" label="最大杠杆倍数">
<NInputNumber v-model:value="tempValues.maxLeverage" :min="1" :max="100" :precision="0" class="w-full">
<template #suffix></template>
</NInputNumber>
</NFormItemGi>
</NGrid>
<NSpace justify="end">
<NButton type="primary" ghost @click="$emit('close')"> </NButton>
<NButton type="primary" @click="handleSubmit"> </NButton>
</NSpace>
</NForm>
</div>
</template>
<style lang="css" scoped></style>

View File

@@ -1,193 +0,0 @@
<script lang="ts" setup>
import { computed, ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import { client, safeClient } from '@/service/api';
type Body = CommonType.TreatyBody<typeof client.api.admin.trading_pairs.post>;
defineOptions({
name: 'TradingPairEdit'
});
const props = defineProps<{
data: Body;
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
const formInst = useTemplateRef<FormInst>('formInst');
const form = ref<Body>({
symbol: props.data.symbol,
name: props.data.name,
baseAsset: props.data.baseAsset,
quoteAsset: props.data.quoteAsset,
description: props.data.description,
minOrderAmount: props.data.minOrderAmount,
minOrderQuantity: props.data.minOrderQuantity,
makerFeeRate: props.data.makerFeeRate,
takerFeeRate: props.data.takerFeeRate,
pricePrecision: props.data.pricePrecision,
iconUrl: props.data.iconUrl,
isActive: props.data.isActive,
marginEnabled: props.data.marginEnabled,
maxLeverage: props.data.maxLeverage,
spotEnabled: props.data.spotEnabled
});
// 用于表单输入的临时数字值
const tempValues = ref({
minOrderAmount: 0,
minOrderQuantity: 0,
makerFeeRate: 0,
takerFeeRate: 0,
maxLeverage: 1
});
const rules: FormRules = {
symbol: [{ required: true, message: '请输入交易对标识', trigger: ['blur', 'input'] }],
name: [{ required: true, message: '请输入交易对显示名称', trigger: ['blur', 'input'] }],
baseAsset: [{ required: true, message: '请输入基础资产代码', trigger: ['blur', 'input'] }],
quoteAsset: [{ required: true, message: '请输入计价资产代码', trigger: ['blur', 'input'] }],
minOrderAmount: [{ required: true, message: '请输入最小下单金额', trigger: ['blur', 'change'] }],
minOrderQuantity: [{ required: true, message: '请输入最小下单数量', trigger: ['blur', 'change'] }],
makerFeeRate: [{ required: true, message: '请输入挂单手续费率', trigger: ['blur', 'change'] }],
takerFeeRate: [{ required: true, message: '请输入成交手续费率', trigger: ['blur', 'change'] }]
};
const showMaxLeverage = computed(() => form.value.marginEnabled);
const { data: assets } = safeClient(client.api.admin.assets.get());
const assetsOption = computed(() => {
return assets.value?.data
.filter(item => item.code !== 'USDT')
.map(item => ({
label: item.code,
value: item.code
}));
});
// 同步数字输入到表单字符串
function syncNumberToString() {
form.value.minOrderAmount = tempValues.value.minOrderAmount.toString();
form.value.minOrderQuantity = tempValues.value.minOrderQuantity.toString();
form.value.makerFeeRate = (tempValues.value.makerFeeRate / 100).toString();
form.value.takerFeeRate = (tempValues.value.takerFeeRate / 100).toString();
if (form.value.marginEnabled) {
form.value.maxLeverage = tempValues.value.maxLeverage;
} else {
form.value.maxLeverage = null;
}
}
function handleSubmit() {
formInst.value?.validate(async errors => {
if (!errors) {
syncNumberToString();
await safeClient(() =>
client.api.admin.trading_pairs.patch(form.value, {
query: { symbol: props.data.symbol }
})
);
emit('close');
}
});
}
</script>
<template>
<div class="my-10">
<NForm
ref="formInst"
:model="form"
label-width="140px"
label-placement="left"
:rules="rules"
require-mark-placement="left"
>
<NFormItem path="name" label="交易对显示名称">
<NInput v-model:value="form.name" placeholder="如:比特币/泰达币" />
</NFormItem>
<NFormItem path="iconUrl" label="交易对图标">
<IconPicker v-model="form.iconUrl" :collections="['cryptocurrency-color']" />
</NFormItem>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi path="baseAsset" label="基础资产代码">
<NSelect
:value="form.baseAsset || undefined"
:options="assetsOption"
placeholder="如BTC"
@update:value="val => (form.baseAsset = val)"
/>
</NFormItemGi>
<NFormItemGi path="quoteAsset" label="计价资产代码">
<NInput v-model:value="form.quoteAsset" placeholder="如USDT" readonly disabled />
</NFormItemGi>
</NGrid>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi path="minOrderAmount" label="最小下单金额USDT">
<NInputNumber v-model:value="tempValues.minOrderAmount" :min="0" :precision="2" class="w-full" />
</NFormItemGi>
<NFormItemGi path="minOrderQuantity" label="最小下单数量">
<NInputNumber v-model:value="tempValues.minOrderQuantity" :min="0" :precision="8" class="w-full" />
</NFormItemGi>
</NGrid>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi path="makerFeeRate" label="挂单手续费率">
<NInputNumber v-model:value="tempValues.makerFeeRate" :min="0" :max="100" :step="0.01" class="w-full">
<template #suffix>%</template>
</NInputNumber>
</NFormItemGi>
<NFormItemGi path="takerFeeRate" label="成交手续费率">
<NInputNumber v-model:value="tempValues.takerFeeRate" :min="0" :max="100" :step="0.01" class="w-full">
<template #suffix>%</template>
</NInputNumber>
</NFormItemGi>
</NGrid>
<NFormItem path="pricePrecision" label="价格小数位数">
<NInputNumber v-model:value="form.pricePrecision" :min="0" :max="8" :precision="0" class="w-full">
<template #suffix></template>
</NInputNumber>
</NFormItem>
<NFormItem path="description" label="交易对描述">
<NInput v-model:value="form.description" type="textarea" placeholder="请输入交易对描述" />
</NFormItem>
<NGrid :cols="2" :x-gap="12">
<NFormItemGi path="spotEnabled" label="是否支持现货交易">
<NSwitch v-model:value="form.spotEnabled" />
</NFormItemGi>
<NFormItemGi path="isActive" label="是否启用">
<NSwitch v-model:value="form.isActive" />
</NFormItemGi>
<NFormItemGi path="marginEnabled" label="是否支持杠杆交易">
<NSwitch v-model:value="form.marginEnabled" />
</NFormItemGi>
<NFormItemGi v-if="showMaxLeverage" path="maxLeverage" label="最大杠杆倍数">
<NInputNumber v-model:value="tempValues.maxLeverage" :min="1" :max="100" :precision="0" class="w-full">
<template #suffix></template>
</NInputNumber>
</NFormItemGi>
</NGrid>
<NSpace justify="end">
<NButton type="primary" ghost @click="$emit('close')"> </NButton>
<NButton type="primary" @click="handleSubmit"> </NButton>
</NSpace>
</NForm>
</div>
</template>
<style lang="css" scoped></style>

View File

@@ -1,251 +0,0 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { NInput, NSelect, NTag, useDialog } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
import Add from './components/add.vue';
import Edit from './components/edit.vue';
const dialog = useDialog();
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.trading_pairs.get({
query: {
...pagination,
...filter
}
})
);
};
const columns: TableBaseColumns = [
{
title: '交易对标识',
key: 'symbol',
width: 150,
fixed: 'left'
},
{
title: '名称',
key: 'name',
width: 150
},
{
title: '基础资产代码',
key: 'baseAsset',
width: 130
},
{
title: '计价资产代码',
key: 'quoteAsset',
width: 130
},
{
title: '挂单手续费率',
key: 'makerFeeRate',
width: 140,
render: row => {
return `${(Number(row.makerFeeRate) * 100).toFixed(2)}%`;
}
},
{
title: '成交手续费率',
key: 'takerFeeRate',
width: 140,
render: row => {
return `${(Number(row.takerFeeRate) * 100).toFixed(2)}%`;
}
},
{
title: '最小下单金额USDT',
key: 'minOrderAmount',
width: 170,
render: row => {
return Number(row.minOrderAmount).toFixed(2);
}
},
{
title: '最大下单金额USDT',
key: 'maxOrderAmount',
width: 170,
render: row => {
return Number(row.maxOrderAmount).toFixed(2);
}
},
{
title: '最小下单数量',
key: 'minOrderQuantity',
width: 130,
render: row => {
return Number(row.minOrderQuantity).toFixed(8);
}
},
{
title: '最大下单数量',
key: 'maxOrderQuantity',
width: 130,
render: row => {
return Number(row.maxOrderQuantity).toFixed(8);
}
},
{
title: '价格小数位数',
key: 'pricePrecision',
width: 120,
align: 'center'
},
{
title: '数量小数位数',
key: 'quantityPrecision',
width: 120,
align: 'center'
},
{
title: '排序权重',
key: 'sortOrder',
width: 100,
align: 'center'
},
{
title: '是否启用',
key: 'isActive',
width: 140,
align: 'center',
render: row => {
return row.isActive ? '是' : '否';
}
},
{
title: '描述',
key: 'description',
width: 200
},
{
title: '操作',
fixed: 'right',
key: 'operation',
width: 200,
operations: (row: any) => [
{
contentText: '编辑',
ghost: true,
size: 'small',
onClick: () => {
handleEdit(row);
}
},
{
contentText: '删除',
type: 'error',
ghost: true,
size: 'small',
onClick: () => {
dialog.create({
title: '删除确认',
content: '确认删除该交易对吗,删除后不可恢复。',
positiveText: '确认',
negativeText: '取消',
onPositiveClick: async () => {
await safeClient(() =>
client.api.admin.trading_pairs.delete(undefined, { query: { symbol: row.symbol } })
);
tableInst.value?.reload();
}
});
}
}
]
}
];
const filterColumns: TableFilterColumns = [
{
title: '关键词',
key: 'keyword',
component: NInput,
componentProps: {
placeholder: '请输入交易对名称或标识',
clearable: true
}
},
{
title: '基础资产',
key: 'baseAsset',
component: NInput,
componentProps: {
placeholder: '请输入基础资产代码',
clearable: true
}
},
{
title: '计价资产',
key: 'quoteAsset',
component: NInput,
componentProps: {
placeholder: '请输入计价资产代码',
clearable: true
}
},
{
title: '是否启用',
key: 'isActive',
component: NSelect,
componentProps: {
placeholder: '请选择状态',
clearable: true,
options: [
{ label: '是', value: true },
{ label: '否', value: false }
]
}
}
];
function handleAdd() {
const dialogInstance = dialog.create({
title: '添加交易对',
showIcon: false,
content: () =>
h(Add, {
onClose: () => {
dialogInstance?.destroy();
tableInst.value?.reload();
}
}),
style: { width: '800px' },
closable: true
});
}
function handleEdit(row: any) {
const dialogInstance = dialog.create({
title: '编辑交易对',
showIcon: false,
content: () =>
h(Edit, {
data: row,
onClose: () => {
dialogInstance?.destroy();
tableInst.value?.reload();
}
}),
style: { width: '800px' },
closable: true
});
}
</script>
<template>
<TableBase
ref="tableInst"
:columns="columns"
:filter-columns="filterColumns"
:fetch-data="fetchData"
:scroll-x="3000"
@add="handleAdd"
/>
</template>
<style lang="css" scoped></style>

View File

@@ -1,140 +0,0 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { useDateFormat } from '@vueuse/core';
import { NInput, NSelect, NTag } from 'naive-ui';
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.transfer.get({
query: {
...filter
}
})
);
};
// 状态枚举映射
const TransferStatusEnum = {
pending: '待完成',
completed: '已完成',
failed: '失败'
};
// 状态标签类型映射
const getStatusTagType = (status: string) => {
const typeMap: Record<string, 'warning' | 'success' | 'error'> = {
pending: 'warning',
completed: 'success',
failed: 'error'
};
return typeMap[status] || 'default';
};
const columns: TableBaseColumns = [
{
title: '订单ID',
key: 'id',
width: 180
},
{
title: '订单号',
key: 'orderNo',
width: 200
},
{
title: '转出账户ID',
key: 'fromUserId',
width: 150
},
{
title: '资产代码',
key: 'assetCode',
width: 120
},
{
title: '金额',
key: 'amount',
width: 150,
render: row => {
return Number(row.amount).toFixed(2);
}
},
{
title: '手续费',
key: 'fee',
width: 120,
render: row => {
return Number(row.fee || 0).toFixed(2);
}
},
{
title: '状态',
key: 'status',
width: 120,
render: (row: any) => {
return h(
NTag,
{
type: getStatusTagType(row.status)
},
{
default: () => TransferStatusEnum[row.status as keyof typeof TransferStatusEnum] || row.status
}
);
}
},
{
title: '创建时间',
key: 'createdAt',
width: 180,
render: (row: any) => {
return useDateFormat(row.createdAt, 'YYYY-MM-DD HH:mm:ss').value;
}
}
];
const filterColumns: TableFilterColumns = [
{
title: '用户ID',
key: 'userId',
component: NInput,
componentProps: {
placeholder: '请输入用户ID',
clearable: true
}
},
{
title: '资产代码',
key: 'assetCode',
component: NInput,
componentProps: {
placeholder: '请输入资产代码',
clearable: true
}
},
{
title: '状态',
key: 'status',
component: NSelect,
componentProps: {
placeholder: '请选择状态',
clearable: true,
options: [
{ label: '待完成', value: 'pending' },
{ label: '已完成', value: 'completed' },
{ label: '失败', value: 'failed' }
]
}
}
];
</script>
<template>
<TableBase ref="tableInst" :columns="columns" :filter-columns="filterColumns" :fetch-data="fetchData" />
</template>
<style lang="css" scoped></style>

View File

@@ -1,134 +0,0 @@
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import type { Treaty } from '@elysiajs/eden';
import { client, safeClient } from '@/service/api';
defineOptions({ name: 'EditBankCard' });
type Data = Treaty.Data<typeof client.api.admin.bank_account.get>['data'][number];
const props = defineProps<{
data: Data;
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
const formRef = useTemplateRef<FormInst | null>('formRef');
const form = ref({
accountName: props.data.accountName,
bankName: props.data.bankName,
bankCode: props.data.bankCode,
riskStatus: props.data.riskStatus,
isVerified: props.data.isVerified || false
});
const riskStatusOptions = [
{ label: '低风险', value: 'low' },
{ label: '中风险', value: 'medium' },
{ label: '高风险', value: 'high' }
];
const rules: FormRules = {
accountName: [
{
required: true,
message: '请输入持卡人姓名',
trigger: ['blur', 'input']
},
{
min: 2,
max: 50,
message: '姓名长度应在2-50个字符之间',
trigger: ['blur', 'input']
}
],
bankName: [
{
required: true,
message: '请输入银行名称',
trigger: ['blur', 'input']
},
{
min: 2,
max: 100,
message: '银行名称长度应在2-100个字符之间',
trigger: ['blur', 'input']
}
],
bankCode: [
{
required: true,
message: '请输入银行编号',
trigger: ['blur', 'input']
}
],
riskStatus: [
{
required: true,
message: '请选择风险等级',
trigger: 'change'
}
]
};
async function handleSubmit() {
formRef.value?.validate(async errors => {
if (!errors) {
const { data } = await safeClient(() =>
client.api.admin.bank_account({ id: props.data.id }).patch({
...form.value
})
);
if (data) {
window.$message?.success('银行卡信息更新成功');
emit('close');
}
}
});
}
</script>
<template>
<NForm
ref="formRef"
:model="form"
:rules="rules"
label-width="100px"
label-placement="left"
require-mark-placement="left"
>
<NFormItem label="持卡人姓名" path="accountName">
<NInput v-model:value="form.accountName" placeholder="请输入持卡人姓名" maxlength="50" show-count />
</NFormItem>
<NFormItem label="银行名称" path="bankName">
<NInput v-model:value="form.bankName" placeholder="请输入银行名称" maxlength="100" show-count />
</NFormItem>
<NFormItem label="银行编号" path="bankCode">
<NInput v-model:value="form.bankCode" placeholder="请输入银行编号" maxlength="50" show-count />
</NFormItem>
<NFormItem label="风险等级" path="riskStatus">
<NSelect v-model:value="form.riskStatus" :options="riskStatusOptions" placeholder="请选择风险等级" />
</NFormItem>
<NFormItem label="是否认证" path="isVerified">
<NSwitch v-model:value="form.isVerified">
<template #checked>已认证</template>
<template #unchecked>未认证</template>
</NSwitch>
</NFormItem>
<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

@@ -1,135 +0,0 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
import Edit from './components/edit.vue';
const dialog = useDialog();
const message = useMessage();
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.bank_account.get({
query: {
...pagination,
...filter
}
})
);
};
const columns: TableBaseColumns = [
{
title: 'ID',
key: 'userId'
},
{
title: '持卡人姓名',
key: 'accountName'
},
{
title: '银行卡名称',
key: 'bankName'
},
{
title: '银行编号',
key: 'bankCode'
},
{
title: '风险等级',
key: 'riskStatus'
},
{
title: '是否认证',
key: 'isVerified',
render: (row: any) => (row.isVerified ? '是' : '否')
},
{
title: '操作',
fixed: 'right',
key: 'operation',
width: 160,
operations: (row: any) => [
{
contentText: '编辑',
type: 'primary',
ghost: true,
size: 'small',
onClick: () => {
handleEdit(row);
}
},
{
contentText: '删除',
type: 'error',
ghost: true,
size: 'small',
onClick: async () => {
dialog.create({
title: '提示',
positiveText: '是',
negativeText: '否',
content: '确认删除该银行信息?',
onPositiveClick: async () => {
await safeClient(() =>
client.api.admin.deposit.reject({ orderId: row.id as string }).post({
reviewNote: '管理员拒绝充值'
})
);
tableInst.value?.reload();
message.success('删除成功');
}
});
}
}
]
}
];
const filterColumns: TableFilterColumns = [
{
title: '银行名称',
key: 'bankName'
},
{
title: '银行编号',
key: 'bankCode'
}
];
function handleEdit(row: any) {
const dialogInstance = dialog.create({
title: '编辑银行卡信息',
content: () =>
h(Edit, {
data: row,
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
}),
style: { width: '800px' },
showIcon: false
});
}
</script>
<template>
<TableBase
ref="tableInst"
show-header-operation
:columns="columns"
:filter-columns="filterColumns"
:fetch-data="fetchData"
:scroll-x="800"
:header-operations="{
add: false,
refresh: true,
columns: true
}"
/>
</template>
<style lang="css" scoped></style>

View File

@@ -1,120 +0,0 @@
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import type { FormInst, FormRules } from 'naive-ui';
import type { Treaty } from '@elysiajs/eden';
import { client, safeClient } from '@/service/api';
defineOptions({ name: 'UserEditForm' });
type Data = Treaty.Data<typeof client.api.admin.users.get>['data'][number];
const props = defineProps<{
data: Data;
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
const formInst = useTemplateRef<FormInst>('formInst');
const form = ref({
userId: props.data.userId,
nickname: props.data.nickname,
email: props.data.user?.email || '',
phoneNumber: props.data.user?.phoneNumber || '',
gender: props.data.gender,
language: props.data.language
});
const genderOptions = [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
{ label: '其他', value: 'other' }
];
const languageOptions = [
{ label: '简体中文', value: 'zh-CN' },
{ label: 'English', value: 'en-US' },
{ label: '繁體中文', value: 'zh-TW' }
];
const rules: FormRules = {
nickname: [
{ required: true, message: '请输入用户名', trigger: ['blur', 'input'] },
{ min: 2, max: 20, message: '用户名长度为2-20个字符', trigger: ['blur', 'input'] }
],
email: [
{ required: false, message: '请输入邮箱', trigger: ['blur', 'input'] },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: ['blur', 'input'] }
],
phoneNumber: [{ required: false, message: '请输入手机号', trigger: ['blur', 'input'] }]
};
const loading = ref(false);
async function handleSubmit() {
await formInst.value?.validate(async errors => {
if (errors) return;
loading.value = true;
try {
await safeClient(() =>
client.api.admin.users({ userId: form.value.userId }).profile.put({
nickname: form.value.nickname,
gender: form.value.gender,
language: form.value.language
})
);
window.$message?.success('更新成功');
emit('close');
} finally {
loading.value = false;
}
});
}
</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="userId" label="用户ID">
<NInput :value="data.userId" disabled />
</NFormItem>
<NFormItem path="uid" label="UID">
<NInput :value="data.uid" disabled />
</NFormItem>
<NFormItem path="nickname" label="用户名">
<NInput v-model:value="form.nickname" placeholder="请输入用户名" />
</NFormItem>
<NFormItem path="email" label="邮箱">
<NInput :value="form.email" disabled placeholder="邮箱不可修改" />
</NFormItem>
<NFormItem path="phoneNumber" label="手机号">
<NInput :value="form.phoneNumber" disabled placeholder="手机号不可修改" />
</NFormItem>
<NFormItem path="referralCode" label="推荐人ID">
<NInput :value="data.referralCode || '无'" disabled />
</NFormItem>
<NFormItem path="gender" label="性别">
<NSelect v-model:value="form.gender" :options="genderOptions" placeholder="请选择性别" clearable />
</NFormItem>
<NFormItem path="language" label="语言">
<NSelect v-model:value="form.language" :options="languageOptions" placeholder="请选择语言" />
</NFormItem>
<NSpace justify="end">
<NButton @click="$emit('close')"> </NButton>
<NButton type="primary" :loading="loading" @click="handleSubmit"> </NButton>
</NSpace>
</NForm>
</div>
</template>
<style lang="css" scoped></style>

View File

@@ -1,132 +0,0 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { NInputNumber, useDialog, useMessage } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import { DepositTypeEnum } from '@/enum';
import type { TableBaseColumns, TableFetchData, TableInst } from '@/components/table';
import EditForm from './components/edit.vue';
const dialog = useDialog();
const message = useMessage();
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.users.get({
query: {
...pagination,
...filter
}
})
);
};
const columns: TableBaseColumns = [
{
title: '用户ID',
key: 'userId'
},
{
title: 'UID',
key: 'uid'
},
{
title: '用户名',
key: 'nickname'
},
{
title: '邮箱',
key: 'user.email'
},
{
title: '手机号',
key: 'user.phoneNumber'
},
{
title: '推荐人ID',
key: 'referralCode'
},
{
title: '性别',
key: 'gender'
},
{
title: '语言',
key: 'language'
},
{
title: '操作',
fixed: 'right',
key: 'operation',
width: 100,
operations: (row: any) => [
{
contentText: '编辑',
type: 'primary',
ghost: true,
size: 'small',
onClick: () => {
handleEdit(row);
}
}
]
}
];
const filterColumns: TableBaseColumns = [
{
title: '用户ID',
key: 'userId'
},
{
title: '用户名',
key: 'nickname'
},
{
title: '邮箱',
key: 'email'
},
{
title: '手机号',
key: 'phoneNumber'
},
{
title: '推荐人ID',
key: 'referralCode'
}
];
function handleEdit(row: any) {
const dialogInstance = dialog.create({
title: '编辑用户',
content: () =>
h(EditForm, {
data: row,
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
}),
style: { width: '700px' },
showIcon: false
});
}
</script>
<template>
<TableBase
ref="tableInst"
show-header-operation
:columns="columns"
:fetch-data="fetchData"
:filter-columns="filterColumns"
:scroll-x="1400"
:header-operations="{
add: false,
refresh: true,
columns: true
}"
/>
</template>
<style lang="css" scoped></style>

View File

@@ -1,70 +0,0 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import type { Treaty } from '@elysiajs/eden';
import { client, safeClient } from '@/service/api';
defineOptions({ name: 'WithdrawApprovedComplete' });
const emit = defineEmits<{
(e: 'close'): void;
}>();
const props = defineProps<{
data: Treaty.Data<typeof client.api.admin.withdraw.approved.get>['data'][number];
}>();
const withdrawType = computed(() => props.data.withdrawMethod);
const input = ref('');
const loading = ref(false);
async function handleSubmit() {
loading.value = true;
await safeClient(
client.api.admin.withdraw({ orderId: props.data.id }).complete.post({
bankTransferProof: withdrawType.value === 'bank' ? input.value : undefined,
txHash: withdrawType.value === 'crypto' ? input.value : undefined,
cashPickupCode: withdrawType.value === 'cash' ? input.value : undefined
})
).finally(() => {
loading.value = false;
});
window.$message?.success('操作成功');
setTimeout(() => {
emit('close');
}, 300);
}
</script>
<template>
<NSpace vertical size="large" class="my-5">
<div>
<div v-if="withdrawType === 'bank'">
<div class="text-md">转账人卡号</div>
<div class="text-2xl">{{ data.bankAccountId }}</div>
</div>
<div v-else-if="withdrawType === 'crypto'">
<div class="text-md">转账人链上地址</div>
<div class="text-2xl">{{ data.toAddress }}</div>
</div>
<div v-else>
<div class="text-md">现金提取码</div>
<div class="text-2xl">{{ data.cashPickupCode }}</div>
</div>
</div>
<NForm>
<NFormItem v-if="withdrawType === 'bank'" label="银行转账凭证">
<NInput v-model:value="input" placeholder="银行转账凭证(可选)" />
</NFormItem>
<NFormItem v-else-if="withdrawType === 'crypto'" label="链上交易哈希">
<NInput v-model:value="input" placeholder="链上交易哈希(可选)" />
</NFormItem>
<NSpace justify="end">
<NButton @click="emit('close')"> </NButton>
<NButton type="primary" :loading="loading" @click="handleSubmit"> </NButton>
</NSpace>
</NForm>
</NSpace>
</template>
<style lang="css" scoped></style>

View File

@@ -1,53 +0,0 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { useDateFormat } from '@vueuse/core';
import type { Treaty } from '@elysiajs/eden';
import type { client } from '@/service/api';
import { WithdrawMethodEnum, WithdrawStatusEnum } from '@/enum';
const props = defineProps<{
data: Treaty.Data<typeof client.api.admin.withdraw.approved.get>['data'][number];
}>();
const withdrawToWhere = computed(() => {
if (props.data.withdrawMethod === 'bank') {
return `银行账户: ${props.data.bankAccountId}`;
} else if (props.data.withdrawMethod === 'crypto') {
return `加密地址: ${props.data.toAddress}`;
}
return `现金提取码: ${props.data.cashPickupCode}`;
});
</script>
<template>
<NDescriptions label-placement="top" bordered :column="2" class="my-4">
<NDescriptionsItem label="ID">
{{ data.id }}
</NDescriptionsItem>
<NDescriptionsItem label="资产账户">
{{ data.assetCode }}
</NDescriptionsItem>
<NDescriptionsItem label="提现金额">
{{ Number(data.amount).toFixed(2) }}
</NDescriptionsItem>
<NDescriptionsItem label="实际到账金额">
{{ Number(data.actualAmount).toFixed(2) }}
</NDescriptionsItem>
<NDescriptionsItem label="提现方式">
{{ WithdrawMethodEnum[data.withdrawMethod as keyof typeof WithdrawMethodEnum] }}
</NDescriptionsItem>
<NDescriptionsItem label="手续费">
{{ Number(data.fee).toFixed(2) }}
</NDescriptionsItem>
<NDescriptionsItem label="提现去向">
{{ withdrawToWhere }}
</NDescriptionsItem>
<NDescriptionsItem label="状态">
{{ WithdrawStatusEnum[data.status as keyof typeof WithdrawStatusEnum] }}
</NDescriptionsItem>
<NDescriptionsItem label="创建时间">
{{ useDateFormat(data.createdAt, 'YYYY-MM-DD HH:mm') }}
</NDescriptionsItem>
</NDescriptions>
</template>
<style lang="css" scoped></style>

View File

@@ -1,163 +0,0 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { useDateFormat } from '@vueuse/core';
import { useDialog, useMessage } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
import { WithdrawMethodEnum, WithdrawStatusEnum } from '@/enum';
import Complete from './components/complete.vue';
import Info from './components/info.vue';
const dialog = useDialog();
const message = useMessage();
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.withdraw.approved.get({
query: {
...pagination,
...filter
}
})
);
};
const columns: TableBaseColumns = [
{
title: 'ID',
key: 'id'
},
{
title: '资产账户',
key: 'assetCode'
},
{
title: '提现金额',
key: 'amount',
render: row => {
return Number(row.amount).toFixed(2);
}
},
{
title: '实际到账金额',
key: 'actualAmount',
render: row => {
return Number(row.actualAmount).toFixed(2);
}
},
{
title: '提现方式',
key: 'withdrawMethod',
render: row => {
return WithdrawMethodEnum[row.withdrawMethod as keyof typeof WithdrawMethodEnum];
}
},
{
title: '手续费',
key: 'fee',
render: row => {
return Number(row.fee).toFixed(2);
}
},
{
title: '状态',
key: 'status',
render: row => {
return WithdrawStatusEnum[row.status as keyof typeof WithdrawStatusEnum];
}
},
{
title: '创建时间',
key: 'createdAt',
render: (row: any) => {
return useDateFormat(row.createdAt, 'YYYY-MM-DD HH:mm').value;
}
},
{
title: '操作',
fixed: 'right',
key: 'operation',
width: 240,
operations: (row: any) => [
{
contentText: '完成',
type: 'primary',
ghost: true,
onClick: () => {
const dialogInstance = dialog.create({
title: '提示',
style: { width: '600px' },
content: () =>
h(Complete, {
data: row,
onClose: () => {
dialogInstance.destroy();
tableInst.value?.reload();
}
})
});
}
},
{
contentText: '拒绝',
type: 'error',
ghost: true,
onClick: async () => {
dialog.create({
title: '提示',
positiveText: '是',
negativeText: '否',
content: '确定拒绝该提现申请吗?',
onPositiveClick: async () => {
await safeClient(() =>
client.api.admin.deposit.reject({ orderId: row.id as string }).post({
reviewNote: '管理员拒绝充值'
})
);
tableInst.value?.reload();
message.success('拒绝成功');
}
});
}
},
{
contentText: '详情',
type: 'tertiary',
ghost: true,
onClick: () => {
dialog.create({
title: '提现详情',
style: { width: '800px' },
content: () => h(Info, { data: row })
});
}
}
]
}
];
const filterColumns: TableFilterColumns = [
{
title: '提现金额',
key: 'amount'
}
];
</script>
<template>
<TableBase
ref="tableInst"
show-header-operation
:columns="columns"
:filter-columns="filterColumns"
:fetch-data="fetchData"
:header-operations="{
add: false,
refresh: true,
columns: true
}"
/>
</template>
<style lang="css" scoped></style>

View File

@@ -1,190 +0,0 @@
<script lang="ts" setup>
import { h, ref, useTemplateRef } from 'vue';
import { useDateFormat } from '@vueuse/core';
import { NDatePicker, NInput, useDialog } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
import TableBase from '@/components/table/table-base.vue';
import { WithdrawMethodEnum, WithdrawStatusEnum } from '@/enum';
const tableInst = useTemplateRef<TableInst>('tableInst');
const dialog = useDialog();
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.withdraw.pending.get({
query: {
...pagination,
...filter
}
})
);
};
const columns: TableBaseColumns = [
{
title: 'ID',
key: 'id'
},
{
title: '资产账户',
key: 'assetCode'
},
{
title: '提现金额',
key: 'amount',
render: row => {
return Number(row.amount).toFixed(2);
}
},
{
title: '实际到账金额',
key: 'actualAmount',
render: row => {
return Number(row.actualAmount).toFixed(2);
}
},
{
title: '提现方式',
key: 'withdrawMethod',
render: row => {
return WithdrawMethodEnum[row.withdrawMethod as keyof typeof WithdrawMethodEnum];
}
},
{
title: '银行卡ID',
key: 'bankAccountId'
},
{
title: '手续费',
key: 'fee',
render: row => {
return Number(row.fee).toFixed(2);
}
},
{
title: '状态',
key: 'status',
render: row => {
return WithdrawStatusEnum[row.status as keyof typeof WithdrawStatusEnum];
}
},
{
title: '创建时间',
key: 'createdAt',
render: (row: any) => {
return useDateFormat(row.createdAt, 'YYYY-MM-DD HH:mm').value;
}
},
{
title: '操作',
fixed: 'right',
width: 180,
key: 'operation',
operations: (row: any) => [
{
contentText: '通过',
type: 'primary',
ghost: true,
visible: row.status === 'pending',
onClick: () => {
const reason = ref<string>('');
dialog.create({
title: '通过提现',
positiveText: '是',
negativeText: '否',
showIcon: false,
content: () =>
h(NInput, {
type: 'textarea',
rows: 4,
placeholder: '请输入通过理由(可选)',
'onUpdate:value': value => {
reason.value = value;
}
}),
onPositiveClick: async () => {
await safeClient(() =>
client.api.admin.withdraw({ orderId: row.id }).approve.post({
reviewNote: reason.value
})
);
tableInst.value?.reload();
}
});
}
},
{
contentText: '拒绝',
type: 'error',
ghost: true,
visible: row.status === 'pending',
onClick: async () => {
const reason = ref<string>('');
dialog.create({
title: '拒绝提现',
positiveText: '是',
negativeText: '否',
showIcon: false,
content: () =>
h(NInput, {
type: 'textarea',
rows: 4,
placeholder: '请输入拒绝理由',
'onUpdate:value': value => {
reason.value = value;
}
}),
onPositiveClick: () => {
return new Promise<void>((resolve, reject) => {
if (!reason.value) {
window.$message?.error('请输入拒绝理由');
reject(new Error('请输入拒绝理由'));
} else {
safeClient(() =>
client.api.admin.withdraw({ orderId: row.id }).reject.post({
rejectReason: reason.value
})
).then(() => {
tableInst.value?.reload();
resolve();
});
}
});
}
});
}
}
]
}
];
const filterColumns: TableFilterColumns = [
{
title: 'id',
key: 'id'
},
{
title: '提现金额',
key: 'amount',
component: NDatePicker
}
];
</script>
<template>
<TableBase
ref="tableInst"
show-header-operation
:columns="columns"
:filter-columns="filterColumns"
:fetch-data="fetchData"
:header-operations="{
add: false,
refresh: true,
columns: true
}"
/>
</template>
<style lang="css" scoped></style>