import React from 'react'; import Navigation from '../lib/navigation.js'; import { BASE_APP_NAME } from '../config.js'; import { resolveTenant, submitTenantApplication, submitTenantEntryFeedback, uploadPublicFile, requestVerificationCode, verifyRegisterCode, verifyPasswordResetCode, confirmPasswordReset, createSecurityChallenge, verifySecurityChallenge, getRealnameStatus, submitRealname, setIndependentPassword, getYouthMode, enableYouthMode } from '../lib/im-api.js'; const USERNAME_RE = /^[a-z0-9]{1,10}$/; const PHONE_RE = /^\d{11}$/; const INVITE_RE = /^[A-Za-z]{5}$/; const CREDIT_CODE_RE = /^[0-9A-Z]{18}$/; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // params is optional and additive only -- every existing call site that passes just `view` keeps // producing the exact same bare hash as the old `Navigation.navigateTo(view)` did. const navigate = (view, params) => Navigation.navigateTo(Navigation.composeUrl([view], params || {})); function Icon({children, className = ''}) { return ; } function AppMark({tenantName}) { return
{BASE_APP_NAME[0]}

{tenantName || BASE_APP_NAME}

; } function Header({title, back = 'tenant'}) { return

{title}

; } function Field({icon, onIconClick, action, error, className = '', ...props}) { return
{icon ? (onIconClick ? : {icon}) : null} {action}
{error ?
{error}
: null}
; } function PasswordField(props) { const [visible, setVisible] = React.useState(false); return setVisible(!visible)} aria-label={visible ? '隐藏密码' : '显示密码'}>{visible ? 'visibility' : 'visibility_off'}} />; } function Agreement({checked, onChange, prefix = `同意${BASE_APP_NAME}`}) { return ; } function Toast({message, onDone}) { React.useEffect(() => { if (!message) return undefined; const timer = window.setTimeout(onDone, 2800); return () => window.clearTimeout(timer); }, [message, onDone]); return message ?
check_circle{message}
: null; } // Shared alert for errors coming back from a real API/WS call (as opposed to local form validation, // which each form already reports inline per-field). function FormAlert({text}) { return text ?
error_outline{text}
: null; } function TenantEntry({onTenantResolved}) { const [code, setCode] = React.useState(sessionStorage.getItem('im-tenant-code') || ''); const [error, setError] = React.useState(''); const [busy, setBusy] = React.useState(false); const submit = async e => { e.preventDefault(); const value = code.trim(); if (!value || value.length > 64) { setError(value ? '企业码不能超过 64 个字符' : '请输入企业码'); return; } setBusy(true); setError(''); try { const data = await resolveTenant(value); // Kept for the mock UI's own display of the tenant name while on this screen; the durable // context (read by every subsequent /v1/im/* call and by the {hi} tenant field) is persisted // by onTenantResolved. sessionStorage.setItem('im-tenant-code', data.tenant.code || value); sessionStorage.setItem('im-tenant-name', data.tenant.name || data.tenant.code || value); onTenantResolved(data, value); navigate('login'); } catch (err) { // Per the product spec, tenant-resolve failures are shown as one generic message // regardless of reason (not found vs. disabled vs. rate limited). setError('企业码不存在或已停用,请核实后重试'); } finally { setBusy(false); } }; return
{ setCode(e.target.value); setError(''); }} placeholder="请输入企业码" autoComplete="organization" maxLength="64" error={error} autoFocus />
; } function Login({tenantName, login, loginDisabled, persist, errorText, errorLevel, onLoginRequest, onPersistenceChange, onError}) { const [account, setAccount] = React.useState(login || ''); const [password, setPassword] = React.useState(''); const [agreed, setAgreed] = React.useState(false); // The prototype (docs/images/登录页面.png) has no "stay logged in" checkbox -- a consumer IM app // just keeps you signed in. Opt in silently instead of exposing the old debug-era toggle. React.useEffect(() => { if (!persist) { onPersistenceChange(true); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Real login failures (wrong password, disabled account, ...) clear the password and keep the account, // matching the product spec. React.useEffect(() => { if (errorLevel === 'err' && errorText) { setPassword(''); } }, [errorText, errorLevel]); const submit = e => { e.preventDefault(); if (!account.trim() || !password || !agreed || loginDisabled) return; onLoginRequest(account.trim(), password); }; return
setAccount(e.target.value)} placeholder="请输入用户名/手机号" autoComplete="username" /> setPassword(e.target.value)} placeholder="请输入密码" autoComplete="current-password" />

没有账户?

; } function EnterpriseRegister() { const [creditCode, setCreditCode] = React.useState(''); const [email, setEmail] = React.useState(''); const [file, setFile] = React.useState(null); const [errors, setErrors] = React.useState({}); const [submitting, setSubmitting] = React.useState(false); const [submitted, setSubmitted] = React.useState(false); const [reviewDays, setReviewDays] = React.useState(1); const [toast, setToast] = React.useState(''); React.useEffect(() => () => file && URL.revokeObjectURL(file.url), [file]); const chooseFile = e => { const selected = e.target.files[0]; if (!selected) return; if (selected.size > 10 * 1024 * 1024 || !['image/jpeg', 'image/png', 'image/webp', 'application/pdf'].includes(selected.type)) { setErrors({...errors, file: '支持 JPG、PNG、WebP、PDF,文件不超过 10MB'}); return; } if (file) URL.revokeObjectURL(file.url); setFile({name: selected.name, type: selected.type, url: URL.createObjectURL(selected), blob: selected}); setErrors({...errors, file: ''}); }; const submit = async e => { e.preventDefault(); const next = {}; if (!CREDIT_CODE_RE.test(creditCode)) next.creditCode = '请输入 18 位统一社会信用代码'; if (email && !EMAIL_RE.test(email)) next.email = '请输入有效的企业邮箱'; setErrors(next); if (Object.keys(next).length) return; setSubmitting(true); try { let licenseFileId = ''; if (file) { const uploaded = await uploadPublicFile(file.blob, 'enterprise_license'); licenseFileId = uploaded.file_id; } const data = await submitTenantApplication({ credit_code: creditCode, enterprise_email: email, license_file_id: licenseFileId, tenant_code_hint: sessionStorage.getItem('im-tenant-code') || '' }); // Prototype (docs/images/企业注册-填写注册信息.png) states this in business days, not hours. setReviewDays(Math.max(1, Math.round((data.estimated_review_hours || 24) / 24))); setSubmitted(true); setToast('提交成功,我们会尽快联系你'); } catch (err) { setErrors({...next, form: err.message || '提交失败,请稍后重试'}); } finally { setSubmitting(false); } }; return
{/* .active marks a step (and the connector after it) as reached, not "this is the current step" -- steps stay lit once passed. There's no separate server-tracked "确定信息" state (submitting the form *is* confirming it), so step 2 only ever gets skipped straight from 1 to 3; without submitted also keeping step 1 lit, submitting made it look like step 1 had been undone instead of completed. */}
  1. 1填写企业信息
  2. 2确定信息
  3. 3提交审核
setCreditCode(e.target.value.trim().toUpperCase().replace(/[^0-9A-Z]/g, ''))} placeholder="请输入18位统一社会信用代码" maxLength="18" error={errors.creditCode} action={} />

请填写营业执照上的统一社会信用代码

{file ? : null} {errors.file ?
{errors.file}
: null} setEmail(e.target.value)} placeholder="请输入企业邮箱" type="email" error={errors.email} />

用于接收审核结果及企业通知

schedule资料提交后,预计 {reviewDays} 个工作日完成审核

setToast('')} />
; } // Per docs/images/企业注册-问题反馈.png: titled "意见反馈", 300-char textarea with the instructions // as its placeholder (not a separate label), "图片(N/9)" counter-style label, and no contact field. function EnterpriseFeedback() { const [description, setDescription] = React.useState(''); const [files, setFiles] = React.useState([]); const [error, setError] = React.useState(''); const [submitting, setSubmitting] = React.useState(false); const [toast, setToast] = React.useState(''); const addFiles = e => { const selected = Array.from(e.target.files || []).slice(0, 9 - files.length); const valid = selected.filter(file => file.size <= 10 * 1024 * 1024 && ['image/jpeg', 'image/png', 'image/webp'].includes(file.type)); setFiles(files.concat(valid.map(file => ({name: file.name, url: URL.createObjectURL(file), blob: file})))); }; const submit = async e => { e.preventDefault(); const length = description.trim().length; if (length < 10 || length > 300) { setError('问题描述需为 10 至 300 个字符'); return; } setSubmitting(true); try { const screenshotFileIds = []; for (const item of files) { const uploaded = await uploadPublicFile(item.blob, 'tenant_entry_feedback'); screenshotFileIds.push(uploaded.file_id); } await submitTenantEntryFeedback({ description: description.trim(), tenant_code_hint: sessionStorage.getItem('im-tenant-code') || '', screenshot_file_ids: screenshotFileIds }); setToast('已收到反馈,我们会尽快处理'); files.forEach(item => URL.revokeObjectURL(item.url)); setDescription(''); setFiles([]); setError(''); } catch (err) { setError(err.message || '提交失败,请稍后重试'); } finally { setSubmitting(false); } }; return