feat(register): move app download banner to top with QR code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
146b396427
commit
7555f1ad5a
|
|
@ -3,6 +3,7 @@
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import QRCode from 'react-qr-code';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { apiClient } from '@/infrastructure/api/api-client';
|
import { apiClient } from '@/infrastructure/api/api-client';
|
||||||
|
|
||||||
|
|
@ -32,15 +33,12 @@ export default function RegisterPage() {
|
||||||
const [confirmPassword, setConfirmPassword] = useState('');
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [appDownloadUrl, setAppDownloadUrl] = useState<{ android?: string; ios?: string }>({});
|
const [androidDownloadUrl, setAndroidDownloadUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Try to fetch latest app version for download links
|
|
||||||
fetch('/api/app/version/check?platform=android¤t_version_code=0')
|
fetch('/api/app/version/check?platform=android¤t_version_code=0')
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((d) => {
|
.then((d) => { if (d?.downloadUrl) setAndroidDownloadUrl(d.downloadUrl); })
|
||||||
if (d?.downloadUrl) setAppDownloadUrl((prev) => ({ ...prev, android: d.downloadUrl }));
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
@ -67,28 +65,15 @@ export default function RegisterPage() {
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (password !== confirmPassword) {
|
if (password !== confirmPassword) { setError(tc('passwordsNoMatch')); return; }
|
||||||
setError(tc('passwordsNoMatch'));
|
if (password.length < 6) { setError('密码至少 6 位'); return; }
|
||||||
return;
|
if (!companyName.trim()) { setError('请填写企业名称'); return; }
|
||||||
}
|
|
||||||
if (password.length < 6) {
|
|
||||||
setError('密码至少 6 位');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!companyName.trim()) {
|
|
||||||
setError('请填写企业名称');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body: Record<string, string> = {
|
const body: Record<string, string> = { password, name, companyName };
|
||||||
password,
|
|
||||||
name,
|
|
||||||
companyName,
|
|
||||||
};
|
|
||||||
if (loginMethod === 'email') {
|
if (loginMethod === 'email') {
|
||||||
body.email = email;
|
body.email = email;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -97,10 +82,7 @@ export default function RegisterPage() {
|
||||||
body.smsCode = smsCode;
|
body.smsCode = smsCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await apiClient<RegisterResponse>('/api/v1/auth/register', {
|
const data = await apiClient<RegisterResponse>('/api/v1/auth/register', { method: 'POST', body });
|
||||||
method: 'POST',
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
|
|
||||||
localStorage.setItem('access_token', data.accessToken);
|
localStorage.setItem('access_token', data.accessToken);
|
||||||
localStorage.setItem('refresh_token', data.refreshToken);
|
localStorage.setItem('refresh_token', data.refreshToken);
|
||||||
|
|
@ -108,9 +90,7 @@ export default function RegisterPage() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = JSON.parse(atob(data.accessToken.split('.')[1]));
|
const payload = JSON.parse(atob(data.accessToken.split('.')[1]));
|
||||||
if (payload.tenantId) {
|
if (payload.tenantId) localStorage.setItem('current_tenant', JSON.stringify({ id: payload.tenantId }));
|
||||||
localStorage.setItem('current_tenant', JSON.stringify({ id: payload.tenantId }));
|
|
||||||
}
|
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
|
|
@ -123,6 +103,25 @@ export default function RegisterPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-lg space-y-6">
|
<div className="w-full max-w-lg space-y-6">
|
||||||
|
{/* App download banner — top of page */}
|
||||||
|
{androidDownloadUrl && (
|
||||||
|
<div className="flex items-center gap-5 p-4 bg-card border rounded-lg">
|
||||||
|
<div className="bg-white p-2 rounded-md shrink-0">
|
||||||
|
<QRCode value={androidDownloadUrl} size={80} />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-semibold">下载 IT0 App</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5 mb-2">注册后扫码下载 Android 客户端,将平台能力带到手机</p>
|
||||||
|
<a
|
||||||
|
href={androidDownloadUrl}
|
||||||
|
className="inline-block px-3 py-1.5 text-xs font-medium bg-primary text-primary-foreground rounded-md hover:opacity-90"
|
||||||
|
>
|
||||||
|
直接下载 APK
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<img src="/icons/logo.svg" alt="iAgent" className="w-16 h-16 mx-auto mb-3" />
|
<img src="/icons/logo.svg" alt="iAgent" className="w-16 h-16 mx-auto mb-3" />
|
||||||
|
|
@ -162,7 +161,6 @@ export default function RegisterPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
{/* Identifier field */}
|
|
||||||
{loginMethod === 'email' ? (
|
{loginMethod === 'email' ? (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">{t('email')}</label>
|
<label className="block text-sm font-medium mb-1">{t('email')}</label>
|
||||||
|
|
@ -213,7 +211,6 @@ export default function RegisterPage() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Full name */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">{t('fullName')}</label>
|
<label className="block text-sm font-medium mb-1">{t('fullName')}</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -226,7 +223,6 @@ export default function RegisterPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Company name — required for enterprise */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">{t('organizationName')}</label>
|
<label className="block text-sm font-medium mb-1">{t('organizationName')}</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -240,7 +236,6 @@ export default function RegisterPage() {
|
||||||
<p className="text-xs text-muted-foreground mt-1">{t('organizationHint')}</p>
|
<p className="text-xs text-muted-foreground mt-1">{t('organizationHint')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Password */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">{t('password')}</label>
|
<label className="block text-sm font-medium mb-1">{t('password')}</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -281,35 +276,6 @@ export default function RegisterPage() {
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* App download section */}
|
|
||||||
<div className="p-5 bg-muted/50 rounded-lg border border-dashed space-y-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-lg">📱</span>
|
|
||||||
<h3 className="text-sm font-semibold">{t('downloadApp')}</h3>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground leading-relaxed">{t('downloadAppDesc')}</p>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
{appDownloadUrl.android ? (
|
|
||||||
<a
|
|
||||||
href={appDownloadUrl.android}
|
|
||||||
className="flex-1 py-2 text-center text-xs font-medium bg-card border rounded-md hover:bg-accent transition-colors"
|
|
||||||
>
|
|
||||||
{t('downloadAndroid')}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<Link
|
|
||||||
href="/app-versions"
|
|
||||||
className="flex-1 py-2 text-center text-xs font-medium bg-card border rounded-md hover:bg-accent transition-colors"
|
|
||||||
>
|
|
||||||
{t('downloadAndroid')}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<span className="flex-1 py-2 text-center text-xs font-medium bg-card border rounded-md text-muted-foreground cursor-not-allowed">
|
|
||||||
{t('downloadIOS')} (即将上线)
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue