fix:kyc界面图片处理和搜索条件修改
This commit is contained in:
6
src/typings/api.d.ts
vendored
6
src/typings/api.d.ts
vendored
@@ -674,7 +674,7 @@ declare namespace Api {
|
||||
id: string;
|
||||
userId: number;
|
||||
userName: string;
|
||||
fullName: string;
|
||||
realName: string;
|
||||
idType: string;
|
||||
idFile: string;
|
||||
identifyPicture: string;
|
||||
@@ -688,7 +688,9 @@ declare namespace Api {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
userName?: string;
|
||||
status?: number;
|
||||
status?: string;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
}
|
||||
|
||||
interface KycResponse {
|
||||
|
||||
334
src/views/user-center/kyc/index.vue
Normal file
334
src/views/user-center/kyc/index.vue
Normal file
@@ -0,0 +1,334 @@
|
||||
<template>
|
||||
<SimpleScrollbar>
|
||||
<div class="min-h-500px flex-col-stretch gap-16px overflow-hidden lt-sm:overflow-auto">
|
||||
<KycSearch
|
||||
v-model:model="searchParams"
|
||||
:loading="loading"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<ACard
|
||||
title="KYC实名认证审核"
|
||||
:bordered="false"
|
||||
:body-style="{ flex: 1, overflow: 'hidden' }"
|
||||
class="flex-col-stretch sm:flex-1-hidden card-wrapper"
|
||||
>
|
||||
<ATable
|
||||
ref="wrapperEl"
|
||||
:columns="columns"
|
||||
:data-source="data"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
size="small"
|
||||
:pagination="{
|
||||
...mobilePagination,
|
||||
total: mobilePagination.total,
|
||||
current: searchParams.pageNum,
|
||||
pageSize: searchParams.pageSize,
|
||||
showTotal: (total: number) => `共 ${total} 条`
|
||||
}"
|
||||
:scroll="scrollConfig"
|
||||
class="h-full"
|
||||
@change="(pagination) => {
|
||||
searchParams.pageNum = pagination.current;
|
||||
searchParams.pageSize = pagination.pageSize;
|
||||
getData();
|
||||
}"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<ATag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</ATag>
|
||||
</template>
|
||||
<template v-if="column.key === 'idFile'">
|
||||
<a-image
|
||||
:width="200"
|
||||
:src="parseFile(record.idFile)"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.key === 'identifyPicture'">
|
||||
<a-image
|
||||
:width="200"
|
||||
:src="parseFile(record.identifyPicture)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'operate'">
|
||||
<ASpace>
|
||||
<AButton
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="record.status !== 'PENDING'"
|
||||
@click="handleApprove(record)"
|
||||
>
|
||||
通过
|
||||
</AButton>
|
||||
<AButton
|
||||
danger
|
||||
size="small"
|
||||
:disabled="record.status !== 'PENDING'"
|
||||
@click="handleReject(record)"
|
||||
>
|
||||
拒绝
|
||||
</AButton>
|
||||
</ASpace>
|
||||
</template>
|
||||
</template>
|
||||
</ATable>
|
||||
</ACard>
|
||||
</div>
|
||||
</SimpleScrollbar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/common/table';
|
||||
import { SimpleScrollbar } from '~/packages/materials/src';
|
||||
import { computed, shallowRef, h } from 'vue';
|
||||
import { useElementSize } from '@vueuse/core';
|
||||
import {fetchKycList, approveKyc, rejectKyc} from '@/service/api/auth';
|
||||
import {
|
||||
Card as ACard,
|
||||
Table as ATable,
|
||||
Tag as ATag,
|
||||
Space as ASpace,
|
||||
Button as AButton,
|
||||
Image as AImage,
|
||||
Modal as AModal,
|
||||
Input as AInput,
|
||||
message
|
||||
} from 'ant-design-vue';
|
||||
import KycSearch from './modules/kyc-search.vue';
|
||||
|
||||
const wrapperEl = shallowRef<HTMLElement | null>(null);
|
||||
const { height: wrapperElHeight } = useElementSize(wrapperEl);
|
||||
|
||||
const scrollConfig = computed(() => ({
|
||||
y: wrapperElHeight.value - 72,
|
||||
x: 800
|
||||
}));
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'PENDING':
|
||||
return 'warning';
|
||||
case 'APPROVED':
|
||||
return 'success';
|
||||
case 'REJECTED':
|
||||
return 'error';
|
||||
default:
|
||||
return 'default';
|
||||
}
|
||||
};
|
||||
//李工杰作 图片请求url组装
|
||||
const parseFile = (path:string)=>{
|
||||
let baseUrl = import.meta.env.VITE_SERVICE_BASE_URL;
|
||||
if (baseUrl.indexOf('/') !== -1){
|
||||
try {
|
||||
const parsedUrl = new URL(baseUrl);
|
||||
baseUrl = parsedUrl.host;
|
||||
} catch (error) {
|
||||
console.error('Invalid URL:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return `//${baseUrl}/file${path}`
|
||||
}
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'VERIFIED':
|
||||
return '未认证';
|
||||
case 'PENDING':
|
||||
return '待审核';
|
||||
case 'APPROVED':
|
||||
return '已通过';
|
||||
case 'REJECTED':
|
||||
return '已拒绝';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
const {
|
||||
columns,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
mobilePagination,
|
||||
searchParams,
|
||||
resetSearchParams
|
||||
} = useTable({
|
||||
apiFn: fetchKycList,
|
||||
immediate: true,
|
||||
apiParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
userName: '',
|
||||
status: undefined
|
||||
} as Api.Kyc.KycParams,
|
||||
rowKey: 'id',
|
||||
pagination: true,
|
||||
columns: (): AntDesign.TableColumn<Api.Kyc.KycInfo>[] => [
|
||||
{
|
||||
key: 'userName',
|
||||
dataIndex: 'userName',
|
||||
title: '用户名',
|
||||
align: 'center',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
key: 'realName',
|
||||
dataIndex: 'realName',
|
||||
title: '姓名',
|
||||
align: 'center',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
key: 'idType',
|
||||
dataIndex: 'idType',
|
||||
title: '证件类型',
|
||||
align: 'center',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
key: 'idFile',
|
||||
dataIndex: 'idFile',
|
||||
title: '证件照片',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
// customRender: ({ text }) => h('div', { class: 'image-wrapper' }, [
|
||||
// h(AImage, {
|
||||
// src: text || '',
|
||||
// alt: '证件照片',
|
||||
// height: 80,
|
||||
// width: 120,
|
||||
// style: { objectFit: 'cover' },
|
||||
// fallback: '/src/assets/images/image-error.png',
|
||||
// preview: true
|
||||
// })
|
||||
// ])
|
||||
},
|
||||
{
|
||||
key: 'identifyPicture',
|
||||
dataIndex: 'identifyPicture',
|
||||
title: '面部照片',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
// customRender: ({ text }) => h('div', { class: 'image-wrapper' }, [
|
||||
// h(AImage, {
|
||||
// src: text || '',
|
||||
// alt: '面部照片',
|
||||
// height: 80,
|
||||
// width: 120,
|
||||
// style: { objectFit: 'cover' },
|
||||
// fallback: '/src/assets/images/image-error.png',
|
||||
// preview: true
|
||||
// })
|
||||
// ])
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
dataIndex: 'status',
|
||||
title: '状态',
|
||||
align: 'center',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
key: 'createTime',
|
||||
dataIndex: 'createTime',
|
||||
title: '提交时间',
|
||||
align: 'center',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
key: 'operate',
|
||||
title: '操作',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
|
||||
|
||||
const handleSearch = () => {
|
||||
getData();
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
resetSearchParams();
|
||||
getData();
|
||||
};
|
||||
|
||||
const handleApprove = async (record: Api.Kyc.KycInfo) => {
|
||||
try {
|
||||
AModal.confirm({
|
||||
title: '确认通过',
|
||||
content: '确定要通过该用户的实名认证吗?',
|
||||
async onOk() {
|
||||
await approveKyc(record.id, record.userId);
|
||||
message.success('审核通过成功');
|
||||
getData(); // 刷新列表
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
console.error('Approve failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReject = (record: Api.Kyc.KycInfo) => {
|
||||
let reason = '';
|
||||
AModal.confirm({
|
||||
title: '确认拒绝',
|
||||
content: h('div', [
|
||||
h('p', '确定要拒绝该用户的实名认证吗?'),
|
||||
h(AInput.TextArea, {
|
||||
placeholder: '请输入拒绝原因',
|
||||
rows: 4,
|
||||
'onUpdate:value': (val: string) => {
|
||||
reason = val;
|
||||
}
|
||||
})
|
||||
]),
|
||||
async onOk() {
|
||||
if (!reason.trim()) {
|
||||
message.error('请输入拒绝原因');
|
||||
return Promise.reject();
|
||||
}
|
||||
try {
|
||||
await rejectKyc(record.id, record.userId, reason);
|
||||
message.success('审核拒绝成功');
|
||||
getData(); // 刷新列表
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
console.error('Reject failed:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.h-full {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.card-wrapper {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
:deep(.ant-image) {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
137
src/views/user-center/kyc/modules/kyc-search.vue
Normal file
137
src/views/user-center/kyc/modules/kyc-search.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<ACard :bordered="false" class="search-card">
|
||||
<AForm
|
||||
ref="formRef"
|
||||
:model="formModel"
|
||||
layout="inline"
|
||||
class="flex flex-wrap gap-16px items-center"
|
||||
>
|
||||
<AFormItem label="提交时间">
|
||||
<ARangePicker
|
||||
v-model:value="queryRangePicker"
|
||||
show-time
|
||||
:placeholder="['开始时间', '结束时间']"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
class="w-400px"
|
||||
@change="handleTimeChange"
|
||||
/>
|
||||
</AFormItem>
|
||||
<AFormItem label="状态">
|
||||
<ASelect
|
||||
v-model:value="formModel.status"
|
||||
placeholder="请选择状态"
|
||||
allow-clear
|
||||
class="w-200px"
|
||||
>
|
||||
<ASelectOption value="PENDING">待审核</ASelectOption>
|
||||
<ASelectOption value="APPROVED">已通过</ASelectOption>
|
||||
<ASelectOption value="REJECTED">已拒绝</ASelectOption>
|
||||
</ASelect>
|
||||
</AFormItem>
|
||||
<AFormItem class="flex-1 justify-end">
|
||||
<ASpace>
|
||||
<AButton @click="handleReset">重置</AButton>
|
||||
<AButton type="primary" @click="handleSearch">查询</AButton>
|
||||
</ASpace>
|
||||
</AFormItem>
|
||||
</AForm>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import {
|
||||
Form as AForm,
|
||||
FormItem as AFormItem,
|
||||
Select as ASelect,
|
||||
SelectOption as ASelectOption,
|
||||
Button as AButton,
|
||||
Space as ASpace,
|
||||
Card as ACard,
|
||||
DatePicker
|
||||
} from 'ant-design-vue';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
const ARangePicker = DatePicker.RangePicker;
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
|
||||
interface SearchModel {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
model: SearchModel;
|
||||
}>(), {
|
||||
model: () => ({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
status: undefined
|
||||
})
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:model': [value: SearchModel];
|
||||
'search': [];
|
||||
'reset': [];
|
||||
}>();
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
const { resetFields } = AForm.useForm(props.model);
|
||||
|
||||
const formModel = computed({
|
||||
get: () => props.model,
|
||||
set: (val: SearchModel) => emit('update:model', val)
|
||||
});
|
||||
|
||||
const queryRangePicker = ref<[string, string]>(['', '']);
|
||||
|
||||
const handleTimeChange = (dates: any, dateStrings: [string, string]) => {
|
||||
formModel.value.startTime = dateStrings[0] || undefined;
|
||||
formModel.value.endTime = dateStrings[1] || undefined;
|
||||
};
|
||||
|
||||
function handleSearch() {
|
||||
emit('search');
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
queryRangePicker.value = ['', ''];
|
||||
resetFields();
|
||||
formModel.value = {
|
||||
...formModel.value,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
status: undefined,
|
||||
pageNum: 1
|
||||
};
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.model.startTime, props.model.endTime],
|
||||
([start, end]) => {
|
||||
queryRangePicker.value = [start || '', end || ''];
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.w-200px {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.w-400px {
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user