Files
template-10586/app/pages/console/account/kyc.vue
赵忠林 5e26fdc7fb feat(app): 初始化项目配置和页面结构
- 添加 .dockerignore 和 .env.example 配置文件
- 添加 .gitignore 忽略规则配置
- 创建服务端代理API路由(_file、_modules、_server)
- 集成 Ant Design Vue 组件库并配置SSR样式提取
- 定义API响应类型封装
- 创建基础布局组件(blank、console)
- 实现应用中心页面和组件(AppsCenter)
- 添加文章列表测试页面
- 配置控制台导航菜单结构
- 实现控制台头部组件
- 创建联系页面表单
2026-01-17 18:23:37 +08:00

457 lines
14 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.

<template>
<div class="space-y-4">
<a-page-header title="实名认证" sub-title="企业/个人认证与资料提交">
<template #extra>
<a-space>
<a-tag v-if="current" :color="statusTagColor">{{ statusText }}</a-tag>
</a-space>
</template>
</a-page-header>
<a-card :bordered="false" class="card">
<a-alert show-icon :type="statusAlertType" :message="statusMessage" :description="statusDescription" />
<a-divider />
<a-form ref="formRef" layout="vertical" :model="form" :rules="rules" :disabled="formDisabled">
<a-form-item label="认证类型" name="type">
<a-radio-group v-model:value="form.type">
<a-radio :value="0">个人</a-radio>
<a-radio :value="1">企业</a-radio>
</a-radio-group>
</a-form-item>
<template v-if="form.type === 0">
<a-row :gutter="[16, 16]">
<a-col :xs="24" :lg="12">
<a-form-item label="真实姓名" name="realName">
<a-input v-model:value="form.realName" placeholder="请输入真实姓名" />
</a-form-item>
</a-col>
<a-col :xs="24" :lg="12">
<a-form-item label="证件号码" name="idCard">
<a-input v-model:value="form.idCard" placeholder="请输入身份证/证件号码" />
</a-form-item>
</a-col>
<a-col :xs="24" :lg="12">
<a-form-item label="手机号" name="phone">
<a-input v-model:value="form.phone" placeholder="用于联系(选填)" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="[16, 16]">
<a-col :xs="24" :lg="12">
<a-form-item label="身份证正面" name="sfz1">
<a-upload
v-model:file-list="sfz1List"
:disabled="formDisabled"
:max-count="1"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="uploadSfz1"
@remove="() => (form.sfz1 = '')"
>
<div>上传</div>
</a-upload>
</a-form-item>
</a-col>
<a-col :xs="24" :lg="12">
<a-form-item label="身份证反面" name="sfz2">
<a-upload
v-model:file-list="sfz2List"
:disabled="formDisabled"
:max-count="1"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="uploadSfz2"
@remove="() => (form.sfz2 = '')"
>
<div>上传</div>
</a-upload>
</a-form-item>
</a-col>
</a-row>
</template>
<template v-else>
<a-row :gutter="[16, 16]">
<a-col :xs="24" :lg="12">
<a-form-item label="主体名称" name="name">
<a-input v-model:value="form.name" placeholder="例如:某某科技有限公司" />
</a-form-item>
</a-col>
<a-col :xs="24" :lg="12">
<a-form-item label="营业执照号码" name="zzCode">
<a-input v-model:value="form.zzCode" placeholder="请输入统一社会信用代码/执照号" />
</a-form-item>
</a-col>
<a-col :xs="24" :lg="12">
<a-form-item label="联系人" name="realName">
<a-input v-model:value="form.realName" placeholder="请输入联系人姓名(选填)" />
</a-form-item>
</a-col>
<a-col :xs="24" :lg="12">
<a-form-item label="联系电话" name="phone">
<a-input v-model:value="form.phone" placeholder="用于联系(选填)" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="营业执照" name="zzImg">
<a-upload
v-model:file-list="zzImgList"
:disabled="formDisabled"
:max-count="1"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="uploadZzImg"
@remove="() => (form.zzImg = '')"
>
<div>上传</div>
</a-upload>
</a-form-item>
</template>
</a-form>
<div class="mt-4 flex justify-end gap-2">
<!-- <a-popconfirm-->
<!-- v-if="current?.id"-->
<!-- :title="withdrawConfirmTitle"-->
<!-- ok-text="撤回"-->
<!-- cancel-text="取消"-->
<!-- @confirm="withdraw"-->
<!-- >-->
<!-- <a-button danger :loading="submitting">撤回</a-button>-->
<!-- </a-popconfirm>-->
<a-button @click="resetForm" :disabled="submitting || formDisabled">重置</a-button>
<a-button
type="primary"
:loading="submitting"
:disabled="formDisabled"
@click="submit"
>
{{ current?.id ? '更新' : '提交' }}
</a-button>
</div>
</a-card>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { message, type FormInstance } from 'ant-design-vue'
import { getUserInfo } from '@/api/layout'
import { addUserVerify, listUserVerify, removeUserVerify, updateUserVerify } from '@/api/system/userVerify'
import { uploadFile } from '@/api/system/file'
import type { UploadFile } from 'ant-design-vue'
import type { UserVerify } from '@/api/system/userVerify/model'
definePageMeta({ layout: 'console' })
type UploadRequestOption = {
file?: File
onSuccess?: (body: unknown, file: File) => void
onError?: (err: unknown) => void
}
const loading = ref(false)
const submitting = ref(false)
const current = ref<UserVerify | null>(null)
const userId = ref<number | null>(null)
const status = computed(() => current.value?.status)
const isPending = computed(() => status.value === 0)
const isApproved = computed(() => status.value === 1)
const isRejected = computed(() => status.value === 2 || status.value === 30)
const formDisabled = computed(() => !!current.value && (isPending.value || isApproved.value))
const statusText = computed(() => {
if (isPending.value) return '待审核'
if (isApproved.value) return '审核通过'
if (isRejected.value) return '已驳回'
if (status.value === undefined || status.value === null) return '未知状态'
return `未知状态(${status.value}`
})
const statusTagColor = computed(() => {
if (isPending.value) return 'gold'
if (isApproved.value) return 'green'
if (isRejected.value) return 'red'
return 'default'
})
const statusAlertType = computed(() => {
if (!current.value) return 'info'
if (isPending.value) return 'warning'
if (isApproved.value) return 'success'
if (isRejected.value) return 'error'
return 'info'
})
const statusMessage = computed(() => {
if (!current.value) return '未提交认证资料'
const prefix = isApproved.value ? '已通过实名认证' : isRejected.value ? '实名认证已驳回' : '已提交认证资料'
return `${prefix}ID: ${current.value.id ?? '-'}`
})
const statusDescription = computed(() => {
if (!current.value) return '提交后将生成一条实名认证记录,你可随时更新或撤回。'
const time = current.value.createTime ?? current.value.updateTime ?? '-'
const reason = (current.value.comments || '').trim()
if (isApproved.value) return `审核通过时间:${time}(审核通过后不可编辑;如需变更请联系管理员)`
if (isPending.value) return `提交时间:${time}(审核中不可编辑;如需修改请先撤回后重新提交)`
if (isRejected.value) return `驳回时间:${time}${reason ? `(原因:${reason}` : ''}(请修改资料后重新提交)`
return `提交时间:${time}`
})
const withdrawConfirmTitle = computed(() => {
if (isApproved.value) return '当前已审核通过,确定撤回(删除)实名认证记录?'
if (isPending.value) return '当前正在审核中,撤回后可修改并重新提交,确定撤回?'
if (isRejected.value) return '当前已驳回,撤回后可重新提交,确定撤回?'
return '确定撤回(删除)当前实名认证记录?'
})
const formRef = ref<FormInstance>()
const form = reactive<UserVerify>({
type: 0,
name: '',
zzCode: '',
zzImg: '',
realName: '',
phone: '',
idCard: '',
sfz1: '',
sfz2: '',
status: 0,
comments: ''
})
const rules = computed(() => {
if (form.type === 1) {
return {
type: [{ required: true, type: 'number', message: '请选择认证类型' }],
name: [{ required: true, type: 'string', message: '请输入主体名称' }],
zzCode: [{ required: true, type: 'string', message: '请输入营业执照号码' }],
zzImg: [{ required: true, type: 'string', message: '请上传营业执照' }]
}
}
return {
type: [{ required: true, type: 'number', message: '请选择认证类型' }],
realName: [{ required: true, type: 'string', message: '请输入真实姓名' }],
idCard: [{ required: true, type: 'string', message: '请输入证件号码' }],
sfz1: [{ required: true, type: 'string', message: '请上传身份证正面' }],
sfz2: [{ required: true, type: 'string', message: '请上传身份证反面' }]
}
})
function applyCurrentToForm(next: UserVerify | null) {
current.value = next
form.id = next?.id
form.type = next?.type ?? 0
form.name = next?.name ?? ''
form.zzCode = next?.zzCode ?? ''
form.zzImg = next?.zzImg ?? ''
form.realName = next?.realName ?? ''
form.phone = next?.phone ?? ''
form.idCard = next?.idCard ?? ''
form.sfz1 = next?.sfz1 ?? ''
form.sfz2 = next?.sfz2 ?? ''
form.status = next?.status ?? 0
form.comments = next?.comments ?? ''
syncFileLists()
}
const sfz1List = ref<UploadFile[]>([])
const sfz2List = ref<UploadFile[]>([])
const zzImgList = ref<UploadFile[]>([])
function toFileList(url: string): UploadFile[] {
const normalized = typeof url === 'string' ? url.trim() : ''
if (!normalized) return []
return [
{
uid: normalized,
name: normalized.split('/').slice(-1)[0] || 'image',
status: 'done',
url: normalized
} as UploadFile
]
}
function syncFileLists() {
sfz1List.value = toFileList(form.sfz1 ?? '')
sfz2List.value = toFileList(form.sfz2 ?? '')
zzImgList.value = toFileList(form.zzImg ?? '')
}
function beforeUpload(file: File) {
const isImage = file.type.startsWith('image/')
if (!isImage) {
message.error('仅支持上传图片文件')
return false
}
const maxSizeMb = 5
if (file.size > maxSizeMb * 1024 * 1024) {
message.error(`图片大小不能超过 ${maxSizeMb}MB`)
return false
}
return true
}
async function doUpload(
option: UploadRequestOption,
setUrl: (url: string) => void,
setList: (list: UploadFile[]) => void
) {
const rawFile = option.file
if (!rawFile) return
try {
const record = await uploadFile(rawFile)
const url = (record?.url || record?.downloadUrl || '').trim()
if (!url) throw new Error('上传成功但未返回文件地址')
setUrl(url)
setList(
toFileList(url).map((f) => ({
...f,
uid: String(rawFile.name) + '-' + String(Date.now())
})) as UploadFile[]
)
option.onSuccess?.(record, rawFile)
} catch (e) {
option.onError?.(e)
message.error(e instanceof Error ? e.message : '上传失败')
}
}
function uploadSfz1(option: UploadRequestOption) {
return doUpload(
option,
(url) => (form.sfz1 = url),
(list) => (sfz1List.value = list)
)
}
function uploadSfz2(option: UploadRequestOption) {
return doUpload(
option,
(url) => (form.sfz2 = url),
(list) => (sfz2List.value = list)
)
}
function uploadZzImg(option: UploadRequestOption) {
return doUpload(
option,
(url) => (form.zzImg = url),
(list) => (zzImgList.value = list)
)
}
async function load() {
loading.value = true
try {
const user = await getUserInfo()
userId.value = user.userId ?? null
} catch {
userId.value = null
}
try {
if (!userId.value) {
applyCurrentToForm(null)
return
}
const list = await listUserVerify({ userId: userId.value })
const mine = Array.isArray(list)
? [...list].sort((a, b) => (Number(b.id ?? 0) - Number(a.id ?? 0)))[0]
: undefined
applyCurrentToForm(mine ?? null)
} catch (e) {
applyCurrentToForm(null)
message.error(e instanceof Error ? e.message : '加载实名认证信息失败')
} finally {
loading.value = false
}
}
async function reload() {
await load()
}
function resetForm() {
applyCurrentToForm(current.value)
formRef.value?.clearValidate()
}
async function submit() {
if (formDisabled.value) {
message.warning(isApproved.value ? '审核通过后不可编辑' : '审核中不可编辑')
return
}
try {
await formRef.value?.validate()
} catch {
return
}
submitting.value = true
try {
const payload: UserVerify = {
id: form.id,
userId: userId.value ?? form.userId,
type: form.type,
name: form.name,
zzCode: form.zzCode,
zzImg: form.zzImg,
realName: form.realName,
phone: form.phone,
idCard: form.idCard,
sfz1: form.sfz1,
sfz2: form.sfz2,
status: 0,
comments: form.comments
}
if (current.value?.id) {
await updateUserVerify(payload)
message.success('认证资料已更新')
} else {
await addUserVerify(payload)
message.success('认证资料已提交')
}
await load()
} catch (e) {
message.error(e instanceof Error ? e.message : '提交失败')
} finally {
submitting.value = false
}
}
async function withdraw() {
if (!current.value?.id) return
submitting.value = true
try {
await removeUserVerify(current.value.id)
message.success('已撤回')
await load()
} catch (e) {
message.error(e instanceof Error ? e.message : '撤回失败')
} finally {
submitting.value = false
}
}
onMounted(async () => {
await load()
})
</script>
<style scoped>
.card {
border-radius: 12px;
}
</style>