diff --git a/web/src/App.tsx b/web/src/App.tsx index 410899a7..8dd9d261 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -4,10 +4,12 @@ import { Navigate, createBrowserRouter, RouterProvider } from 'react-router-dom' import { AppShell } from '@/app/AppShell'; import { RequireAuth } from '@/app/RequireAuth'; import { SectionPlaceholder } from '@/app/SectionPlaceholder'; +import { EnterpriseRegisterPage } from '@/features/auth/EnterpriseRegisterPage'; import { LoginPage } from '@/features/auth/LoginPage'; import { RegisterPage } from '@/features/auth/RegisterPage'; import { ResetPasswordPage } from '@/features/auth/ResetPasswordPage'; import { TenantEntryPage } from '@/features/auth/TenantEntryPage'; +import { TenantFeedbackPage } from '@/features/auth/TenantFeedbackPage'; import { ConversationView } from '@/features/messages/ConversationView'; import { EmptyMain } from '@/features/messages/EmptyMain'; import { MessagesLayout } from '@/features/messages/MessagesLayout'; @@ -19,6 +21,8 @@ const router = createBrowserRouter([ { path: 'login', element: }, { path: 'register', element: }, { path: 'reset-password', element: }, + { path: 'enterprise-register', element: }, + { path: 'feedback', element: }, { element: , children: [ diff --git a/web/src/features/auth/EnterpriseRegisterPage.tsx b/web/src/features/auth/EnterpriseRegisterPage.tsx new file mode 100644 index 00000000..a49c20ab --- /dev/null +++ b/web/src/features/auth/EnterpriseRegisterPage.tsx @@ -0,0 +1,138 @@ +import { useState } from 'react'; + +import { Link } from 'react-router-dom'; + +import { Icon } from '@/app/Icon'; +import { Toast } from '@/features/auth/components/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 [creditCode, setCreditCode] = useState(''); + const [email, setEmail] = useState(''); + const [file, setFile] = useState(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 }); + 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 ( +
+
+ + + 返回 + +

企业注册

+ +
    + {(['填写企业信息', '确定信息', '提交审核'] as const).map((label, i) => ( +
  1. + + {i + 1} + + {label} + {i < 2 ? : null} +
  2. + ))} +
+ +
+ {error ? ( +
+ + {error} +
+ ) : null} + +
+ + setCreditCode(e.target.value.trim().toUpperCase().replace(/[^0-9A-Z]/g, ''))} + placeholder="请输入18位统一社会信用代码" + maxLength={18} + className="h-[50px] rounded-lg bg-surface-input px-3.5 text-[15px] outline-none" + /> +

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

+
+ +
+ + +
+ +
+ + setEmail(e.target.value)} + type="email" + placeholder="请输入企业邮箱" + className="h-[50px] rounded-lg bg-surface-input px-3.5 text-[15px] outline-none" + /> +

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

+
+ + +

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

+
+
+ setToast('')} /> +
+ ); +} diff --git a/web/src/features/auth/LoginPage.tsx b/web/src/features/auth/LoginPage.tsx index 429a8820..5311bcc4 100644 --- a/web/src/features/auth/LoginPage.tsx +++ b/web/src/features/auth/LoginPage.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { Link, Navigate, useNavigate } from 'react-router-dom'; +import { Icon } from '@/app/Icon'; import { Agreement } from '@/features/auth/components/Agreement'; import { PasswordInput } from '@/features/auth/components/PasswordInput'; import { BASE_APP_NAME } from '@/lib/config'; @@ -14,7 +15,6 @@ export function LoginPage() { const navigate = useNavigate(); const tenant = useSessionStore((s) => s.tenant); const setMyUserId = useSessionStore((s) => s.setMyUserId); - const tinode = getTinode(); const [login, setLogin] = useState(''); const [password, setPassword] = useState(''); const [agreed, setAgreed] = useState(false); @@ -29,6 +29,10 @@ export function LoginPage() { }, [error]); if (!tenant) { + // Read tenant before touching getTinode() -- getTinode() lazily creates and PERMANENTLY caches + // the singleton off whatever tenant is in the store at that moment (or a hardcoded fallback + // host if there's none yet); calling it here first would risk creating that singleton against + // the wrong tenant before this redirect even happens. return ; } @@ -38,6 +42,7 @@ export function LoginPage() { setSubmitting(true); setError(''); try { + const tinode = getTinode(); if (!tinode.isConnected()) { await connect(); } @@ -60,59 +65,75 @@ export function LoginPage() { return (
-
+
M
{BASE_APP_NAME}
-

+

企业内部沟通,安全、即时、多端同步。

单聊、群聊、通讯录、文件与收藏、音视频通话,消息与回执在桌面端和手机端保持一致。

+
+ 下载桌面客户端 + iOS / Android + 帮助中心 +
-
-
- - arrow_back_ios_new - 切换企业 - -
- {tenant.name} +
+ + + 切换企业 + + +
+ +
+
M
+ {tenant.name} +
+
+ {tenant.name} +

登录

-
- {error ? ( -
{error}
- ) : null} - setLogin(e.target.value)} - placeholder="请输入用户名/手机号" - autoComplete="username" - className="h-[50px] rounded-lg bg-surface-input px-3.5 text-[15px] outline-none focus:border focus:border-focus-border focus:shadow-focus-ring" - /> - - - 忘记密码 - - -

- 没有账户?{' '} - - 马上注册 + {error ? ( +

+ + {error} +
+ ) : null} + setLogin(e.target.value)} + placeholder="请输入用户名/手机号" + autoComplete="username" + className="h-[50px] rounded-lg bg-surface-input px-3.5 text-[15px] outline-none focus:border focus:border-focus-border focus:shadow-focus-ring" + /> + + + 忘记密码 -

- - + +

+ 没有账户?{' '} + + 马上注册 + +

+ +
+ +
); diff --git a/web/src/features/auth/RegisterPage.tsx b/web/src/features/auth/RegisterPage.tsx index 09dbf8cd..8b3e7ec6 100644 --- a/web/src/features/auth/RegisterPage.tsx +++ b/web/src/features/auth/RegisterPage.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { Link, Navigate, useNavigate } from 'react-router-dom'; +import { Icon } from '@/app/Icon'; import { Agreement } from '@/features/auth/components/Agreement'; import { PasswordInput } from '@/features/auth/components/PasswordInput'; import { VerificationCodeButton } from '@/features/auth/components/VerificationCodeButton'; @@ -99,110 +100,115 @@ export function RegisterPage() { }; return ( -
-
-
-
- M -
- {tenant.name} -
-

创建账号

- {formError ? ( -
{formError}
- ) : null} +
+
+ + + + 注册 +
+
+ +

创建账号

+ {formError ? ( +
+ + {formError} +
+ ) : null} -
- setUsername(e.target.value.replace(/[^a-z0-9]/g, '').slice(0, 10))} - placeholder="用户名(1-10位小写字母或数字)" - autoComplete="username" - className={`h-[50px] rounded-lg bg-surface-input px-3.5 text-[15px] outline-none focus:border focus:border-focus-border focus:shadow-focus-ring ${ - errors.username ? 'border border-danger-border' : '' - }`} - /> - {errors.username ?
{errors.username}
: null} -
- -
-
- 或 -
-
- -
-