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>
148 lines
6.8 KiB
TypeScript
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>
|
|
);
|
|
}
|