Compare commits

...

10 Commits

34 changed files with 915 additions and 79 deletions

View File

@@ -1,5 +1,5 @@
# backend service base url, test environment
VITE_SERVICE_BASE_URL=https://capp-api.riwsan.com
VITE_SERVICE_BASE_URL=http://192.168.1.2:9538
# other backend service base url, test environment
VITE_OTHER_SERVICE_BASE_URL= `{}`

View File

@@ -51,7 +51,7 @@
"@better-scroll/core": "2.5.1",
"@elysiajs/eden": "^1.4.5",
"@iconify/vue": "5.0.0",
"@riwa/api-types": "http://192.168.1.2:9538/api/capp-eden-0.0.38.tgz",
"@riwa/api-types": "http://192.168.1.2:9538/api/capp-eden-0.0.45.tgz",
"@sa/axios": "workspace:*",
"@sa/color": "workspace:*",
"@sa/hooks": "workspace:*",

12
pnpm-lock.yaml generated
View File

@@ -18,8 +18,8 @@ importers:
specifier: 5.0.0
version: 5.0.0(vue@3.5.25(typescript@5.9.3))
'@riwa/api-types':
specifier: http://192.168.1.2:9538/api/capp-eden-0.0.38.tgz
version: '@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.38.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))'
specifier: http://192.168.1.2:9538/api/capp-eden-0.0.45.tgz
version: '@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.45.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))'
'@sa/axios':
specifier: workspace:*
version: link:packages/axios
@@ -496,9 +496,9 @@ packages:
'@borewit/text-codec@0.1.1':
resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==}
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.38.tgz':
resolution: {tarball: http://192.168.1.2:9538/api/capp-eden-0.0.38.tgz}
version: 0.0.38
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.45.tgz':
resolution: {tarball: http://192.168.1.2:9538/api/capp-eden-0.0.45.tgz}
version: 0.0.45
peerDependencies:
'@elysiajs/eden': ^1.4.6
@@ -4871,7 +4871,7 @@ snapshots:
'@borewit/text-codec@0.1.1': {}
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.38.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))':
'@capp/eden@http://192.168.1.2:9538/api/capp-eden-0.0.45.tgz(@elysiajs/eden@1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3)))':
dependencies:
'@elysiajs/eden': 1.4.5(elysia@1.4.19(@sinclair/typebox@0.34.41)(exact-mirror@0.2.5(@sinclair/typebox@0.34.41))(file-type@21.1.1)(openapi-types@12.1.3)(typescript@5.9.3))

View File

@@ -47,8 +47,9 @@ function refresh() {
<template>
<NSpace :align="itemAlign" justify="space-between" wrap class="lt-sm:w-200px">
<slot name="prefix"></slot>
<div class="space-x-5">
<div class="flex items-center space-x-5">
<slot name="default">
<slot name="suffix"></slot>
<NButton v-if="operations?.add" size="small" ghost type="primary" @click="add">
<template #icon>
<icon-ic-round-plus class="text-icon" />
@@ -78,7 +79,6 @@ function refresh() {
</NButton>
<TableColumnSetting v-if="operations?.columns" v-model:columns="columns" />
</div>
<slot name="suffix"></slot>
</NSpace>
</template>

View File

@@ -146,6 +146,9 @@ defineExpose({} as Expose);
<template #prefix>
<div class="text-lg font-bold">{{ title }}</div>
</template>
<template #suffix>
<slot name="header-operation-suffix" />
</template>
</TableHeaderOperation>
<NDataTable

View File

@@ -49,7 +49,7 @@ function handleConfirm() {
<template>
<div class="rounded-lg bg-white p-5 dark:bg-container">
<NForm :label-width="80" label-align="left" label-placement="left" :show-feedback="false">
<NGrid x-gap="20" :cols="filterColumnsCount || 4">
<NGrid x-gap="20" y-gap="10" :cols="filterColumnsCount || 4">
<NGi v-for="col in columns" :key="col.key">
<NFormItem :label="col.title" :path="col.key">
<component

View File

@@ -1,14 +1,12 @@
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import { ref, watch } from 'vue';
import type { UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import { type UploadFetchOptions, uploadToS3 } from '@/utils/aws/s3';
defineOptions({ name: 'UploadS3' });
const props = withDefaults(
defineProps<{
modelValue?: string[];
label?: string;
placeholder?: string;
accept?: string;
@@ -18,7 +16,6 @@ const props = withDefaults(
fetchOptions: Partial<UploadFetchOptions>;
}>(),
{
modelValue: () => [],
accept: '*/*',
multiple: true,
maxSize: 10,
@@ -28,19 +25,17 @@ const props = withDefaults(
}
);
const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void;
}>();
const model = defineModel({ type: Array as () => string[], default: () => [] });
const fileList = ref<UploadFileInfo[]>([]);
const loading = ref(false);
async function initFileList() {
const fileIds = props.modelValue.filter(url => url && url.trim() !== '');
if (!props.modelValue || fileIds.length === 0) {
const fileIds = model.value.filter(url => url && url.trim() !== '');
if (!model.value || fileIds.length === 0) {
fileList.value = [];
} else {
fileList.value = props.modelValue.map(url => ({
fileList.value = model.value.map(url => ({
id: url,
name: url.split('/').pop() || 'file',
url,
@@ -50,19 +45,15 @@ async function initFileList() {
}
watch(
() => props.modelValue,
() => model.value,
(newVal, oldVal) => {
if (JSON.stringify(newVal) !== JSON.stringify(oldVal)) {
initFileList();
}
},
{ deep: true }
{ deep: true, immediate: true }
);
onMounted(() => {
initFileList();
});
async function handleCustomRequest({ file, onProgress, onFinish, onError }: UploadCustomRequestOptions) {
try {
const fileSizeMB = file.file!.size / 1024 / 1024;
@@ -96,8 +87,7 @@ async function handleCustomRequest({ file, onProgress, onFinish, onError }: Uplo
file.status = 'finished';
file.url = res.publicUrl;
const values = [...props.modelValue, res.publicUrl].filter(Boolean);
emit('update:modelValue', values);
model.value = [...model.value, res.publicUrl].filter(Boolean);
onFinish();
window.$message?.success(`文件 ${file.name} 上传成功`);
@@ -108,9 +98,9 @@ async function handleCustomRequest({ file, onProgress, onFinish, onError }: Uplo
function handleRemove({ file }: { file: UploadFileInfo }) {
// 只删除上传成功的文件(有有效 id 的文件)
if (file.url && typeof file.url === 'string' && props.modelValue.includes(file.url)) {
const newFileIds = props.modelValue.filter(url => url !== file.url);
emit('update:modelValue', newFileIds);
if (file.url && typeof file.url === 'string' && model.value.includes(file.url)) {
const newFileIds = model.value.filter(url => url !== file.url);
model.value = newFileIds;
}
return true;
}
@@ -139,13 +129,16 @@ function beforeUpload({ file }: { file: UploadFileInfo }) {
:on-before-upload="beforeUpload"
list-type="text"
show-download-button
directory-dnd
>
<NButton>
<template #icon>
<icon-ic-round-upload class="text-icon" />
</template>
{{ placeholder || '选择文件' }}
</NButton>
<slot>
<NButton>
<template #icon>
<icon-ic-round-upload class="text-icon" />
</template>
{{ placeholder || '选择文件' }}
</NButton>
</slot>
</NUpload>
</NSpin>
<div class="mt-2 text-12px text-gray-400">

View File

@@ -236,7 +236,10 @@ const local: App.I18n.Schema = {
kyc: 'KYC',
check: 'CheckIn',
referral: 'Referral',
deposit: 'Deposit'
deposit: 'Deposit',
subscription: 'Subscription',
user_user: 'Users',
user_wallet: 'Wallet Logs'
},
page: {
login: {

View File

@@ -230,9 +230,12 @@ const local: App.I18n.Schema = {
withdraw: '提现管理',
wallet: '钱包管理',
kyc: '实名管理',
check: '签到管理',
check: '签到记录',
referral: '推广管理',
deposit: '充值管理'
deposit: '充值管理',
subscription: '认购记录',
user_user: '用户列表',
user_wallet: '上分记录'
},
page: {
login: {

View File

@@ -26,7 +26,9 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
news: () => import("@/views/news/index.vue"),
product: () => import("@/views/product/index.vue"),
referral: () => import("@/views/referral/index.vue"),
user: () => import("@/views/user/index.vue"),
subscription: () => import("@/views/subscription/index.vue"),
user_user: () => import("@/views/user/user/index.vue"),
user_wallet: () => import("@/views/user/wallet/index.vue"),
wallet: () => import("@/views/wallet/index.vue"),
withdraw: () => import("@/views/withdraw/index.vue"),
};

View File

@@ -49,6 +49,16 @@ export const generatedRoutes: GeneratedRoute[] = [
order: 5
}
},
{
name: 'deposit',
path: '/deposit',
component: 'layout.base$view.deposit',
meta: {
title: 'deposit',
i18nKey: 'route.deposit',
order: 7
}
},
{
name: 'iframe-page',
path: '/iframe-page/:url',
@@ -104,15 +114,55 @@ export const generatedRoutes: GeneratedRoute[] = [
order: 3
}
},
{
name: 'referral',
path: '/referral',
component: 'layout.base$view.referral',
meta: {
title: 'referral',
i18nKey: 'route.referral',
order: 9
}
},
{
name: 'subscription',
path: '/subscription',
component: 'layout.base$view.subscription',
meta: {
title: 'subscription',
i18nKey: 'route.subscription',
order: 9
}
},
{
name: 'user',
path: '/user',
component: 'layout.base$view.user',
component: 'layout.base',
meta: {
title: 'user',
i18nKey: 'route.user',
order: 4
}
},
children: [
{
name: 'user_user',
path: '/user/user',
component: 'view.user_user',
meta: {
title: 'user_user',
i18nKey: 'route.user_user'
}
},
{
name: 'user_wallet',
path: '/user/wallet',
component: 'view.user_wallet',
meta: {
title: 'user_wallet',
i18nKey: 'route.user_wallet'
}
}
]
},
{
name: 'wallet',
@@ -124,16 +174,6 @@ export const generatedRoutes: GeneratedRoute[] = [
order: 6
}
},
{
name: 'deposit',
path: '/deposit',
component: 'layout.base$view.deposit',
meta: {
title: 'deposit',
i18nKey: 'route.deposit',
order: 7
}
},
{
name: 'withdraw',
path: '/withdraw',
@@ -143,15 +183,5 @@ export const generatedRoutes: GeneratedRoute[] = [
i18nKey: 'route.withdraw',
order: 8
}
},
{
name: 'referral',
path: '/referral',
component: 'layout.base$view.referral',
meta: {
title: 'referral',
i18nKey: 'route.referral',
order: 9
}
},
}
];

View File

@@ -174,7 +174,10 @@ const routeMap: RouteMap = {
"news": "/news",
"product": "/product",
"referral": "/referral",
"subscription": "/subscription",
"user": "/user",
"user_user": "/user/user",
"user_wallet": "/user/wallet",
"wallet": "/wallet",
"withdraw": "/withdraw"
};

View File

@@ -14,7 +14,6 @@ const client = treaty<App>(baseURL, {
headers() {
const token = localStg.get('token');
return {
'Content-Type': 'application/json',
Authorization: token ? `Bearer ${token}` : ''
};
}

View File

@@ -63,6 +63,7 @@ declare module 'vue' {
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
NGi: typeof import('naive-ui')['NGi']
NGrid: typeof import('naive-ui')['NGrid']
NIcon: typeof import('naive-ui')['NIcon']
NInput: typeof import('naive-ui')['NInput']
NInputGroup: typeof import('naive-ui')['NInputGroup']
NInputNumber: typeof import('naive-ui')['NInputNumber']
@@ -71,6 +72,7 @@ declare module 'vue' {
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
NModal: typeof import('naive-ui')['NModal']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NP: typeof import('naive-ui')['NP']
NPopover: typeof import('naive-ui')['NPopover']
NResult: typeof import('naive-ui')['NResult']
NRow: typeof import('naive-ui')['NRow']
@@ -82,8 +84,10 @@ declare module 'vue' {
NSwitch: typeof import('naive-ui')['NSwitch']
NTab: typeof import('naive-ui')['NTab']
NTabs: typeof import('naive-ui')['NTabs']
NText: typeof import('naive-ui')['NText']
NTooltip: typeof import('naive-ui')['NTooltip']
NUpload: typeof import('naive-ui')['NUpload']
NUploadDragger: typeof import('naive-ui')['NUploadDragger']
NWatermark: typeof import('naive-ui')['NWatermark']
PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
ReloadButton: typeof import('./../components/common/reload-button.vue')['default']
@@ -156,6 +160,7 @@ declare global {
const NFormItemGi: typeof import('naive-ui')['NFormItemGi']
const NGi: typeof import('naive-ui')['NGi']
const NGrid: typeof import('naive-ui')['NGrid']
const NIcon: typeof import('naive-ui')['NIcon']
const NInput: typeof import('naive-ui')['NInput']
const NInputGroup: typeof import('naive-ui')['NInputGroup']
const NInputNumber: typeof import('naive-ui')['NInputNumber']
@@ -164,6 +169,7 @@ declare global {
const NMessageProvider: typeof import('naive-ui')['NMessageProvider']
const NModal: typeof import('naive-ui')['NModal']
const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
const NP: typeof import('naive-ui')['NP']
const NPopover: typeof import('naive-ui')['NPopover']
const NResult: typeof import('naive-ui')['NResult']
const NRow: typeof import('naive-ui')['NRow']
@@ -175,8 +181,10 @@ declare global {
const NSwitch: typeof import('naive-ui')['NSwitch']
const NTab: typeof import('naive-ui')['NTab']
const NTabs: typeof import('naive-ui')['NTabs']
const NText: typeof import('naive-ui')['NText']
const NTooltip: typeof import('naive-ui')['NTooltip']
const NUpload: typeof import('naive-ui')['NUpload']
const NUploadDragger: typeof import('naive-ui')['NUploadDragger']
const NWatermark: typeof import('naive-ui')['NWatermark']
const PinToggler: typeof import('./../components/common/pin-toggler.vue')['default']
const ReloadButton: typeof import('./../components/common/reload-button.vue')['default']

View File

@@ -28,7 +28,10 @@ declare module "@elegant-router/types" {
"news": "/news";
"product": "/product";
"referral": "/referral";
"subscription": "/subscription";
"user": "/user";
"user_user": "/user/user";
"user_wallet": "/user/wallet";
"wallet": "/wallet";
"withdraw": "/withdraw";
};
@@ -73,6 +76,7 @@ declare module "@elegant-router/types" {
| "news"
| "product"
| "referral"
| "subscription"
| "user"
| "wallet"
| "withdraw"
@@ -103,7 +107,9 @@ declare module "@elegant-router/types" {
| "news"
| "product"
| "referral"
| "user"
| "subscription"
| "user_user"
| "user_wallet"
| "wallet"
| "withdraw"
>;

View File

@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { useTemplateRef } from 'vue';
import { NDatePicker } from 'naive-ui';
import { NDatePicker, NSelect } from 'naive-ui';
import dayjs from 'dayjs';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
@@ -25,6 +25,13 @@ const columns: TableBaseColumns = [
key: 'user.name',
title: '用户'
},
{
key: 'checkInType',
title: '签到类型',
render: (row: any) => {
return row.checkInType === 'meeting' ? '会议签到' : '应用签到';
}
},
{
key: 'checkInAt',
title: '签到时间',
@@ -40,6 +47,17 @@ const filterColumns: TableFilterColumns = [
title: '用户',
component: UserSelect
},
{
key: 'checkInType',
title: '签到类型',
component: NSelect,
componentProps: {
options: [
{ label: '会议签到', value: 'meeting' },
{ label: '应用签到', value: 'app' }
]
}
},
{
key: 'startDate',
title: '开始日期',

View File

@@ -25,9 +25,7 @@ enum DepositStatus {
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.deposit.pending.get({ query: { userId: props.userId, ...pagination, ...filter } })
);
return safeClient(() => client.api.admin.deposit.get({ query: { userId: props.userId, ...pagination, ...filter } }));
};
const columns: TableBaseColumns = [
@@ -85,7 +83,7 @@ const columns: TableBaseColumns = [
negativeText: '取消',
onPositiveClick: async () => {
await safeClient(() =>
client.api.admin.withdraw({ orderId: row.id }).approve.post({
client.api.admin.deposit.approve({ orderId: row.id }).post({
reviewNote: `管理员通过于 ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`
})
);
@@ -110,8 +108,8 @@ const columns: TableBaseColumns = [
negativeText: '取消',
onPositiveClick: async () => {
await safeClient(() =>
client.api.admin.withdraw({ orderId: row.id }).reject.post({
rejectReason: `管理员拒绝于 ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`
client.api.admin.deposit.reject({ orderId: row.id }).post({
reviewNote: `管理员拒绝于 ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`
})
);
window.$message?.success('操作成功');

View File

@@ -0,0 +1,17 @@
<script lang="ts" setup>
import SubscriptionPage from '@/views/subscription/index.vue';
defineOptions({
name: 'SubscriptionDialog'
});
const props = defineProps<{
productId?: string;
}>();
</script>
<template>
<SubscriptionPage :product-id="productId" :scroll-x="1200" :show-header-operation="false" />
</template>
<style lang="css" scoped></style>

View File

@@ -6,6 +6,7 @@ import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableInst } from '@/components/table';
import Add from './components/add.vue';
import Edit from './components/edit.vue';
import Records from './components/records.vue';
const message = useMessage();
const dialog = useDialog();
@@ -97,9 +98,9 @@ const columns: TableBaseColumns = [
{
key: 'operations',
title: '操作',
width: 200,
width: 240,
fixed: 'right',
operations: row => [
operations: (row: any) => [
{
contentText: '编辑',
size: 'small',
@@ -127,6 +128,18 @@ const columns: TableBaseColumns = [
}
});
}
},
{
contentText: '认购记录',
size: 'small',
onClick() {
dialog.create({
title: '认购记录',
showIcon: false,
style: { width: '1000px' },
content: () => h(Records, { productId: row.id })
});
}
}
]
}

View File

@@ -0,0 +1,138 @@
<script lang="ts" setup>
import { useTemplateRef } from 'vue';
import { NSelect } from 'naive-ui';
import dayjs from 'dayjs';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
import UserSelect from '@/components/common/user-select.vue';
defineOptions({
name: 'DepositPage'
});
const props = defineProps<{
userId?: string;
productId?: string;
}>();
enum DepositStatus {
pending = '确认中',
locked = '认购成功',
unlocked = '收益已发放',
cancelled = '已取消'
}
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.subscription.orders.get({
query: { userId: props.userId, productId: props.productId, ...pagination, ...filter }
})
);
};
const columns: TableBaseColumns = [
{
key: 'product.name',
title: '产品名称',
fixed: 'left'
},
{
key: 'user.name',
title: '认购用户'
},
{
key: 'user.username',
title: '认购用户手机号'
},
{
key: 'walletType.name',
title: '认购钱包'
},
{
key: 'price',
title: '价格(元)',
render: (row: any) => {
return Number(row.price);
}
},
{
key: 'maturityYield',
title: '到期收益(元)',
render: (row: any) => {
return Number(row.maturityYield);
}
},
{
key: 'cycleDays',
title: '周期(天)'
},
{
key: 'status',
title: '订阅状态',
render: (row: any) => {
return DepositStatus[row.status];
}
},
{
key: 'createdAt',
title: '创建时间',
render: (row: any) => {
return dayjs(row.createdAt).format('YYYY-MM-DD HH:mm');
}
}
// {
// key: 'operations',
// title: '操作',
// width: 150,
// fixed: 'right',
// operations: (row: any) => []
// }
];
const filterColumns: TableFilterColumns = [
{
key: 'userId',
title: '用户',
component: UserSelect
},
{
key: 'productId',
title: '产品ID'
},
{
key: 'status',
title: '状态',
component: NSelect,
componentProps: {
clearable: true,
options: [
{ label: '确认中', value: 'pending' },
{ label: '认购成功', value: 'locked' },
{ label: '收益已发放', value: 'unlocked' },
{ label: '已取消', value: 'cancelled' }
]
}
}
];
</script>
<template>
<TableBase
ref="tableInst"
:fetch-data="fetchData"
:columns="columns"
:filter-columns="filterColumns"
:scroll-x="1500"
:show-header-operation="true"
:header-operations="{
add: false,
refresh: true,
columns: true
}"
v-bind="{ ...$attrs }"
/>
</template>
<style lang="css" scoped></style>

View File

@@ -0,0 +1,131 @@
<script lang="ts" setup>
import { ref } from 'vue';
import type { UploadCustomRequestOptions, UploadFileInfo } from 'naive-ui';
import { client, safeClient } from '@/service/api';
import { localStg } from '@/utils/storage';
import { type UploadFetchOptions, uploadToS3 } from '@/utils/aws/s3';
import IconParkOutlineUpload from '~icons/icon-park-outline/upload';
const downloading = ref(false);
const loading = ref(false);
const fileId = ref('');
const emit = defineEmits<{
(e: 'close'): void;
}>();
const { data: template, execute } = safeClient(() => client.api.admin.wallet_import.template.get(), {
immediate: false
});
async function handleDownloadTemplate() {
downloading.value = true;
try {
await execute();
if (template.value) {
const blob = new Blob([template.value]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `批量上分模版-${new Date().getTime()}.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
window.$message?.success('模板下载成功');
}
} catch (error) {
window.$message?.error('模板下载失败');
} finally {
downloading.value = false;
}
}
async function handleCustomRequest({ file, onProgress, onFinish, onError }: UploadCustomRequestOptions) {
try {
const options: UploadFetchOptions = {
fileName: file.name,
fileSize: file.file!.size,
businessType: 'wallet_import'
};
const res = await uploadToS3(file.file as File, {
fetchOptions: options,
onProgress: percent => {
onProgress({ percent });
}
});
fileId.value = res.fileId;
onFinish();
window.$message?.success(`文件 ${file.name} 上传成功`);
} catch (error: any) {
onError();
}
}
function beforeUpload({ file }: { file: UploadFileInfo }) {
const fileSizeMB = file.file!.size / 1024 / 1024;
if (fileSizeMB > 100) {
window.$message?.error(`文件大小不能超过 100MB`);
return false;
}
return true;
}
async function handleSubmit() {
if (fileId.value === '') {
window.$message?.error('请先上传文件');
return;
}
loading.value = true;
await safeClient(
client.api.admin.wallet_import.jobs.post({
fileId: fileId.value
})
).finally(() => {
loading.value = false;
});
window.$message?.success('操作成功');
emit('close');
}
</script>
<template>
<NSpace vertical class="m-10">
<NSpin :show="loading">
<NUpload
:max="1"
accept=".xlsx,.xls"
show-download-button
directory-dnd
:custom-request="handleCustomRequest"
:on-before-upload="beforeUpload"
>
<NUploadDragger>
<div class="mb-5">
<NIcon size="48" :depth="3">
<IconParkOutlineUpload />
</NIcon>
</div>
<NText class="text-sm">点击或者拖动文件到该区域来上传</NText>
<NP depth="3" class="mt-2">请不要上传敏感数据比如你的银行卡号和密码信用卡号有效期和安全码</NP>
</NUploadDragger>
</NUpload>
</NSpin>
<div class="mt-2 text-12px text-gray-400">
<div>单个文件大小不超过 100MB</div>
<div>支持格式.xlsx,.xls</div>
</div>
<NButton text type="primary" :loading="downloading" @click="handleDownloadTemplate">点击下载Excel模版</NButton>
</NSpace>
<div class="mt-16px flex justify-end gap-12px">
<NButton @click="emit('close')">取消</NButton>
<NButton type="primary" :loading="loading" @click="handleSubmit">确定</NButton>
</div>
</template>
<style lang="css" scoped></style>

View File

@@ -0,0 +1,126 @@
<script lang="ts" setup>
import { computed, ref } from 'vue';
import type { FormInst, FormRules, SelectOption } from 'naive-ui';
import { client, safeClient } from '@/service/api';
defineOptions({
name: 'AdjustWallet'
});
const props = defineProps<{
userIds: string[];
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
interface AdjustForm {
amount: number;
walletTypeId: string | undefined;
}
const formRef = ref<FormInst | null>(null);
const loading = ref(false);
const { data: wallets } = safeClient(client.api.admin.wallet_types.get());
const walletOptions = computed<SelectOption[]>(() => {
if (!wallets.value?.data) return [];
return wallets.value.data.map((item: any) => ({
label: item.name,
value: item.id
}));
});
const formModel = ref<AdjustForm>({
amount: 0,
walletTypeId: undefined
});
const rules: FormRules = {
amount: [
{ required: true, type: 'number', message: '请输入充值金额', trigger: 'blur' },
{
validator: (rule, value) => {
if (value === null || value === undefined) {
return new Error('请输入充值金额');
}
const num = Number(value);
if (Number.isNaN(num)) {
return new Error('请输入有效的数字');
}
if (num <= 0) {
return new Error('充值金额必须大于0');
}
return true;
},
trigger: 'blur'
}
],
walletTypeId: [{ required: true, message: '请选择钱包类型', trigger: 'change' }]
};
async function handleSubmit() {
await formRef.value?.validate();
loading.value = true;
try {
const result = await safeClient(() =>
client.api.admin.wallet.adjust.batch.post({
userIds: props.userIds,
amount: String(formModel.value.amount),
walletTypeId: formModel.value.walletTypeId!
})
);
if (result.data) {
window.$message?.success('充值成功');
emit('close');
}
} finally {
loading.value = false;
}
}
</script>
<template>
<div class="p-6">
<NForm ref="formRef" :model="formModel" :rules="rules" label-placement="left" :label-width="100">
<NFormItem label="钱包类型" path="walletTypeId">
<NSelect
v-model:value="formModel.walletTypeId"
:options="walletOptions"
placeholder="请选择钱包类型"
clearable
/>
</NFormItem>
<NFormItem label="充值金额" path="amount">
<NInputNumber
v-model:value="formModel.amount"
placeholder="请输入充值金额"
:min="0"
:precision="2"
class="w-full"
>
<template #suffix></template>
</NInputNumber>
</NFormItem>
<NFormItem label="充值用户">
<div class="text-gray-500">
已选择
<span class="text-primary font-semibold">{{ userIds.length }}</span>
个用户
</div>
</NFormItem>
<NSpace justify="end" class="mt-4">
<NButton @click="emit('close')">取消</NButton>
<NButton type="primary" :loading="loading" @click="handleSubmit">确认充值</NButton>
</NSpace>
</NForm>
</div>
</template>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,17 @@
<script lang="ts" setup>
import DepositPage from '@/views/deposit/index.vue';
defineOptions({
name: 'DepositDialog'
});
const props = defineProps<{
userId?: string;
}>();
</script>
<template>
<DepositPage :user-id="userId" :scroll-x="800" :show-header-operation="false" />
</template>
<style lang="css" scoped></style>

View File

@@ -0,0 +1,17 @@
<script lang="ts" setup>
import SubscriptionPage from '@/views/subscription/index.vue';
defineOptions({
name: 'SubscriptionDialog'
});
const props = defineProps<{
userId?: string;
}>();
</script>
<template>
<SubscriptionPage :user-id="userId" :scroll-x="1200" :show-header-operation="false" />
</template>
<style lang="css" scoped></style>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { h, ref, useTemplateRef } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import dayjs from 'dayjs';
import { authClient, client, safeClient } from '@/service/api';
@@ -12,6 +12,10 @@ import Withdraw from './components/withdraw.vue';
import Team from './components/team.vue';
import Kyc from './components/kyc.vue';
import Ledger from './components/ledger.vue';
import Deposit from './components/deposit.vue';
import Subscription from './components/subscription.vue';
import AdjustWallet from './components/adjust-wallet.vue';
import AdjustWalletWithFile from './components/adjust-wallet-with-file.vue';
const dialog = useDialog();
const message = useMessage();
@@ -29,6 +33,12 @@ const fetchData: TableFetchData = ({ pagination, filter }) => {
};
const columns: TableBaseColumns = [
{
key: 'selection',
type: 'selection',
title: '',
width: 60
},
{
key: 'userProfile.uid',
title: 'UID',
@@ -95,6 +105,30 @@ const columns: TableBaseColumns = [
});
}
},
{
contentText: '充值订单',
size: 'small',
onClick: () => {
dialog.create({
title: '充值订单',
showIcon: false,
style: { width: '1000px' },
content: () => h(Deposit, { userId: row.id })
});
}
},
{
contentText: '认购记录',
size: 'small',
onClick: () => {
dialog.create({
title: '认购记录',
showIcon: false,
style: { width: '1000px' },
content: () => h(Subscription, { userId: row.id })
});
}
},
{
contentText: '提现订单',
size: 'small',
@@ -113,7 +147,7 @@ const columns: TableBaseColumns = [
onClick: () => {
dialog.warning({
title: '重置交易密码',
content: '确认重置该用户的交易密码为初始密码123456吗?',
content: '确认重置该用户的交易密码吗?',
positiveText: '确认重置',
negativeText: '取消',
onPositiveClick: async () => {
@@ -129,6 +163,26 @@ const columns: TableBaseColumns = [
});
}
},
{
contentText: '重置登录密码',
size: 'small',
onClick: () => {
dialog.warning({
title: '重置登录密码',
content: '确认重置该用户的登录密码吗?',
positiveText: '确认重置',
negativeText: '取消',
onPositiveClick: async () => {
const { data } = await safeClient(() => client.api.admin.users({ userId: row.id }).password.reset.post());
dialog.success({
title: '操作成功',
content: `该用户的登录密码已重置为初始密码 ${data.value?.password},请妥善告知用户。`
});
}
});
}
},
{
contentText: '查看团队',
size: 'small',
@@ -220,11 +274,48 @@ const filterColumns: TableFilterColumns = [
title: '姓名'
}
];
const checkedRowKeys = ref<string[]>([]);
function handleAdjustWallet() {
if (checkedRowKeys.value.length === 0) {
message.warning('请先选择用户');
return;
}
const d = dialog.create({
title: '一键上分',
showIcon: false,
style: { width: '600px' },
content: () =>
h(AdjustWallet, {
userIds: checkedRowKeys.value,
onClose: () => {
d.destroy();
tableInst.value?.reload();
}
})
});
}
function handleAdjustWalletWithFile() {
const d = dialog.create({
title: '文件导入上分',
showIcon: false,
style: { width: '600px' },
content: () =>
h(AdjustWalletWithFile, {
onClose: () => {
d.destroy();
}
})
});
}
</script>
<template>
<TableBase
ref="tableInst"
v-model:checked-row-keys="checkedRowKeys"
:row-key="row => row.id"
:fetch-data="fetchData"
:columns="columns"
:filter-columns="filterColumns"
@@ -234,7 +325,12 @@ const filterColumns: TableFilterColumns = [
refresh: true,
columns: true
}"
/>
>
<template #header-operation-suffix>
<NButton size="small" type="primary" ghost @click="handleAdjustWallet">一键上分</NButton>
<NButton size="small" type="primary" ghost @click="handleAdjustWalletWithFile">文件导入上分</NButton>
</template>
</TableBase>
</template>
<style lang="css" scoped></style>

View File

@@ -0,0 +1,84 @@
<script lang="ts" setup>
import { useTemplateRef } from 'vue';
import { NSelect, useDialog } from 'naive-ui';
import dayjs from 'dayjs';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
const tableInst = useTemplateRef<TableInst>('tableInst');
const props = defineProps<{
jobId: string;
}>();
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() =>
client.api.admin.wallet_import.jobs({ id: props.jobId }).items.get({ query: { ...pagination, ...filter } })
);
};
const columns: TableBaseColumns = [
{
key: 'id',
title: 'ID'
},
{
key: 'walletTypeCodeRaw',
title: '钱包类型'
},
{
key: 'phoneNumber',
title: '手机号'
},
{
key: 'memo',
title: '备注'
},
{
key: 'createdAt',
title: '创建时间',
render: (row: any) => {
return dayjs(row.createdAt).format('YYYY-MM-DD HH:mm');
}
},
{
key: 'status',
title: '状态'
},
{
key: 'amount',
title: '金额'
},
{
key: 'error',
title: '错误信息'
}
];
const filterColumns: TableFilterColumns = [
{
key: 'status',
title: '状态',
component: NSelect,
componentProps: {
options: [
{ label: '成功', value: 'success' },
{ label: '失败', value: 'failed' },
{ label: '忽略', value: 'ignored' }
]
}
}
];
</script>
<template>
<TableBase
ref="tableInst"
:fetch-data="fetchData"
:columns="columns"
:filter-columns="filterColumns"
:scroll-x="1200"
:show-header-operation="false"
/>
</template>
<style lang="css" scoped></style>

View File

@@ -0,0 +1,131 @@
<script lang="ts" setup>
import { h, useTemplateRef } from 'vue';
import { NSelect, useDialog } from 'naive-ui';
import dayjs from 'dayjs';
import { client, safeClient } from '@/service/api';
import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
import ImportJobDetails from './components/import-job-details.vue';
defineOptions({
name: 'WalletImportJobs'
});
const dialog = useDialog();
const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() => client.api.admin.wallet_import.jobs.get({ query: { ...pagination, ...filter } }));
};
const columns: TableBaseColumns = [
{
key: 'id',
title: 'ID'
},
{
key: 'createdBy',
title: '创建者'
},
{
key: 'status',
title: '状态'
},
{
key: 'total',
title: '任务总数'
},
{
key: 'processed',
title: '已处理数'
},
{
key: 'success',
title: '成功数'
},
{
key: 'failed',
title: '失败数'
},
{
key: 'ignored',
title: '忽略数'
},
{
key: 'error',
title: '错误信息'
},
{
key: 'startedAt',
title: '开始时间',
render: (row: any) => {
return dayjs(row.startedAt).format('YYYY-MM-DD HH:mm');
}
},
{
key: 'finishedAt',
title: '结束时间',
render: (row: any) => {
return dayjs(row.finishedAt).format('YYYY-MM-DD HH:mm');
}
},
{
key: 'createdAt',
title: '创建时间',
render: (row: any) => {
return dayjs(row.createdAt).format('YYYY-MM-DD HH:mm');
}
},
{
key: 'operations',
title: '操作',
width: 150,
fixed: 'right',
operations: (row: any) => [
{
contentText: '查看明细',
size: 'small',
onClick: () => {
dialog.create({
title: '导入明细',
showIcon: false,
style: { width: '1200px' },
content: () => h(ImportJobDetails, { jobId: row.id })
});
}
}
]
}
];
const filterColumns: TableFilterColumns = [
{
key: 'status',
title: '状态',
component: NSelect,
componentProps: {
options: [
{ label: '待处理', value: 'pending' },
{ label: '处理中', value: 'processing' },
{ label: '已完成', value: 'completed' },
{ label: '失败', value: 'failed' }
]
}
}
];
</script>
<template>
<TableBase
ref="tableInst"
:fetch-data="fetchData"
:columns="columns"
:filter-columns="filterColumns"
:scroll-x="2000"
:header-operations="{
add: false,
refresh: true,
columns: true
}"
/>
</template>
<style lang="css" scoped></style>