feat:KYC认证
This commit is contained in:
@@ -15,6 +15,7 @@ const viewEn: any = {
|
|||||||
"view.billing_billservice":"Bill service",
|
"view.billing_billservice":"Bill service",
|
||||||
"view.set-meal": "Package",
|
"view.set-meal": "Package",
|
||||||
"view.userInfo":"User Information",
|
"view.userInfo":"User Information",
|
||||||
|
"view.userInfo_kyc":"KYC certification",
|
||||||
"view.userInfo_profile":"Change Information",
|
"view.userInfo_profile":"Change Information",
|
||||||
"view.userInfo_resetpwd":"Reset Password",
|
"view.userInfo_resetpwd":"Reset Password",
|
||||||
"view.userInfo_device":"Device management",
|
"view.userInfo_device":"Device management",
|
||||||
@@ -535,6 +536,7 @@ const local: any = {
|
|||||||
deviceCount: "Device",
|
deviceCount: "Device",
|
||||||
},
|
},
|
||||||
userInfo:{
|
userInfo:{
|
||||||
|
kyc:'KYC certification',
|
||||||
user:'User',
|
user:'User',
|
||||||
ownInfo:'Personal Information',
|
ownInfo:'Personal Information',
|
||||||
changepassword:'Change password',
|
changepassword:'Change password',
|
||||||
@@ -613,7 +615,7 @@ const local: any = {
|
|||||||
usercard:{
|
usercard:{
|
||||||
changeInfo:"Change Information",
|
changeInfo:"Change Information",
|
||||||
resetpwd:"Reset password",
|
resetpwd:"Reset password",
|
||||||
KYC:"KYC Certification",
|
kyc:"KYC Certification",
|
||||||
deviceconsole:"Device management",
|
deviceconsole:"Device management",
|
||||||
access:"Currently connected device",
|
access:"Currently connected device",
|
||||||
records:"History connected",
|
records:"History connected",
|
||||||
@@ -635,6 +637,7 @@ const local: any = {
|
|||||||
updateFailed: 'Update failed'
|
updateFailed: 'Update failed'
|
||||||
},
|
},
|
||||||
recharge:{
|
recharge:{
|
||||||
|
recharge:"Recharge",
|
||||||
balanceRecharge:'Banlance Recharge',
|
balanceRecharge:'Banlance Recharge',
|
||||||
packageSubscription:'Package Subscription',
|
packageSubscription:'Package Subscription',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const viewZh: any = {
|
|||||||
"view.billing_Internetdetails":"上网详单",
|
"view.billing_Internetdetails":"上网详单",
|
||||||
"view.set-meal": "套餐",
|
"view.set-meal": "套餐",
|
||||||
"view.userInfo":"个人信息",
|
"view.userInfo":"个人信息",
|
||||||
|
"view.userInfo_kyc":"KYC认证",
|
||||||
"view.userInfo_profile":"修改信息",
|
"view.userInfo_profile":"修改信息",
|
||||||
"view.userInfo_resetpwd":"修改密码",
|
"view.userInfo_resetpwd":"修改密码",
|
||||||
"view.userInfo_device":"设备管理",
|
"view.userInfo_device":"设备管理",
|
||||||
@@ -535,6 +536,7 @@ const local:any = {
|
|||||||
deviceCount: "设备数",
|
deviceCount: "设备数",
|
||||||
},
|
},
|
||||||
userInfo:{
|
userInfo:{
|
||||||
|
kyc:'KYC认证',
|
||||||
user:'用户',
|
user:'用户',
|
||||||
ownInfo:'个人信息',
|
ownInfo:'个人信息',
|
||||||
changepassword:'修改密码',
|
changepassword:'修改密码',
|
||||||
@@ -613,7 +615,7 @@ const local:any = {
|
|||||||
usercard:{
|
usercard:{
|
||||||
changeInfo:"修改信息",
|
changeInfo:"修改信息",
|
||||||
resetpwd:"修改密码",
|
resetpwd:"修改密码",
|
||||||
KYC:"KYC认证",
|
kyc:"KYC认证",
|
||||||
deviceconsole:"设备管理",
|
deviceconsole:"设备管理",
|
||||||
access:"当前设备",
|
access:"当前设备",
|
||||||
records:"历史连接",
|
records:"历史连接",
|
||||||
@@ -635,6 +637,7 @@ const local:any = {
|
|||||||
updateFailed: '更新失败'
|
updateFailed: '更新失败'
|
||||||
},
|
},
|
||||||
recharge:{
|
recharge:{
|
||||||
|
recharge:"充值服务",
|
||||||
balanceRecharge:'余额充值',
|
balanceRecharge:'余额充值',
|
||||||
packageSubscription:'套餐办理',
|
packageSubscription:'套餐办理',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -334,6 +334,18 @@ export const customRoutes: GeneratedRoute[] = [
|
|||||||
hideInMenu: true
|
hideInMenu: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'billing_recharge',
|
||||||
|
path: '/billing/recharge',
|
||||||
|
component: 'view.billing_recharge',
|
||||||
|
meta: {
|
||||||
|
title: '充值服务',
|
||||||
|
i18nKey: 'view.recharge_rechargeservice',
|
||||||
|
icon: 'material-symbols:filter-list-off',
|
||||||
|
order: 2,
|
||||||
|
hideInMenu: true
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'billing_histories',
|
name: 'billing_histories',
|
||||||
path: '/billing/histories',
|
path: '/billing/histories',
|
||||||
@@ -342,7 +354,7 @@ export const customRoutes: GeneratedRoute[] = [
|
|||||||
title: '历史查询',
|
title: '历史查询',
|
||||||
i18nKey: 'view.billing_histories',
|
i18nKey: 'view.billing_histories',
|
||||||
icon: 'material-symbols:filter-list-off',
|
icon: 'material-symbols:filter-list-off',
|
||||||
order: 2
|
order: 3
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -353,7 +365,7 @@ export const customRoutes: GeneratedRoute[] = [
|
|||||||
title: '充值记录',
|
title: '充值记录',
|
||||||
i18nKey: 'view.billing_Rechargehistory',
|
i18nKey: 'view.billing_Rechargehistory',
|
||||||
icon: 'material-symbols:filter-list-off',
|
icon: 'material-symbols:filter-list-off',
|
||||||
order: 7
|
order: 4
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -415,6 +427,17 @@ export const customRoutes: GeneratedRoute[] = [
|
|||||||
hideInMenu: true
|
hideInMenu: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'user-info_kyc',
|
||||||
|
path: '/userInfo/kyc',
|
||||||
|
component: 'view.userInfo_kyc',
|
||||||
|
meta: {
|
||||||
|
title: 'KYC认证',
|
||||||
|
i18nKey: 'view.userInfo_kyc',
|
||||||
|
order:18,
|
||||||
|
hideInMenu: true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'user-info_profile',
|
name: 'user-info_profile',
|
||||||
path: '/userInfo/profile',
|
path: '/userInfo/profile',
|
||||||
|
|||||||
@@ -160,4 +160,36 @@ export function fetchPackageHistory(params: Api.Package.PackageHistoryQueryParam
|
|||||||
params
|
params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/** Get KYC status */
|
||||||
|
export function fetchKYCStatus() {
|
||||||
|
return request<Api.KYC.KYCInfo>({
|
||||||
|
url: '/u/kyc/page',
|
||||||
|
method: 'get'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Submit KYC verification */
|
||||||
|
export function submitKYCVerification(data: Api.KYC.KYCVerifyParams) {
|
||||||
|
return request({
|
||||||
|
url: '/u/kyc/verify',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Upload file */
|
||||||
|
export function uploadFile(file: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
return request<{ name: string; url: string }>({
|
||||||
|
url: '/file/upload',
|
||||||
|
method: 'post',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
},
|
||||||
|
data: formData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
42
src/typings/api.d.ts
vendored
42
src/typings/api.d.ts
vendored
@@ -721,4 +721,46 @@ declare namespace Api {
|
|||||||
/** Combined order params type */
|
/** Combined order params type */
|
||||||
type SubmitOrderParams = PackageOrderParams | RechargeOrderParams;
|
type SubmitOrderParams = PackageOrderParams | RechargeOrderParams;
|
||||||
}
|
}
|
||||||
|
namespace KYC {
|
||||||
|
/** KYC status enum */
|
||||||
|
type KYCStatus = 0 | 1 | 2; // 0: 未认证, 1: 认证中, 2: 已认证
|
||||||
|
|
||||||
|
/** KYC info */
|
||||||
|
interface KYCInfo {
|
||||||
|
status: KYCStatus;
|
||||||
|
fullName?: string;
|
||||||
|
birthDate?: string;
|
||||||
|
idCardFile?: string;
|
||||||
|
photoFile?: string;
|
||||||
|
rejectReason?: string;
|
||||||
|
verifiedTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** KYC response */
|
||||||
|
interface KYCResponse {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
data: KYCInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ID Type enum */
|
||||||
|
enum IDType {
|
||||||
|
DRIVERS_LICENSE = 1,
|
||||||
|
PASSPORT = 2,
|
||||||
|
RESIDENCE_PERMIT = 3,
|
||||||
|
STUDENT_ID = 4,
|
||||||
|
MEDICARE_CARD = 5,
|
||||||
|
BIRTH_CERTIFICATE = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
/** KYC verify params */
|
||||||
|
interface KYCVerifyParams {
|
||||||
|
realName:string;
|
||||||
|
birthDate: string;
|
||||||
|
idType: IDType; // 改为数字类型
|
||||||
|
idFile: string;
|
||||||
|
identifyPicture: string;
|
||||||
|
kycRequestStatus: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
src/typings/auto-imports.d.ts
vendored
3
src/typings/auto-imports.d.ts
vendored
@@ -102,6 +102,7 @@ declare global {
|
|||||||
const fetchGetMenuTree: typeof import('../service/api/menu')['fetchGetMenuTree']
|
const fetchGetMenuTree: typeof import('../service/api/menu')['fetchGetMenuTree']
|
||||||
const fetchHistoricalDevices: typeof import('../service/api/auth')['fetchHistoricalDevices']
|
const fetchHistoricalDevices: typeof import('../service/api/auth')['fetchHistoricalDevices']
|
||||||
const fetchIsRouteExist: typeof import('../service/api/route')['fetchIsRouteExist']
|
const fetchIsRouteExist: typeof import('../service/api/route')['fetchIsRouteExist']
|
||||||
|
const fetchKYCStatus: typeof import('../service/api/auth')['fetchKYCStatus']
|
||||||
const fetchLogin: typeof import('../service/api/auth')['fetchLogin']
|
const fetchLogin: typeof import('../service/api/auth')['fetchLogin']
|
||||||
const fetchPackageHistory: typeof import('../service/api/auth')['fetchPackageHistory']
|
const fetchPackageHistory: typeof import('../service/api/auth')['fetchPackageHistory']
|
||||||
const fetchPackageList: typeof import('../service/api/auth')['fetchPackageList']
|
const fetchPackageList: typeof import('../service/api/auth')['fetchPackageList']
|
||||||
@@ -204,6 +205,7 @@ declare global {
|
|||||||
const shallowRef: typeof import('vue')['shallowRef']
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
const sortRoutesByOrder: typeof import('../store/modules/route/shared')['sortRoutesByOrder']
|
const sortRoutesByOrder: typeof import('../store/modules/route/shared')['sortRoutesByOrder']
|
||||||
const storeToRefs: typeof import('pinia')['storeToRefs']
|
const storeToRefs: typeof import('pinia')['storeToRefs']
|
||||||
|
const submitKYCVerification: typeof import('../service/api/auth')['submitKYCVerification']
|
||||||
const submitOrder: typeof import('../service/api/auth')['submitOrder']
|
const submitOrder: typeof import('../service/api/auth')['submitOrder']
|
||||||
const submitPackageOrder: typeof import('../service/api/auth')['submitPackageOrder']
|
const submitPackageOrder: typeof import('../service/api/auth')['submitPackageOrder']
|
||||||
const suite: typeof import('vitest')['suite']
|
const suite: typeof import('vitest')['suite']
|
||||||
@@ -236,6 +238,7 @@ declare global {
|
|||||||
const updateLocaleOfGlobalMenus: typeof import('../store/modules/route/shared')['updateLocaleOfGlobalMenus']
|
const updateLocaleOfGlobalMenus: typeof import('../store/modules/route/shared')['updateLocaleOfGlobalMenus']
|
||||||
const updateTabByI18nKey: typeof import('../store/modules/tab/shared')['updateTabByI18nKey']
|
const updateTabByI18nKey: typeof import('../store/modules/tab/shared')['updateTabByI18nKey']
|
||||||
const updateTabsByI18nKey: typeof import('../store/modules/tab/shared')['updateTabsByI18nKey']
|
const updateTabsByI18nKey: typeof import('../store/modules/tab/shared')['updateTabsByI18nKey']
|
||||||
|
const uploadFile: typeof import('../service/api/auth')['uploadFile']
|
||||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||||
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
||||||
const useAntdForm: typeof import('../hooks/common/form')['useAntdForm']
|
const useAntdForm: typeof import('../hooks/common/form')['useAntdForm']
|
||||||
|
|||||||
2
src/typings/components.d.ts
vendored
2
src/typings/components.d.ts
vendored
@@ -7,6 +7,7 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
AAlert: typeof import('ant-design-vue/es')['Alert']
|
||||||
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
|
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
|
||||||
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
|
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
|
||||||
AButton: typeof import('ant-design-vue/es')['Button']
|
AButton: typeof import('ant-design-vue/es')['Button']
|
||||||
@@ -54,6 +55,7 @@ declare module 'vue' {
|
|||||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||||
ATree: typeof import('ant-design-vue/es')['Tree']
|
ATree: typeof import('ant-design-vue/es')['Tree']
|
||||||
ATreeSelect: typeof import('ant-design-vue/es')['TreeSelect']
|
ATreeSelect: typeof import('ant-design-vue/es')['TreeSelect']
|
||||||
|
AUpload: typeof import('ant-design-vue/es')['Upload']
|
||||||
BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
|
BetterScroll: typeof import('./../components/custom/better-scroll.vue')['default']
|
||||||
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
|
ButtonIcon: typeof import('./../components/custom/button-icon.vue')['default']
|
||||||
CountTo: typeof import('./../components/custom/count-to.vue')['default']
|
CountTo: typeof import('./../components/custom/count-to.vue')['default']
|
||||||
|
|||||||
549
src/views/userInfo/kyc/index.vue
Normal file
549
src/views/userInfo/kyc/index.vue
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
<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>(0);
|
||||||
|
const kycInfo = ref<Partial<Api.KYC.KYCInfo>>({
|
||||||
|
status: 0,
|
||||||
|
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, error } = await fetchKYCStatus();
|
||||||
|
if (error) {
|
||||||
|
console.error('Failed to fetch KYC status:', error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data) {
|
||||||
|
kycStatus.value = data.status;
|
||||||
|
kycInfo.value = data;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch KYC status:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理文件上传
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化出生日期为 YYYY-MM-DD 格式
|
||||||
|
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) => {
|
||||||
|
switch (status) {
|
||||||
|
case 0:
|
||||||
|
return 'warning'; // 未认证:橙色警告
|
||||||
|
case 1:
|
||||||
|
return 'processing'; // 认证中:蓝色处理中
|
||||||
|
case 2:
|
||||||
|
return 'success'; // 已认证:绿色成功
|
||||||
|
default:
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取状态文本
|
||||||
|
const getStatusText = (status: Api.KYC.KYCStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case 0:
|
||||||
|
return '未认证';
|
||||||
|
case 1:
|
||||||
|
return '认证中';
|
||||||
|
case 2:
|
||||||
|
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 === 2" 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 !== 2" 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 === 1"
|
||||||
|
@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>
|
||||||
Reference in New Issue
Block a user