Files
im_webapp/web/src/features/auth/EnterpriseRegisterPage.tsx
T
Your NameandClaude Sonnet 5 5c3f3b7653 feat(web): complete groups/contacts/favorites/settings/calls/system domains + extensive review-round fixes
Rounds out the full rewrite with the remaining feature domains: group
management (admin panel, mute, files, announcements, QR-based invite/join),
contacts (friend profile, add-friend, blocklist, new-friends), favorites
(real media rendering across text/image/video/voice/file/link), settings
(profile/security/privacy/general/about), calls (Agora-backed audio/video
signaling + call history), and system messages/file assistant.

Also folds in a large batch of live-verified fixes surfaced by repeated
fresh code-review passes and hands-on E2E testing against the real
backend, including: WS reconnect not resubscribing any topic after a
disconnect, missing x-im-client-mid on every send path (protocol
compliance for retry dedup), group mute/member-settings/self-profile
endpoint corrections, relationship-state type fixes across search/QR/
friend-profile, tenant-switch logout gap, password-policy now read from
the real per-tenant config instead of guessed, NavRail highlight/
aria-current desync, several call-state-machine races, and responsive
layout fixes across all four breakpoints (mobile/tablet/laptop/desktop)
including a full mobile rebuild of favorites, call UI, and the settings
home/sub-list split.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 20:16:33 +08:00

148 lines
6.8 KiB
TypeScript

import { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Icon } from '@/app/Icon';
import { Toast } from '@/app/Toast';
import { ImApiError } from '@/lib/api/client';
import { submitTenantApplication, uploadPublicFile } from '@/lib/api/tenant';
const CREDIT_CODE_RE = /^[0-9A-Z]{18}$/;
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
/** README §10.5 / old app's EnterpriseRegister -- a company registering itself as a new tenant. */
export function EnterpriseRegisterPage() {
const tenantCodeHint = (useLocation().state as { tenantCodeHint?: string } | null)?.tenantCodeHint || undefined;
const [creditCode, setCreditCode] = useState('');
const [email, setEmail] = useState('');
const [file, setFile] = useState<File | null>(null);
const [error, setError] = useState('');
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [reviewDays, setReviewDays] = useState(1);
const [toast, setToast] = useState('');
const submit = async (e: React.FormEvent) => {
e.preventDefault();
if (!CREDIT_CODE_RE.test(creditCode)) return setError('请输入 18 位统一社会信用代码');
if (email && !EMAIL_RE.test(email)) return setError('请输入有效的企业邮箱');
setSubmitting(true);
setError('');
try {
let licenseFileId: string | undefined;
if (file) {
licenseFileId = (await uploadPublicFile(file, 'enterprise_license')).file_id;
}
const data = await submitTenantApplication({
credit_code: creditCode,
enterprise_email: email || undefined,
license_file_id: licenseFileId,
tenant_code_hint: tenantCodeHint,
});
setReviewDays(Math.max(1, Math.round((data.estimated_review_hours || 24) / 24)));
setSubmitted(true);
setToast('提交成功,我们会尽快联系你');
} catch (err) {
setError(err instanceof ImApiError ? err.message : '提交失败,请稍后重试');
} finally {
setSubmitting(false);
}
};
return (
<div className="flex h-dvh w-full flex-col items-center overflow-y-auto bg-surface-page px-6 py-10">
<div className="w-full max-w-[420px]">
<Link to="/tenant" className="flex items-center gap-1 text-sm text-text-secondary no-underline">
<Icon name="arrow_back_ios_new" className="text-[16px]" />
返回
</Link>
<h1 className="mb-6 mt-4 text-[22px] font-bold text-text-title">企业注册</h1>
<ol className="mb-6 flex items-center text-xs text-text-tertiary-3">
{(['填写企业信息', '确定信息', '提交审核'] as const).map((label, i) => (
<li key={label} className="flex flex-1 items-center last:flex-none">
<span
className={`grid h-[22px] w-[22px] flex-none place-items-center rounded-full text-[11px] ${
i === 0 || submitted ? 'bg-accent text-white' : 'border border-[#d5d9df] text-text-tertiary-3'
}`}
>
{i + 1}
</span>
<span className={`ml-1.5 whitespace-nowrap ${i === 0 || submitted ? 'text-text-body' : ''}`}>{label}</span>
{i < 2 ? <span className={`mx-2 h-px flex-1 ${submitted ? 'bg-accent' : 'bg-[#d5d9df]'}`} /> : null}
</li>
))}
</ol>
<form onSubmit={submit} className="flex flex-col gap-4">
{error ? (
<div className="flex items-center gap-1.5 rounded-lg bg-error-strip-bg px-3 py-2.5 text-sm text-error-strip-text">
<Icon name="error_outline" className="text-[18px]" />
{error}
</div>
) : null}
<div className="flex flex-col gap-1.5">
<label className="text-sm text-text-body">统一社会信用代码</label>
<div className="flex h-[50px] items-center rounded-lg bg-surface-input px-3.5 has-focus:border has-focus:border-focus-border has-focus:shadow-focus-ring">
<input
value={creditCode}
onChange={(e) => setCreditCode(e.target.value.trim().toUpperCase().replace(/[^0-9A-Z]/g, ''))}
placeholder="请输入18位统一社会信用代码"
maxLength={18}
className="w-full bg-transparent text-[15px] outline-none"
/>
<Icon name="document_scanner" className="flex-none text-[20px] text-focus-border" />
</div>
<p className="m-0 text-xs text-text-tertiary-3">请填写营业执照上的统一社会信用代码</p>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-sm text-text-body">
营业执照 <span className="text-text-tertiary-3">选填</span>
</label>
<label className="flex h-24 cursor-pointer flex-col items-center justify-center gap-1 rounded-lg border-[1.5px] border-dashed border-[#c5cad2]">
<input
type="file"
accept="image/jpeg,image/png,image/webp,application/pdf"
onChange={(e) => setFile(e.target.files?.[0] || null)}
className="hidden"
/>
<Icon name="upload_file" className="text-[22px] text-accent" />
<span className="text-sm text-text-body">{file ? file.name : '点击上传营业执照'}</span>
<span className="text-xs text-text-tertiary-3">支持 JPG、PNG、WebP、PDF,不超过 10MB</span>
</label>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-sm text-text-body">
企业邮箱 <span className="text-text-tertiary-3">选填</span>
</label>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
type="email"
placeholder="请输入企业邮箱"
className="h-[50px] rounded-lg bg-surface-input px-3.5 text-[15px] outline-none"
/>
<p className="m-0 text-xs text-text-tertiary-3">用于接收审核结果及企业通知</p>
</div>
<button
type="submit"
disabled={submitted || submitting || creditCode.length !== 18}
className="h-[50px] rounded-lg bg-btn text-base font-medium text-white shadow-btn disabled:bg-btn-disabled disabled:shadow-none"
>
{submitted ? '已提交' : submitting ? '提交中…' : '提交审核'}
</button>
<p className="m-0 flex items-center justify-center gap-1 text-xs text-text-tertiary-3">
<Icon name="schedule" className="text-[16px]" />
资料提交后,预计 {reviewDays} 个工作日完成审核
</p>
</form>
</div>
<Toast message={toast} onDone={() => setToast('')} />
</div>
);
}