feat: add HTTP signature tools and fix independent HTTP send validation
CSS quality / css-quality (push) Has been cancelled

This commit is contained in:
hectorzhao
2026-09-15 15:58:29 +08:00
parent 18ecf8045f
commit c781313de5
23 changed files with 986 additions and 255 deletions
+266 -96
View File
@@ -1,13 +1,5 @@
import { useEffect, useState } from 'react';
import {
AlertCircle,
Check,
ChevronRight,
Landmark,
ShieldCheck,
Upload,
UserCheck,
} from 'lucide-react';
import { AlertCircle, Check, ChevronRight, Landmark, ShieldCheck, Upload, UserCheck } from 'lucide-react';
import { Button, FileActions, Input, Select, Textarea } from '@/components/ui';
import { clientApi, type EnterpriseCertification, type FileObject, type FileRef } from '@/api/adminApi';
import { displayFileName } from '@/utils/fileName';
@@ -52,7 +44,15 @@ const emptyCertificationForm: CertificationForm = {
legalPersonIdCard: '',
};
function UploadPanel({ file, uploading, onFile }: { file: FileObject | null; uploading: boolean; onFile: (file: File | undefined) => void }) {
function UploadPanel({
file,
uploading,
onFile,
}: {
file: FileObject | null;
uploading: boolean;
onFile: (file: File | undefined) => void;
}) {
const fileRef: FileRef | null = file
? { contentType: file.contentType, fileName: file.fileName, fileObjectId: file.id }
: null;
@@ -85,7 +85,11 @@ function EnterpriseStepper({ current }: { current: number }) {
return (
<div className="enterprise-stepper__item" key={label}>
<span className={['enterprise-stepper__dot', complete ? 'is-complete' : '', active ? 'is-active' : ''].filter(Boolean).join(' ')}>
<span
className={['enterprise-stepper__dot', complete ? 'is-complete' : '', active ? 'is-active' : '']
.filter(Boolean)
.join(' ')}
>
{complete ? <Check size={20} /> : step}
</span>
<strong className={active || complete ? 'is-active' : ''}>{label}</strong>
@@ -108,7 +112,9 @@ function AuthHeader({ status }: { status: CertificationStatus }) {
return (
<div className="system-page-toolbar">
<div className="sms-send-title">
<span className="sms-send-title__icon"><ShieldCheck size={22} /></span>
<span className="sms-send-title__icon">
<ShieldCheck size={22} />
</span>
<h1></h1>
</div>
<span className={`enterprise-review-status enterprise-review-status--${status}`}>{statusText[status]}</span>
@@ -140,15 +146,27 @@ export function ClientEnterpriseAuthPage() {
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 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));
const displayLegalPerson = String(
certificationMaterials.legalPerson ?? (form.legalPerson || companyInfo.legalPerson),
);
function loadCertification() {
clientApi.listEnterpriseCertifications()
clientApi
.listEnterpriseCertifications()
.then((items) => {
const latest = items[0] ?? null;
setLatestCertification(latest);
@@ -185,7 +203,8 @@ export function ClientEnterpriseAuthPage() {
function uploadLicense(file: File | undefined) {
if (!file) return;
setUploading(true);
clientApi.uploadFileObject(file, { purpose: 'enterprise_certification', prefix: 'enterprise-certifications/license' })
clientApi
.uploadFileObject(file, { purpose: 'enterprise_certification', prefix: 'enterprise-certifications/license' })
.then((fileObject) => {
setLicenseFile(fileObject);
setError('');
@@ -201,25 +220,26 @@ export function ClientEnterpriseAuthPage() {
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,
licenseFileContentType: licenseFile?.contentType,
province: form.province,
city: form.city,
address: form.address,
contactIdCard: form.contactIdCard,
contactEmail: form.contactEmail,
legalPerson: form.legalPerson,
legalPersonIdCard: form.legalPersonIdCard,
authMethod: method,
},
})
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,
licenseFileContentType: licenseFile?.contentType,
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');
@@ -231,13 +251,14 @@ export function ClientEnterpriseAuthPage() {
}
if (step === 'overview') {
const overviewCopy = status === 'approved'
? '企业认证已审核通过,可正常使用发送、签名报备等能力。'
: status === 'pending'
? '企业认证资料已提交,平台运营将在 1 个工作日内完成审核。'
: status === 'rejected'
? '企业认证未通过,请根据驳回原因修改资料后重新提交。'
: '您还未进行企业认证';
const overviewCopy =
status === 'approved'
? '企业认证已审核通过,可正常使用发送、签名报备等能力。'
: status === 'pending'
? '企业认证资料已提交,平台运营将在 1 个工作日内完成审核。'
: status === 'rejected'
? '企业认证未通过,请根据驳回原因修改资料后重新提交。'
: '您还未进行企业认证';
return (
<section className="page-stack enterprise-page">
@@ -246,20 +267,35 @@ export function ClientEnterpriseAuthPage() {
<div className={`surface enterprise-status-card enterprise-status-card--${status}`}>
<strong>{overviewCopy}</strong>
{status === 'approved' ? null : (
<button type="button" onClick={() => setStep(status === 'pending' ? 'pending' : 'profile')}>
{status === 'pending' ? '查看审核进度 >' : '企业认证 >'}
{status === 'pending' ? (
<button type="button" onClick={() => setStep('pending')}>
&gt;
</button>
)}
) : null}
</div>
<div className="surface enterprise-info-card">
<dl>
<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>
<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>
@@ -280,8 +316,19 @@ export function ClientEnterpriseAuthPage() {
<UploadPanel file={licenseFile} onFile={uploadLicense} uploading={uploading} />
<p className="enterprise-help">JPG或PNG格式5M</p>
<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} />
<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>
@@ -310,16 +357,43 @@ export function ClientEnterpriseAuthPage() {
/>
</div>
</div>
<Textarea onChange={(event) => updateForm('address', event.target.value)} placeholder="请填写详细的通讯地址,可与证件上的地址不一致" rows={5} value={form.address} />
<Textarea
onChange={(event) => updateForm('address', event.target.value)}
placeholder="请填写详细的通讯地址,可与证件上的地址不一致"
rows={5}
value={form.address}
/>
<p className="enterprise-form-note">便</p>
<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} />
<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>
<Button onClick={() => setStep('overview')} variant="secondary">
</Button>
<Button onClick={() => setStep('method')}></Button>
</div>
</div>
@@ -334,20 +408,26 @@ export function ClientEnterpriseAuthPage() {
>
<UserCheck size={56} />
<div>
<strong> <span></span></strong>
<strong>
<span></span>
</strong>
<p></p>
<p></p>
</div>
</button>
<button
className={['enterprise-method-card', method === 'recharge' ? 'is-selected' : ''].filter(Boolean).join(' ')}
className={['enterprise-method-card', method === 'recharge' ? 'is-selected' : '']
.filter(Boolean)
.join(' ')}
onClick={() => setMethod('recharge')}
type="button"
>
<Landmark size={56} />
<div>
<strong> <span>1</span></strong>
<strong>
<span>1</span>
</strong>
<p>使1</p>
<p>使</p>
<p>退</p>
@@ -355,7 +435,9 @@ export function ClientEnterpriseAuthPage() {
</button>
<div className="enterprise-actions">
<Button onClick={() => setStep('profile')} variant="secondary"></Button>
<Button onClick={() => setStep('profile')} variant="secondary">
</Button>
<Button onClick={() => setStep(method === 'face' ? 'face' : 'recharge')}></Button>
</div>
</div>
@@ -363,36 +445,68 @@ export function ClientEnterpriseAuthPage() {
{step === 'recharge' ? (
<div className="enterprise-verify-panel">
<p><span></span><strong></strong></p>
<p>
<span></span>
<strong></strong>
</p>
<p className="enterprise-verify-copy">
使 <em></em> <em>1</em>
使 <em></em> <em>1</em>{' '}
</p>
<h2></h2>
<p><span></span><strong>{form.companyName || '待填写企业名称'}</strong></p>
<small>使便</small>
<p>
<span></span>
<strong>{form.companyName || '待填写企业名称'}</strong>
</p>
<small>
使便
</small>
<div className="enterprise-info-alert">
<AlertCircle size={20} />
<span> <strong></strong> <em>7</em>2</span>
<span>
<strong></strong>{' '}
<em>7</em>
2
</span>
</div>
<div className="enterprise-actions enterprise-actions--center">
<Button disabled={submitting} onClick={submitCertification}>{submitting ? '提交中...' : '确认并充值'}</Button>
<Button onClick={() => setStep('method')} variant="secondary"></Button>
<Button disabled={submitting} onClick={submitCertification}>
{submitting ? '提交中...' : '确认并充值'}
</Button>
<Button onClick={() => setStep('method')} variant="secondary">
</Button>
</div>
</div>
) : null}
{step === 'face' ? (
<div className="enterprise-face-panel">
<p><span></span><strong></strong></p>
<p>
<span></span>
<strong></strong>
</p>
<h2></h2>
<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} />
<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>
<Button onClick={() => setStep('method')} variant="secondary">
</Button>
<Button onClick={() => setStep('faceScan')}></Button>
</div>
</div>
@@ -400,21 +514,36 @@ export function ClientEnterpriseAuthPage() {
{step === 'faceScan' ? (
<div className="enterprise-face-panel">
<p><span></span><strong></strong></p>
<p>
<span></span>
<strong></strong>
</p>
<h2></h2>
<dl className="enterprise-legal-summary">
<div><dt></dt><dd>{form.legalPerson || '-'}</dd></div>
<div><dt></dt><dd>{form.legalPersonIdCard || '-'}</dd></div>
<div>
<dt></dt>
<dd>{form.legalPerson || '-'}</dd>
</div>
<div>
<dt></dt>
<dd>{form.legalPersonIdCard || '-'}</dd>
</div>
</dl>
<div className="enterprise-qr-section">
<h2></h2>
<p>使<em>5957</em></p>
<p>
使<em>5957</em>
</p>
<div className="enterprise-qr"></div>
<span></span>
<div className="enterprise-actions enterprise-actions--center">
<Button onClick={() => setStep('face')} variant="secondary"></Button>
<Button disabled={submitting} onClick={submitCertification}>{submitting ? '提交中...' : '提交审核'}</Button>
<Button onClick={() => setStep('face')} variant="secondary">
</Button>
<Button disabled={submitting} onClick={submitCertification}>
{submitting ? '提交中...' : '提交审核'}
</Button>
</div>
</div>
</div>
@@ -422,33 +551,70 @@ export function ClientEnterpriseAuthPage() {
{step === 'pending' ? (
<div className="enterprise-result enterprise-result--pending">
<span><ShieldCheck size={70} /></span>
<span>
<ShieldCheck size={70} />
</span>
<h2></h2>
<p></p>
<p>
</p>
<dl>
<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>
<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>
<div className="enterprise-actions enterprise-actions--center">
<Button onClick={() => setStep('overview')} variant="secondary"></Button>
<Button onClick={() => setStep('overview')} variant="secondary">
</Button>
</div>
</div>
) : null}
{step === 'success' ? (
<div className="enterprise-result enterprise-result--success">
<span><Check size={70} /></span>
<span>
<Check size={70} />
</span>
<h2></h2>
<dl>
<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>
<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>
<Button onClick={() => setStep('overview')} variant="secondary">
</Button>
</div>
) : null}
@@ -457,10 +623,14 @@ export function ClientEnterpriseAuthPage() {
<span>!</span>
<h2></h2>
<p>{latestCertification?.rejectReason ?? '请根据运营端审核意见修改资料后重新提交。'}</p>
<button type="button" onClick={() => setStep('profile')}> <ChevronRight size={18} /></button>
<button type="button" onClick={() => setStep('profile')}>
<ChevronRight size={18} />
</button>
<div className="enterprise-actions enterprise-actions--center">
<Button onClick={() => setStep('profile')}></Button>
<Button onClick={() => setStep('overview')} variant="secondary"></Button>
<Button onClick={() => setStep('overview')} variant="secondary">
</Button>
</div>
</div>
) : null}