2
0
Files
fe.wfc.user/src/views/userInfo/kyc/index.vue
2025-01-24 19:19:15 +08:00

561 lines
15 KiB
Vue

<script setup lang="ts">
import { ref, onMounted } from 'vue';
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
import { useI18n } from 'vue-i18n';
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: t('page.kyc.drive') },
{ value: 2, label: t('page.kyc.pass') },
{ value: 3, label: t('page.kyc.idcard') },
{ value: 4, label: t('page.kyc.stu') },
{ value: 5, label: t('page.kyc.health') },
{ value: 6, label: t('page.kyc.birth') }
];
// 表单数据
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(t('page.kyc.kycerror'));
// 发生错误时也设置默认值
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(t('page.kyc.support'));
return Upload.LIST_IGNORE;
}
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
message.error(t('page.kyc.file'));
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(t('page.kyc.picturesuc'));
}
} catch (error) {
console.error('Failed to upload ID card:', error);
message.error(t('page.kyc.picturefal'));
} finally {
idCardLoading.value = false;
}
return false;
};
const handlePhotoUpload: UploadProps['beforeUpload'] = async (file) => {
const isValidFormat = file.type === 'image/jpeg' || file.type === 'image/png' || file.type === 'application/pdf';
if (!isValidFormat) {
message.error(t('page.kyc.support'));
return Upload.LIST_IGNORE;
}
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
message.error(t('page.kyc.file'));
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(t('page.kyc.picturesuc'));
}
} catch (error) {
console.error('Failed to upload photo:', error);
message.error(t('page.kyc.picturefal'));
} finally {
photoLoading.value = false;
}
return false;
};
// 提交 KYC 认证
const submitKYC = async () => {
try {
if (!formData.value.fullName || !formData.value.birthDate || !formData.value.idType) {
message.error(t('page.kyc.completeinfo'));
return;
}
if (!formData.value.idCardFile || !formData.value.photoFile) {
message.error(t('page.kyc.upload'));
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(t('page.kyc.kycsubmit'));
await getKYCStatus();
} catch (error) {
console.error('Failed to submit KYC:', error);
message.error(t('page.kyc.submitfalse'));
}
};
// 修改日期禁用逻辑
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 t('page.kyc.nocertified');
case 'PENDING':
return t('page.kyc.certifing');
case 'VERIFIED':
return t('page.kyc.certified');
case 'REJECTED':
return t('page.kyc.rejected');
default:
return t('page.kyc.unstatus');
}
};
onMounted(() => {
getKYCStatus();
});
</script>
<template>
<div class="kyc-container responsive-container">
<!-- 标题部分 -->
<div class="page-header">
<h1 class="page-title">{{ t('page.kyc.kyctitle') }}</h1>
<p class="page-desc">{{ t('page.kyc.service') }}</p>
</div>
<!-- 状态卡片 -->
<div class="status-card">
<div class="status-header">
<span class="status-title">{{ t('page.kyc.cerstatus') }}</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">{{ t('page.kyc.cername') }}</span>
<span class="value">{{ kycInfo.fullName }}</span>
</div>
<div class="info-item">
<span class="label">{{ t('page.kyc.certime') }}</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">{{ t('page.kyc.cerinfo') }}</h2>
<p class="form-desc">{{ t('page.kyc.cerinfoservice') }}</p>
</div>
<a-form layout="vertical" class="kyc-form">
<!-- 基本信息部分 -->
<div class="form-section">
<h3 class="section-title">{{ t('page.kyc.baseinfo') }}</h3>
<div class="form-grid">
<a-form-item :label="t('page.kyc.realname')" required>
<a-input
v-model:value="formData.fullName"
:placeholder="t('page.kyc.realnameple')"
:maxLength="20"
/>
</a-form-item>
<a-form-item :label="t('page.kyc.birthdate')" required>
<a-date-picker
v-model:value="formData.birthDate"
class="date-picker"
:disabledDate="disabledDate"
:placeholder="t('page.kyc.birthdateple')"
/>
</a-form-item>
</div>
</div>
<!-- 证件信息部分 -->
<div class="form-section">
<h3 class="section-title">{{ t('page.kyc.idinfo') }}</h3>
<div class="form-grid">
<a-form-item :label="t('page.kyc.idtype')" required>
<a-select
v-model:value="formData.idType"
:placeholder="t('page.kyc.idtypeple')"
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="t('page.kyc.idphoto')" 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">{{ t('page.kyc.uploadidphoto') }}</div>
</div>
</a-upload>
<div class="upload-tip">{{ t('page.kyc.format') }}</div>
</a-form-item>
</div>
<div class="upload-item">
<a-form-item :label="t('page.kyc.photopic')" 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">{{ t('page.kyc.uploadphotopic') }}</div>
</div>
</a-upload>
<div class="upload-tip">{{ t('page.kyc.format') }}</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"
>
{{ t('page.kyc.submitcer') }}
</a-button>
</div>
</a-form>
</div>
<!-- 添加图片预览模态框 -->
<a-modal
:open="previewVisible"
:title="previewTitle"
:footer="null"
@cancel="previewVisible = false"
>
<img :alt="t('page.kyc.previewimage')" 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);
}
}
</style>