更新表格组件,新增过滤功能,调整数据获取逻辑,优化界面布局

This commit is contained in:
2025-12-17 23:24:18 +07:00
parent 8a9d617129
commit 63ca414f2b
7 changed files with 225 additions and 85 deletions

View File

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

View File

@@ -39,34 +39,32 @@ function refresh() {
</script> </script>
<template> <template>
<NSpace :align="itemAlign" wrap justify="end" class="lt-sm:w-200px"> <NSpace :align="itemAlign" justify="space-between" wrap class="lt-sm:w-200px">
<slot name="prefix"></slot> <slot name="prefix"></slot>
<slot name="default"> <div class="space-x-5">
<NButton size="small" ghost type="primary" @click="add"> <slot name="default">
<NButton size="small" ghost type="primary" @click="add">
<template #icon>
<icon-ic-round-plus class="text-icon" />
</template>
{{ $t('common.add') }}
</NButton>
<NButton size="small" ghost type="error" :disabled="disabledDelete" @click="batchDelete">
<template #icon>
<icon-ic-round-delete class="text-icon" />
</template>
{{ $t('common.batchDelete') }}
</NButton>
</slot>
<NButton size="small" @click="refresh">
<template #icon> <template #icon>
<icon-ic-round-plus class="text-icon" /> <icon-mdi-refresh class="text-icon" :class="{ 'animate-spin': loading }" />
</template> </template>
{{ $t('common.add') }} {{ $t('common.refresh') }}
</NButton> </NButton>
<NPopconfirm @positive-click="batchDelete"> <TableColumnSetting v-model:columns="columns" />
<template #trigger> </div>
<NButton size="small" ghost type="error" :disabled="disabledDelete">
<template #icon>
<icon-ic-round-delete class="text-icon" />
</template>
{{ $t('common.batchDelete') }}
</NButton>
</template>
{{ $t('common.confirmDelete') }}
</NPopconfirm>
</slot>
<NButton size="small" @click="refresh">
<template #icon>
<icon-mdi-refresh class="text-icon" :class="{ 'animate-spin': loading }" />
</template>
{{ $t('common.refresh') }}
</NButton>
<TableColumnSetting v-model:columns="columns" />
<slot name="suffix"></slot> <slot name="suffix"></slot>
</NSpace> </NSpace>
</template> </template>

View File

@@ -1,3 +1,4 @@
import type { Component, VNode } from 'vue';
import { h } from 'vue'; import { h } from 'vue';
import type { ButtonProps, DataTableColumn, DataTableColumns } from 'naive-ui'; import type { ButtonProps, DataTableColumn, DataTableColumns } from 'naive-ui';
import { NButton, NSpace } from 'naive-ui'; import { NButton, NSpace } from 'naive-ui';
@@ -24,12 +25,27 @@ export interface Pagination {
[key: string]: any; [key: string]: any;
} }
export type TableFetchData = (page: Pagination) => ReturnType<typeof safeClient>; export type TableFetchData = (query: {
pagination: Pagination;
filter?: Record<string, any>;
}) => ReturnType<typeof safeClient>;
export type TableFilterColumn = {
key: string;
title: string;
component?: Component | VNode;
componentProps?: Record<string, any>;
};
export type TableFilterColumns = Array<TableFilterColumn>;
export function transformColumns<T = InternalRowData>(columns: TableBaseColumns<T>): DataTableColumns<T> { export function transformColumns<T = InternalRowData>(columns: TableBaseColumns<T>): DataTableColumns<T> {
return columns.map(col => { return columns.map(col => {
return { return {
...col, ...col,
ellipsis: {
tooltip: true
},
render: col.operations render: col.operations
? (row: T) => ? (row: T) =>
h(NSpace, null, { h(NSpace, null, {

View File

@@ -1,12 +1,25 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import type { PaginationProps } from 'naive-ui'; import { useRoute } from 'vue-router';
import { type TableBaseColumns, type TableFetchData, transformColumns, transformHeaderColumns } from '.'; import type { DataTableColumns, PaginationProps } from 'naive-ui';
import { useI18n } from 'vue-i18n';
import {
type TableBaseColumns,
type TableFetchData,
type TableFilterColumns,
transformColumns,
transformHeaderColumns
} from '.';
import type { TableColumnCheck } from '~/packages/hooks/src';
const route = useRoute();
const { t } = useI18n();
const title = t(route.meta.i18nKey as string);
const props = defineProps<{ const props = defineProps<{
fetchData: TableFetchData; fetchData: TableFetchData;
columns: TableBaseColumns; columns: TableBaseColumns;
showHeaderOperation?: boolean; showHeaderOperation?: boolean;
filterColumns?: TableFilterColumns;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'add'): void; (e: 'add'): void;
@@ -15,8 +28,8 @@ const emit = defineEmits<{
}>(); }>();
const tableData = ref<any[]>([]); const tableData = ref<any[]>([]);
const dataTableColumns = transformColumns(props.columns); const dataTableColumns = ref<DataTableColumns>(transformColumns(props.columns));
const headerTableColumns = transformHeaderColumns(props.columns); const headerTableColumns = ref<NaiveUI.TableColumnCheck[]>(transformHeaderColumns(props.columns));
const pagination = ref<PaginationProps>({ const pagination = ref<PaginationProps>({
page: 1, page: 1,
pageSize: 10, pageSize: 10,
@@ -25,12 +38,15 @@ const pagination = ref<PaginationProps>({
pageSizes: [10, 20, 50, 100] pageSizes: [10, 20, 50, 100]
}); });
async function loadData() { async function loadData(query?: Record<string, any>) {
const page = pagination.value.page || 1; const page = pagination.value.page || 1;
const pageSize = pagination.value.pageSize || 10; const pageSize = pagination.value.pageSize || 10;
const { data } = await props.fetchData({ const { data } = await props.fetchData({
pageIndex: page, pagination: {
pageSize pageIndex: page,
pageSize
},
filter: query
}); });
tableData.value = (data.value as any).data; tableData.value = (data.value as any).data;
@@ -45,6 +61,19 @@ function handlePageSizeChange(curPageSize: number) {
pagination.value.pageSize = curPageSize; pagination.value.pageSize = curPageSize;
loadData(); loadData();
} }
function handleSearch(form: Record<string, any>) {
pagination.value.page = 1;
loadData(form);
}
function handleUpdateColumns(columns: TableColumnCheck[]) {
headerTableColumns.value = columns;
const sortKeys = columns.map(col => col.key);
const currentColumns: DataTableColumns = [...dataTableColumns.value];
const sortedColumns = sortKeys.map(key => currentColumns.find(col => (col as any).key === key)).filter(Boolean);
dataTableColumns.value = sortedColumns as DataTableColumns;
}
onMounted(() => { onMounted(() => {
loadData(); loadData();
@@ -57,21 +86,32 @@ defineExpose({
<template> <template>
<div class="space-y-5"> <div class="space-y-5">
<TableHeaderOperation <TableFilter :columns="filterColumns" @confirm="handleSearch" />
v-if="showHeaderOperation"
:columns="headerTableColumns" <div class="rounded-lg bg-white p-5 space-y-5">
@add="emit('add')" <TableHeaderOperation
@refresh="emit('refresh')" v-if="showHeaderOperation"
@delete="emit('delete')" :columns="headerTableColumns"
/> @update:columns="handleUpdateColumns"
<NDataTable @add="emit('add')"
:columns="dataTableColumns" @refresh="loadData()"
:data="tableData" @delete="emit('delete')"
:pagination="pagination" >
:bordered="false" <template #prefix>
:on-update:page="handlePageChange" <div class="text-lg font-bold">{{ title }}</div>
:on-update:page-size="handlePageSizeChange" </template>
/> </TableHeaderOperation>
<NDataTable
:scroll-x="2000"
:columns="dataTableColumns"
:data="tableData"
:pagination="pagination"
:bordered="false"
:on-update:page="handlePageChange"
:on-update:page-size="handlePageSizeChange"
/>
</div>
</div> </div>
</template> </template>

View File

@@ -0,0 +1,47 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { NInput } from 'naive-ui';
import type { TableFilterColumns } from '.';
defineProps<{
columns?: TableFilterColumns;
}>();
const emit = defineEmits<{
(e: 'confirm', form: Record<string, any>): void;
}>();
const form = ref<Record<string, any>>({});
function handleReset() {
form.value = {};
emit('confirm', form.value);
}
function handleConfirm() {
emit('confirm', form.value);
}
</script>
<template>
<div class="rounded-lg bg-white p-5">
<div class="mb-5">搜索</div>
<NForm :label-width="80" label-align="left" label-placement="left">
<NGrid x-gap="20" :cols="4">
<NGi v-for="col in columns" :key="col.key">
<NFormItem :label="col.title" :path="col.key">
<component :is="col.component || NInput" v-model:value="form[col.key]" v-bind="col.componentProps" />
</NFormItem>
</NGi>
<NGi>
<NSpace>
<NButton type="primary" ghost @click="handleReset">重置</NButton>
<NButton type="primary" @click="handleConfirm">搜索</NButton>
</NSpace>
</NGi>
</NGrid>
</NForm>
</div>
</template>
<style lang="css" scoped></style>

View File

@@ -91,6 +91,7 @@ declare module 'vue' {
SystemLogo: typeof import('./../components/common/system-logo.vue')['default'] SystemLogo: typeof import('./../components/common/system-logo.vue')['default']
TableBase: typeof import('./../components/table/table-base.vue')['default'] TableBase: typeof import('./../components/table/table-base.vue')['default']
TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default'] TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default']
TableFilter: typeof import('./../components/table/table-filter.vue')['default']
TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default'] TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default'] ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default'] WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
@@ -178,6 +179,7 @@ declare global {
const SystemLogo: typeof import('./../components/common/system-logo.vue')['default'] const SystemLogo: typeof import('./../components/common/system-logo.vue')['default']
const TableBase: typeof import('./../components/table/table-base.vue')['default'] const TableBase: typeof import('./../components/table/table-base.vue')['default']
const TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default'] const TableColumnSetting: typeof import('./../components/advanced/table-column-setting.vue')['default']
const TableFilter: typeof import('./../components/table/table-filter.vue')['default']
const TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default'] const TableHeaderOperation: typeof import('./../components/advanced/table-header-operation.vue')['default']
const ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default'] const ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
const WaveBg: typeof import('./../components/custom/wave-bg.vue')['default'] const WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']

View File

@@ -1,18 +1,20 @@
<script lang="ts" setup> <script lang="ts" setup>
import { h, ref, useTemplateRef } from 'vue'; import { useTemplateRef } from 'vue';
import { NInputNumber, useDialog, useMessage } from 'naive-ui'; import { NDatePicker, useDialog, useMessage } from 'naive-ui';
import { client, safeClient } from '@/service/api'; import { client, safeClient } from '@/service/api';
import { DepositTypeEnum } from '@/enum'; import type { TableBaseColumns, TableFetchData, TableFilterColumns, TableInst } from '@/components/table';
import type { TableBaseColumns, TableFetchData, TableInst } from '@/components/table';
const dialog = useDialog(); const dialog = useDialog();
const message = useMessage(); const message = useMessage();
const tableInst = useTemplateRef<TableInst>('tableInst'); const tableInst = useTemplateRef<TableInst>('tableInst');
const fetchData: TableFetchData = () => { const fetchData: TableFetchData = ({ pagination, filter }) => {
return safeClient(() => return safeClient(() =>
client.api.admin.rwa.issuance.products.get({ client.api.admin.rwa.issuance.products.get({
query: {} query: {
...pagination,
...filter
}
}) })
); );
}; };
@@ -23,7 +25,7 @@ const columns: TableBaseColumns = [
key: 'id' key: 'id'
}, },
{ {
title: 'code', title: 'Code',
key: 'code' key: 'code'
}, },
{ {
@@ -31,64 +33,99 @@ const columns: TableBaseColumns = [
key: 'name' key: 'name'
}, },
{ {
title: 'description', title: '估值',
key: 'description'
},
{
title: 'estimatedValue',
key: 'estimatedValue' key: 'estimatedValue'
}, },
{ {
title: 'categoryId', title: '产品分类',
key: 'categoryId' key: 'categoryId'
}, },
{ {
title: 'createdBy', title: '创建人',
key: 'createdBy' key: 'createdBy'
}, },
{
title: '创建时间',
key: 'createdAt',
render: (row: any) => {
return new Date(row.createdAt).toLocaleDateString();
}
},
{
title: '状态',
key: 'status'
},
{
title: '描述',
key: 'description'
},
{ {
title: '操作', title: '操作',
fixed: 'right', fixed: 'right',
key: 'operation', key: 'operation',
width: 160, width: 230,
operations: (row: any) => [ operations: (row: any) => [
{ {
contentText: '编辑', contentText: '批准',
type: 'primary', size: 'small',
onClick: () => { onClick: async () => {
safeClient(() =>
client.api.admin.rwa.issuance.approve.post({
productId: row.id as string
})
);
tableInst.value?.reload(); tableInst.value?.reload();
} }
}, },
{ {
contentText: '删除', contentText: '拒绝',
type: 'error',
ghost: true,
size: 'small', size: 'small',
onClick: async () => { onClick: async () => {
dialog.create({ safeClient(() =>
title: '提示', client.api.admin.rwa.issuance.reject.post({
positiveText: '是', productId: row.id as string,
negativeText: '否', rejectionReason: '不符合要求'
content: '确认删除该银行信息?', })
onPositiveClick: async () => { );
safeClient(() => tableInst.value?.reload();
client.api.admin.deposit.reject({ orderId: row.id as string }).post({ }
reviewNote: '管理员拒绝充值' },
}) {
); contentText: '编辑',
// tableInst.value?.reload(); size: 'small',
message.success('删除成功'); onClick: () => {
} tableInst.value?.reload();
});
} }
} }
] ]
} }
]; ];
const filterColumns: TableFilterColumns = [
{
title: '产品名称',
key: 'name'
},
{
title: '产品Code',
key: 'Code'
},
{
title: '创建时间',
key: 'createdAt',
component: NDatePicker
}
];
</script> </script>
<template> <template>
<TableBase ref="tableInst" :columns="columns" :fetch-data="fetchData" /> <TableBase
ref="tableInst"
show-header-operation
:columns="columns"
:filter-columns="filterColumns"
:fetch-data="fetchData"
/>
</template> </template>
<style lang="css" scoped></style> <style lang="css" scoped></style>