- 添加 .dockerignore 和 .env.example 配置文件 - 添加 .gitignore 忽略规则配置 - 创建服务端代理API路由(_file、_modules、_server) - 集成 Ant Design Vue 组件库并配置SSR样式提取 - 定义API响应类型封装 - 创建基础布局组件(blank、console) - 实现应用中心页面和组件(AppsCenter) - 添加文章列表测试页面 - 配置控制台导航菜单结构 - 实现控制台头部组件 - 创建联系页面表单
457 lines
14 KiB
Vue
457 lines
14 KiB
Vue
<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>
|