2
0
Files
fe.wfc.user/src/views/userInfo/kyc/index.vue
2025-01-20 16:47:33 +08:00

577 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, onMounted } from 'vue';
// import { useI18n } from 'vue-i18n';
import { message, Upload } from 'ant-design-vue';
import type { UploadProps } from 'ant-design-vue';
import { LoadingOutlined, PlusOutlined } from '@ant-design/icons-vue';
import type { Dayjs } from 'dayjs';
import { fetchKYCStatus, submitKYCVerification, uploadFile } from '@/service/api/auth';
import dayjs from 'dayjs'; // 导入 dayjs
// const { t } = useI18n();
// KYC 状态
const kycStatus = ref<Api.KYC.KYCStatus>('UNVERIFIED');
const kycInfo = ref<Partial<Api.KYC.KYCInfo>>({
status: 'UNVERIFIED',
fullName: '',
birthDate: '',
rejectReason: '',
verifiedTime: ''
});
// 证件类型选项
const idTypeOptions = [
{ value: 1, label: '驾驶证' },
{ value: 2, label: '护照' },
{ value: 3, label: '身份证/居住证' },
{ value: 4, label: '学生证' },
{ value: 5, label: '医保卡' },
{ value: 6, label: '出生证明' }
];
// 表单数据
interface FormData {
fullName: string;
birthDate: string;
idType: number; // 使用 number 类型
idCardFile?: string;
photoFile?: string;
}
// 表单数据
const formData = ref<FormData>({
fullName: '',
birthDate: '',
idType: 2, // 默认选择护照
idCardFile: undefined,
photoFile: undefined
});
// 文件对象存储
const idCardFileObj = ref<File>();
const photoFileObj = ref<File>();
// 上传状态
const idCardLoading = ref(false);
const photoLoading = ref(false);
// 文件列表状态
const idCardFileList = ref<any[]>([]);
const photoFileList = ref<any[]>([]);
// 添加图片预览状态
const previewVisible = ref(false);
const previewImage = ref('');
const previewTitle = ref('');
// 处理图片预览
const handlePreview = async (file: any) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
previewImage.value = file.url || file.preview;
previewVisible.value = true;
previewTitle.value = file.name || file.url.substring(file.url.lastIndexOf('/') + 1);
};
// 处理图片删除
const handleIdCardRemove = () => {
formData.value.idCardFile = undefined;
idCardFileObj.value = undefined;
idCardFileList.value = [];
};
const handlePhotoRemove = () => {
formData.value.photoFile = undefined;
photoFileObj.value = undefined;
photoFileList.value = [];
};
// 辅助函数:将文件转换为 base64
const getBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = error => reject(error);
});
};
// 获取 KYC 状态
const getKYCStatus = async () => {
try {
const { data } = await fetchKYCStatus();
if (data?.status) {
// 将状态转换为标准格式
const normalizedStatus = data.status.toUpperCase() as Api.KYC.KYCStatus;
kycStatus.value = normalizedStatus;
kycInfo.value = {
...data,
status: normalizedStatus
};
} else {
// 如果没有状态数据,设置为默认值
kycStatus.value = 'UNVERIFIED';
kycInfo.value = {
status: 'UNVERIFIED',
fullName: '',
birthDate: '',
rejectReason: '',
verifiedTime: ''
};
console.warn('No KYC status data received');
}
} catch (error) {
console.error('Failed to fetch KYC status:', error);
message.error('获取认证状态失败');
// 发生错误时也设置默认值
kycStatus.value = 'UNVERIFIED';
kycInfo.value = {
status: 'UNVERIFIED',
fullName: '',
birthDate: '',
rejectReason: '',
verifiedTime: ''
};
}
};
// 处理文件上传
const handleIdCardUpload: UploadProps['beforeUpload'] = async (file) => {
const isValidFormat = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'application/pdf';
if (!isValidFormat) {
message.error('只支持 JPG/PNG/PDF 格式文件!');
return Upload.LIST_IGNORE;
}
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
message.error('文件大小不能超过 5MB');
return Upload.LIST_IGNORE;
}
try {
idCardLoading.value = true;
const { data, error } = await uploadFile(file);
if (error) {
throw error;
}
if (data) {
formData.value.idCardFile = data.url; // 存储文件 URL
idCardFileObj.value = file; // 存储文件对象
message.success('证件照片上传成功!');
}
} catch (error) {
console.error('Failed to upload ID card:', error);
message.error('证件照片上传失败,请重试!');
} finally {
idCardLoading.value = false;
}
return false;
};
const handlePhotoUpload: UploadProps['beforeUpload'] = async (file) => {
const isValidFormat = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isValidFormat) {
message.error('只支持 JPG/PNG 格式图片!');
return Upload.LIST_IGNORE;
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('图片大小不能超过 2MB');
return Upload.LIST_IGNORE;
}
try {
photoLoading.value = true;
const { data, error } = await uploadFile(file);
if (error) {
throw error;
}
if (data) {
formData.value.photoFile = data.url; // 存储文件 URL
photoFileObj.value = file; // 存储文件对象
message.success('面部照片上传成功!');
}
} catch (error) {
console.error('Failed to upload photo:', error);
message.error('面部照片上传失败,请重试!');
} finally {
photoLoading.value = false;
}
return false;
};
// 提交 KYC 认证
const submitKYC = async () => {
try {
if (!formData.value.fullName || !formData.value.birthDate || !formData.value.idType) {
message.error('请填写完整信息!');
return;
}
if (!formData.value.idCardFile || !formData.value.photoFile) {
message.error('请上传所需文件!');
return;
}
const formattedBirthDate = dayjs(formData.value.birthDate).format('YYYY-MM-DD');
await submitKYCVerification({
realName: formData.value.fullName,
birthDate: formattedBirthDate,
idType: formData.value.idType,
idFile: formData.value.idCardFile,
identifyPicture: formData.value.photoFile,
kycRequestStatus: 1
});
message.success('KYC 认证申请提交成功!');
await getKYCStatus();
} catch (error) {
console.error('Failed to submit KYC:', error);
message.error('提交失败,请重试!');
}
};
// 修改日期禁用逻辑
const disabledDate = (current: Dayjs) => {
// 禁用今天之后的日期
return current && current.valueOf() > Date.now();
};
// 获取状态颜色
const getStatusColor = (status: Api.KYC.KYCStatus) => {
const normalizedStatus = status.toUpperCase();
switch (normalizedStatus) {
case 'UNVERIFIED':
return 'warning'; // 未认证:橙色警告
case 'PENDING':
return 'processing'; // 认证中:蓝色处理中
case 'VERIFIED':
return 'success'; // 已认证:绿色成功
case 'REJECTED':
return 'error'; // 已拒绝:红色错误
default:
return 'default';
}
};
// 获取状态文本
const getStatusText = (status: Api.KYC.KYCStatus) => {
const normalizedStatus = status.toUpperCase();
switch (normalizedStatus) {
case 'UNVERIFIED':
return '未认证';
case 'PENDING':
return '认证中';
case 'VERIFIED':
return '已认证';
case 'REJECTED':
return '已拒绝';
default:
return '未知状态';
}
};
onMounted(() => {
getKYCStatus();
});
</script>
<template>
<div class="kyc-container responsive-container">
<!-- 标题部分 -->
<div class="page-header">
<h1 class="page-title">KYC 实名认证</h1>
<p class="page-desc">根据相关法规要求使用服务前需要完成实名认证</p>
</div>
<!-- 状态卡片 -->
<div class="status-card">
<div class="status-header">
<span class="status-title">认证状态</span>
<a-tag
class="status-tag"
:color="getStatusColor(kycStatus)"
>
{{ getStatusText(kycStatus) }}
</a-tag>
</div>
<div v-if="kycStatus === 'VERIFIED'" class="verified-info">
<div class="info-item">
<span class="label">认证姓名</span>
<span class="value">{{ kycInfo.fullName }}</span>
</div>
<div class="info-item">
<span class="label">认证时间</span>
<span class="value">{{ kycInfo.verifiedTime }}</span>
</div>
</div>
<div v-if="kycInfo.rejectReason" class="reject-reason">
<a-alert
type="warning"
:message="kycInfo.rejectReason"
show-icon
banner
/>
</div>
</div>
<!-- 认证表单 -->
<div v-if="kycStatus === 'UNVERIFIED' || kycStatus === 'REJECTED'" class="form-card">
<div class="form-header">
<h2 class="form-title">认证信息</h2>
<p class="form-desc">请填写真实的个人信息所有信息仅用于身份认证</p>
</div>
<a-form layout="vertical" class="kyc-form">
<!-- 基本信息部分 -->
<div class="form-section">
<h3 class="section-title">基本信息</h3>
<div class="form-grid">
<a-form-item label="真实姓名" required>
<a-input
v-model:value="formData.fullName"
placeholder="请输入真实姓名"
:maxLength="20"
/>
</a-form-item>
<a-form-item label="出生日期" required>
<a-date-picker
v-model:value="formData.birthDate"
class="date-picker"
:disabledDate="disabledDate"
placeholder="请选择出生日期"
/>
</a-form-item>
</div>
</div>
<!-- 证件信息部分 -->
<div class="form-section">
<h3 class="section-title">证件信息</h3>
<div class="form-grid">
<a-form-item label="证件类型" required>
<a-select
v-model:value="formData.idType"
placeholder="请选择证件类型"
class="id-type-select"
:get-popup-container="(triggerNode) => triggerNode.parentNode"
>
<a-select-option
v-for="option in idTypeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-select-option>
</a-select>
</a-form-item>
</div>
<div class="upload-section">
<div class="upload-item">
<a-form-item label="证件照片" required>
<a-upload
v-model:file-list="idCardFileList"
list-type="picture-card"
:show-upload-list="true"
:before-upload="handleIdCardUpload"
@preview="handlePreview"
@remove="handleIdCardRemove"
>
<div v-if="idCardFileList.length < 1">
<loading-outlined v-if="idCardLoading" />
<plus-outlined v-else />
<div class="upload-text">上传证件照片</div>
</div>
</a-upload>
<div class="upload-tip">支持 JPG/PNG/PDF 格式,大小不超过 5MB</div>
</a-form-item>
</div>
<div class="upload-item">
<a-form-item label="面部照片" required>
<a-upload
v-model:file-list="photoFileList"
list-type="picture-card"
:show-upload-list="true"
:before-upload="handlePhotoUpload"
@preview="handlePreview"
@remove="handlePhotoRemove"
>
<div v-if="photoFileList.length < 1">
<loading-outlined v-if="photoLoading" />
<plus-outlined v-else />
<div class="upload-text">上传面部照片</div>
</div>
</a-upload>
<div class="upload-tip">支持 JPG/PNG 格式,大小不超过 2MB</div>
</a-form-item>
</div>
</div>
</div>
<div class="form-footer">
<a-button
type="primary"
size="large"
:disabled="kycStatus === 'PENDING'"
@click="submitKYC"
class="submit-btn"
>
提交认证
</a-button>
</div>
</a-form>
</div>
<!-- 添加图片预览模态框 -->
<a-modal
:open="previewVisible"
:title="previewTitle"
:footer="null"
@cancel="previewVisible = false"
>
<img alt="预览图片" style="width: 100%" :src="previewImage" />
</a-modal>
</div>
</template>
<style scoped>
/* 响应式容器 */
.responsive-container {
width: 100%;
max-width: 1200px;
margin: 0 auto;
padding: 24px;
box-sizing: border-box;
}
/* 卡片样式 */
.status-card, .form-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
padding: clamp(16px, 4vw, 32px);
margin-bottom: 24px;
}
/* 表单网格布局 */
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
margin-bottom: 24px;
}
/* 上传区域布局 */
.upload-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: clamp(16px, 3vw, 24px);
}
/* 响应式标题 */
.page-title {
font-size: clamp(20px, 4vw, 24px);
font-weight: 600;
margin-bottom: clamp(8px, 2vw, 16px);
}
/* 响应式描述文本 */
.page-desc {
font-size: clamp(14px, 2vw, 16px);
color: #86909c;
}
/* 响应式表单项 */
:deep .ant-form-item {
margin-bottom: clamp(16px, 3vw, 24px);
}
/* 响应式上传组件 */
:deep .ant-upload-select,
:deep .ant-upload-list-picture-card-container {
width: 100% !important;
height: clamp(140px, 30vw, 180px) !important;
}
/* 移动端适配 */
@media (max-width: 768px) {
.responsive-container {
padding: 16px;
}
.status-card, .form-card {
padding: 16px;
}
.form-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.upload-section {
grid-template-columns: 1fr;
gap: 16px;
}
.section-title {
font-size: 16px;
margin-bottom: 16px;
}
.form-footer {
margin-top: 24px;
}
.submit-btn {
width: 100%;
max-width: none;
}
}
/* 平板适配 */
@media (min-width: 769px) and (max-width: 1024px) {
.responsive-container {
max-width: 900px;
}
.upload-section {
grid-template-columns: repeat(2, 1fr);
}
}
/* 大屏适配 */
@media (min-width: 1025px) {
.responsive-container {
padding: 32px;
}
.form-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* 暗色模式支持 */
@media (prefers-color-scheme: dark) {
.status-card, .form-card {
background: #1f1f1f;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.page-title {
color: #ffffff;
}
.page-desc {
color: #a8a8a8;
}
}
</style>