fix: harden real backend admin workflows and ui
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
AlertCircle,
|
||||
Check,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
UserCheck,
|
||||
} from 'lucide-react';
|
||||
import { Button, Input, Select, Textarea } from '@/components/ui';
|
||||
import { clientApi, type EnterpriseCertification, type FileObject } from '@/api/adminApi';
|
||||
|
||||
type AuthStep = 'overview' | 'profile' | 'method' | 'recharge' | 'face' | 'faceScan' | 'pending' | 'success' | 'failed';
|
||||
type AuthMethod = 'face' | 'recharge';
|
||||
@@ -22,12 +23,47 @@ const companyInfo = {
|
||||
address: '上海XXX区XX路XX号',
|
||||
};
|
||||
|
||||
function UploadPanel() {
|
||||
type CertificationForm = {
|
||||
companyName: string;
|
||||
licenseNo: string;
|
||||
province: string;
|
||||
city: string;
|
||||
address: string;
|
||||
contactName: string;
|
||||
contactIdCard: string;
|
||||
contactPhone: string;
|
||||
contactEmail: string;
|
||||
legalPerson: string;
|
||||
legalPersonIdCard: string;
|
||||
};
|
||||
|
||||
const emptyCertificationForm: CertificationForm = {
|
||||
companyName: '',
|
||||
licenseNo: '',
|
||||
province: '',
|
||||
city: '',
|
||||
address: '',
|
||||
contactName: '',
|
||||
contactIdCard: '',
|
||||
contactPhone: '',
|
||||
contactEmail: '',
|
||||
legalPerson: '',
|
||||
legalPersonIdCard: '',
|
||||
};
|
||||
|
||||
function UploadPanel({ file, uploading, onFile }: { file: FileObject | null; uploading: boolean; onFile: (file: File | undefined) => void }) {
|
||||
return (
|
||||
<div className="enterprise-upload">
|
||||
<label className="enterprise-upload">
|
||||
<Upload size={38} />
|
||||
<strong>点击上传</strong>
|
||||
</div>
|
||||
<strong>{uploading ? '上传中...' : file?.fileName ?? '点击上传'}</strong>
|
||||
<input
|
||||
accept="image/png,image/jpeg,image/webp,application/pdf"
|
||||
disabled={uploading}
|
||||
onChange={(event) => onFile(event.target.files?.[0])}
|
||||
style={{ display: 'none' }}
|
||||
type="file"
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -74,12 +110,118 @@ function AuthHeader({ status }: { status: CertificationStatus }) {
|
||||
);
|
||||
}
|
||||
|
||||
function statusFromCertification(certification: EnterpriseCertification | null): CertificationStatus {
|
||||
if (!certification) {
|
||||
return 'uncertified';
|
||||
}
|
||||
if (certification.status === 'approved') {
|
||||
return 'approved';
|
||||
}
|
||||
if (certification.status === 'rejected') {
|
||||
return 'rejected';
|
||||
}
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
export function ClientEnterpriseAuthPage() {
|
||||
const [step, setStep] = useState<AuthStep>('overview');
|
||||
const [method, setMethod] = useState<AuthMethod>('face');
|
||||
const [status, setStatus] = useState<CertificationStatus>('uncertified');
|
||||
const [form, setForm] = useState<CertificationForm>(emptyCertificationForm);
|
||||
const [latestCertification, setLatestCertification] = useState<EnterpriseCertification | null>(null);
|
||||
const [licenseFile, setLicenseFile] = useState<FileObject | null>(null);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const currentStep = step === 'profile' ? 1 : step === 'method' ? 2 : step === 'recharge' || step === 'face' || step === 'faceScan' ? 3 : step === 'pending' || step === 'success' || step === 'failed' ? 4 : 1;
|
||||
const certificationMaterials = latestCertification?.materials ?? {};
|
||||
const displayCompany = latestCertification?.companyName || form.companyName || companyInfo.name;
|
||||
const displayLicenseNo = latestCertification?.licenseNo || form.licenseNo || companyInfo.code;
|
||||
const displayAddress = String(certificationMaterials.address ?? (form.address || companyInfo.address));
|
||||
const displayLegalPerson = String(certificationMaterials.legalPerson ?? (form.legalPerson || companyInfo.legalPerson));
|
||||
|
||||
function loadCertification() {
|
||||
clientApi.listEnterpriseCertifications()
|
||||
.then((items) => {
|
||||
const latest = items[0] ?? null;
|
||||
setLatestCertification(latest);
|
||||
setStatus(statusFromCertification(latest));
|
||||
if (latest) {
|
||||
const materials = latest.materials ?? {};
|
||||
setForm({
|
||||
companyName: latest.companyName ?? '',
|
||||
licenseNo: latest.licenseNo ?? '',
|
||||
province: String(materials.province ?? ''),
|
||||
city: String(materials.city ?? ''),
|
||||
address: String(materials.address ?? ''),
|
||||
contactName: latest.contactName ?? '',
|
||||
contactIdCard: String(materials.contactIdCard ?? ''),
|
||||
contactPhone: latest.contactPhone ?? '',
|
||||
contactEmail: String(materials.contactEmail ?? ''),
|
||||
legalPerson: String(materials.legalPerson ?? ''),
|
||||
legalPersonIdCard: String(materials.legalPersonIdCard ?? ''),
|
||||
});
|
||||
}
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '企业认证信息加载失败'));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadCertification();
|
||||
}, []);
|
||||
|
||||
function updateForm<K extends keyof CertificationForm>(key: K, value: CertificationForm[K]) {
|
||||
setForm((current) => ({ ...current, [key]: value }));
|
||||
}
|
||||
|
||||
function uploadLicense(file: File | undefined) {
|
||||
if (!file) return;
|
||||
setUploading(true);
|
||||
clientApi.uploadFileObject(file, { purpose: 'enterprise_certification', prefix: 'enterprise-certifications/license' })
|
||||
.then((fileObject) => {
|
||||
setLicenseFile(fileObject);
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '营业执照上传失败'))
|
||||
.finally(() => setUploading(false));
|
||||
}
|
||||
|
||||
function submitCertification() {
|
||||
if (!form.companyName.trim() || !form.licenseNo.trim() || !form.contactName.trim() || !form.contactPhone.trim()) {
|
||||
setError('请填写企业名称、统一社会信用代码、联系人姓名和联系人手机号');
|
||||
setStep('profile');
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
clientApi.submitEnterpriseCertification({
|
||||
companyName: form.companyName.trim(),
|
||||
licenseNo: form.licenseNo.trim(),
|
||||
contactName: form.contactName.trim(),
|
||||
contactPhone: form.contactPhone.trim(),
|
||||
materials: {
|
||||
licenseFileObjectId: licenseFile?.id,
|
||||
licenseFileName: licenseFile?.fileName,
|
||||
province: form.province,
|
||||
city: form.city,
|
||||
address: form.address,
|
||||
contactIdCard: form.contactIdCard,
|
||||
contactEmail: form.contactEmail,
|
||||
legalPerson: form.legalPerson,
|
||||
legalPersonIdCard: form.legalPersonIdCard,
|
||||
authMethod: method,
|
||||
},
|
||||
})
|
||||
.then((created) => {
|
||||
setLatestCertification(created);
|
||||
setStatus('pending');
|
||||
setStep('pending');
|
||||
setError('');
|
||||
})
|
||||
.catch((failure: Error) => setError(failure.message || '企业认证提交失败'))
|
||||
.finally(() => setSubmitting(false));
|
||||
}
|
||||
|
||||
if (step === 'overview') {
|
||||
const overviewCopy = status === 'approved'
|
||||
@@ -93,6 +235,7 @@ export function ClientEnterpriseAuthPage() {
|
||||
return (
|
||||
<section className="page-stack enterprise-page">
|
||||
<AuthHeader status={status} />
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
|
||||
<div className={`surface enterprise-status-card enterprise-status-card--${status}`}>
|
||||
<strong>{overviewCopy}</strong>
|
||||
@@ -105,11 +248,11 @@ export function ClientEnterpriseAuthPage() {
|
||||
|
||||
<div className="surface enterprise-info-card">
|
||||
<dl>
|
||||
<div><dt>企业名称:</dt><dd>{status === 'approved' ? companyInfo.name : '待认证'}</dd></div>
|
||||
<div><dt>认证时间:</dt><dd>{status === 'approved' ? companyInfo.certifiedAt : '待审核完成'}</dd></div>
|
||||
<div><dt>统一社会信用代码:</dt><dd>{status === 'approved' ? companyInfo.code : '待认证'}</dd></div>
|
||||
<div><dt>通讯地址:</dt><dd>{status === 'approved' ? companyInfo.address : '待认证'}</dd></div>
|
||||
<div><dt>法定代表人:</dt><dd>{status === 'approved' ? companyInfo.legalPerson : '待认证'}</dd></div>
|
||||
<div><dt>企业名称:</dt><dd>{latestCertification ? displayCompany : '待认证'}</dd></div>
|
||||
<div><dt>认证时间:</dt><dd>{latestCertification?.reviewedAt ?? latestCertification?.submittedAt ?? '待审核完成'}</dd></div>
|
||||
<div><dt>统一社会信用代码:</dt><dd>{latestCertification ? displayLicenseNo : '待认证'}</dd></div>
|
||||
<div><dt>通讯地址:</dt><dd>{latestCertification ? displayAddress : '待认证'}</dd></div>
|
||||
<div><dt>法定代表人:</dt><dd>{latestCertification ? displayLegalPerson : '待认证'}</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
@@ -119,6 +262,7 @@ export function ClientEnterpriseAuthPage() {
|
||||
return (
|
||||
<section className="page-stack enterprise-page">
|
||||
<h1 className="enterprise-page-title">企业认证</h1>
|
||||
{error ? <p className="form-error">{error}</p> : null}
|
||||
|
||||
<div className="surface enterprise-flow-card">
|
||||
<EnterpriseStepper current={currentStep} />
|
||||
@@ -126,40 +270,46 @@ export function ClientEnterpriseAuthPage() {
|
||||
{step === 'profile' ? (
|
||||
<div className="enterprise-form-panel">
|
||||
<label className="enterprise-required">营业执照</label>
|
||||
<UploadPanel />
|
||||
<UploadPanel file={licenseFile} onFile={uploadLicense} uploading={uploading} />
|
||||
<p className="enterprise-help">请上传电子版营业执照,JPG或PNG格式,大小不超过5M</p>
|
||||
|
||||
<Input label="* 企业名称" placeholder="请填写企业全称" hint="请严格按照营业执照上的企业名称进行填写" />
|
||||
<Input label="* 统一社会信用代码/其他组织机构代码" placeholder="请填写统一社会信用代码(若无请填写其他组织机构代码)" />
|
||||
<Input label="* 企业名称" onChange={(event) => updateForm('companyName', event.target.value)} placeholder="请填写企业全称" hint="请严格按照营业执照上的企业名称进行填写" value={form.companyName} />
|
||||
<Input label="* 统一社会信用代码/其他组织机构代码" onChange={(event) => updateForm('licenseNo', event.target.value)} placeholder="请填写统一社会信用代码(若无请填写其他组织机构代码)" value={form.licenseNo} />
|
||||
|
||||
<div className="enterprise-address-selects">
|
||||
<span>* 通讯地址</span>
|
||||
<div>
|
||||
<Select
|
||||
onChange={(event) => updateForm('province', event.target.value)}
|
||||
options={[
|
||||
{ label: '请选择省/直辖市', value: '' },
|
||||
{ label: '上海市', value: 'shanghai' },
|
||||
{ label: '北京市', value: 'beijing' },
|
||||
{ label: '上海市', value: '上海市' },
|
||||
{ label: '北京市', value: '北京市' },
|
||||
{ label: '山东省', value: '山东省' },
|
||||
{ label: '河南省', value: '河南省' },
|
||||
]}
|
||||
defaultValue=""
|
||||
value={form.province}
|
||||
/>
|
||||
<Select
|
||||
onChange={(event) => updateForm('city', event.target.value)}
|
||||
options={[
|
||||
{ label: '请选择', value: '' },
|
||||
{ label: '浦东新区', value: 'pudong' },
|
||||
{ label: '徐汇区', value: 'xuhui' },
|
||||
{ label: '济南市', value: '济南市' },
|
||||
{ label: '郑州市', value: '郑州市' },
|
||||
]}
|
||||
defaultValue=""
|
||||
value={form.city}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Textarea placeholder="请填写详细的通讯地址,可与证件上的地址不一致" rows={5} />
|
||||
<Textarea onChange={(event) => updateForm('address', event.target.value)} placeholder="请填写详细的通讯地址,可与证件上的地址不一致" rows={5} value={form.address} />
|
||||
|
||||
<p className="enterprise-form-note">为方便沟通企业进展情况,需补充联系人(法人或员工都可)信息</p>
|
||||
<Input label="* 企业联系人姓名" placeholder="请填写企业联系人姓名" />
|
||||
<Input label="* 企业联系人身份证号" placeholder="请填写企业联系人身份证号" />
|
||||
<Input label="* 企业联系人手机号" placeholder="请填写企业联系人手机号" />
|
||||
<Input label="企业联系人邮箱" placeholder="请填写企业联系人邮箱" />
|
||||
<Input label="* 企业联系人姓名" onChange={(event) => updateForm('contactName', event.target.value)} placeholder="请填写企业联系人姓名" value={form.contactName} />
|
||||
<Input label="* 企业联系人身份证号" onChange={(event) => updateForm('contactIdCard', event.target.value)} placeholder="请填写企业联系人身份证号" value={form.contactIdCard} />
|
||||
<Input label="* 企业联系人手机号" onChange={(event) => updateForm('contactPhone', event.target.value)} placeholder="请填写企业联系人手机号" value={form.contactPhone} />
|
||||
<Input label="企业联系人邮箱" onChange={(event) => updateForm('contactEmail', event.target.value)} placeholder="请填写企业联系人邮箱" value={form.contactEmail} />
|
||||
|
||||
<div className="enterprise-actions">
|
||||
<Button onClick={() => setStep('overview')} variant="secondary">取消</Button>
|
||||
@@ -212,7 +362,7 @@ export function ClientEnterpriseAuthPage() {
|
||||
</p>
|
||||
|
||||
<h2>付款方信息</h2>
|
||||
<p><span>付款企业</span><strong>XXXXXXX公司</strong></p>
|
||||
<p><span>付款企业</span><strong>{form.companyName || '待填写企业名称'}</strong></p>
|
||||
<small>请使用与企业营业执照名称一致的对公账户进行转账,以便系统进行识别,若填报错误企业信息,转账将无效暨审核不通过</small>
|
||||
|
||||
<div className="enterprise-info-alert">
|
||||
@@ -221,7 +371,7 @@ export function ClientEnterpriseAuthPage() {
|
||||
</div>
|
||||
|
||||
<div className="enterprise-actions enterprise-actions--center">
|
||||
<Button onClick={() => { setStatus('pending'); setStep('pending'); }}>确认并充值</Button>
|
||||
<Button disabled={submitting} onClick={submitCertification}>{submitting ? '提交中...' : '确认并充值'}</Button>
|
||||
<Button onClick={() => setStep('method')} variant="secondary">返回选择认证方式</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -231,8 +381,8 @@ export function ClientEnterpriseAuthPage() {
|
||||
<div className="enterprise-face-panel">
|
||||
<p><span>认证方式</span><strong>企业法人人脸识别认证</strong></p>
|
||||
<h2>企业法人基本信息</h2>
|
||||
<Input label="* 企业法人姓名" placeholder="请填写企业法人姓名" />
|
||||
<Input label="* 企业法人身份证号" placeholder="请填写企业法人身份证号" />
|
||||
<Input label="* 企业法人姓名" onChange={(event) => updateForm('legalPerson', event.target.value)} placeholder="请填写企业法人姓名" value={form.legalPerson} />
|
||||
<Input label="* 企业法人身份证号" onChange={(event) => updateForm('legalPersonIdCard', event.target.value)} placeholder="请填写企业法人身份证号" value={form.legalPersonIdCard} />
|
||||
|
||||
<div className="enterprise-actions">
|
||||
<Button onClick={() => setStep('method')} variant="secondary">返回选择认证方式</Button>
|
||||
@@ -246,8 +396,8 @@ export function ClientEnterpriseAuthPage() {
|
||||
<p><span>认证方式</span><strong>企业法人人脸识别认证</strong></p>
|
||||
<h2>企业法人基本信息</h2>
|
||||
<dl className="enterprise-legal-summary">
|
||||
<div><dt>企业法人姓名</dt><dd>张三</dd></div>
|
||||
<div><dt>企业法人身份证号</dt><dd>162xxxxxxxxxxxxx</dd></div>
|
||||
<div><dt>企业法人姓名</dt><dd>{form.legalPerson || '-'}</dd></div>
|
||||
<div><dt>企业法人身份证号</dt><dd>{form.legalPersonIdCard || '-'}</dd></div>
|
||||
</dl>
|
||||
|
||||
<div className="enterprise-qr-section">
|
||||
@@ -257,7 +407,7 @@ export function ClientEnterpriseAuthPage() {
|
||||
<span>完成扫描操作后,资料将提交至运营端审核。</span>
|
||||
<div className="enterprise-actions enterprise-actions--center">
|
||||
<Button onClick={() => setStep('face')} variant="secondary">重新填写法人信息</Button>
|
||||
<Button onClick={() => { setStatus('pending'); setStep('pending'); }}>提交审核</Button>
|
||||
<Button disabled={submitting} onClick={submitCertification}>{submitting ? '提交中...' : '提交审核'}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,8 +419,8 @@ export function ClientEnterpriseAuthPage() {
|
||||
<h2>认证资料已提交,等待运营审核</h2>
|
||||
<p>当前状态:运营端审核中。审核通过后将自动解锁短信发送、签名报备等能力;未通过时可根据驳回原因重新提交。</p>
|
||||
<dl>
|
||||
<div><dt>企业名称:</dt><dd>{companyInfo.name}</dd></div>
|
||||
<div><dt>提交时间:</dt><dd>2026年07月02日 09:58:00</dd></div>
|
||||
<div><dt>企业名称:</dt><dd>{displayCompany}</dd></div>
|
||||
<div><dt>提交时间:</dt><dd>{latestCertification?.submittedAt ?? '-'}</dd></div>
|
||||
<div><dt>审核时效:</dt><dd>1 个工作日内</dd></div>
|
||||
<div><dt>当前节点:</dt><dd>运营端资料审核</dd></div>
|
||||
</dl>
|
||||
@@ -285,11 +435,11 @@ export function ClientEnterpriseAuthPage() {
|
||||
<span><Check size={70} /></span>
|
||||
<h2>认证审核通过</h2>
|
||||
<dl>
|
||||
<div><dt>企业名称:</dt><dd>{companyInfo.name}</dd></div>
|
||||
<div><dt>统一社会信用代码:</dt><dd>{companyInfo.code}</dd></div>
|
||||
<div><dt>法定代表人:</dt><dd>{companyInfo.legalPerson}</dd></div>
|
||||
<div><dt>认证时间:</dt><dd>{companyInfo.certifiedAt}</dd></div>
|
||||
<div><dt>通讯地址:</dt><dd>{companyInfo.address}</dd></div>
|
||||
<div><dt>企业名称:</dt><dd>{displayCompany}</dd></div>
|
||||
<div><dt>统一社会信用代码:</dt><dd>{displayLicenseNo}</dd></div>
|
||||
<div><dt>法定代表人:</dt><dd>{displayLegalPerson}</dd></div>
|
||||
<div><dt>认证时间:</dt><dd>{latestCertification?.reviewedAt ?? latestCertification?.submittedAt ?? '-'}</dd></div>
|
||||
<div><dt>通讯地址:</dt><dd>{displayAddress}</dd></div>
|
||||
</dl>
|
||||
<Button onClick={() => setStep('overview')} variant="secondary">返回概览</Button>
|
||||
</div>
|
||||
@@ -299,7 +449,7 @@ export function ClientEnterpriseAuthPage() {
|
||||
<div className="enterprise-result enterprise-result--failed">
|
||||
<span>!</span>
|
||||
<h2>认证审核未通过</h2>
|
||||
<p>驳回原因:营业执照照片不清晰,企业联系人手机号无法核验。请修改资料后重新提交。</p>
|
||||
<p>驳回原因:{latestCertification?.rejectReason ?? '请根据运营端审核意见修改资料后重新提交。'}</p>
|
||||
<button type="button" onClick={() => setStep('profile')}>重新提交资料 <ChevronRight size={18} /></button>
|
||||
<div className="enterprise-actions enterprise-actions--center">
|
||||
<Button onClick={() => setStep('profile')}>重新提交</Button>
|
||||
|
||||
Reference in New Issue
Block a user