Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getMe, getWechatAuthConfig, login, loginLegacy, register, resetPassword } from '../api/client';
|
||||
import type { CapabilityMap, PortalUser } from '../types';
|
||||
import { buildWechatAuthorizeUrl, isWechatContext, readWechatAuthError } from '../utils/wechat';
|
||||
import { TKMindAvatar } from './TKMindAvatar';
|
||||
import { resolvePlazaHomeUrl } from '../utils/publicUrl';
|
||||
|
||||
type Mode = 'login' | 'register' | 'reset';
|
||||
|
||||
const MODE_META: Record<Mode, { title: string; desc: string }> = {
|
||||
login: { title: 'TKMind', desc: '登录你的账号' },
|
||||
register: { title: '创建账号', desc: '填写信息完成注册' },
|
||||
reset: { title: '重置密码', desc: '验证注册邮箱后设置新密码' },
|
||||
};
|
||||
|
||||
export function AuthView({
|
||||
legacyMode,
|
||||
onAuth,
|
||||
}: {
|
||||
legacyMode?: boolean;
|
||||
onAuth: (
|
||||
user?: PortalUser | null,
|
||||
capabilities?: CapabilityMap,
|
||||
grantedSkills?: string[],
|
||||
) => void;
|
||||
}) {
|
||||
const [mode, setMode] = useState<Mode>('login');
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [wechatEnabled, setWechatEnabled] = useState(false);
|
||||
const [wechatContext, setWechatContext] = useState(() =>
|
||||
isWechatContext({ search: window.location.search }),
|
||||
);
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const fromPlaza = (searchParams.get('utm_source') ?? '').toLowerCase() === 'plaza';
|
||||
const returnTo = searchParams.get('return_to');
|
||||
|
||||
useEffect(() => {
|
||||
const wechatError = readWechatAuthError();
|
||||
if (wechatError) {
|
||||
setError(wechatError);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (legacyMode) return;
|
||||
void getWechatAuthConfig().then((config) => {
|
||||
setWechatEnabled(config.enabled);
|
||||
setWechatContext((prev) =>
|
||||
isWechatContext({
|
||||
search: window.location.search,
|
||||
serverInWechat: config.inWechat,
|
||||
}) || prev,
|
||||
);
|
||||
});
|
||||
}, [legacyMode]);
|
||||
|
||||
const showWechatLogin = !legacyMode && mode === 'login' && wechatContext && wechatEnabled;
|
||||
|
||||
const switchMode = (next: Mode) => {
|
||||
setMode(next);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
setConfirmPassword('');
|
||||
};
|
||||
|
||||
const handleWechatLogin = () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
window.location.href = buildWechatAuthorizeUrl({
|
||||
returnTo: returnTo ?? undefined,
|
||||
utmSource: params.get('utm_source') ?? params.get('from') ?? 'wechat',
|
||||
utmMedium: params.get('utm_medium') ?? undefined,
|
||||
utmCampaign: params.get('utm_campaign') ?? undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const handleLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
if (legacyMode) {
|
||||
await loginLegacy(password);
|
||||
onAuth(null);
|
||||
return;
|
||||
}
|
||||
const user = await login(username, password);
|
||||
const me = await getMe().catch(() => null);
|
||||
onAuth(user, me?.capabilities, me?.grantedSkills);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '登录失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRegister = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
await register(
|
||||
username,
|
||||
password,
|
||||
displayName || undefined,
|
||||
email,
|
||||
{
|
||||
utm_source: searchParams.get('utm_source') ?? undefined,
|
||||
utm_medium: searchParams.get('utm_medium') ?? undefined,
|
||||
utm_campaign: searchParams.get('utm_campaign') ?? undefined,
|
||||
ref: searchParams.get('ref') ?? undefined,
|
||||
},
|
||||
);
|
||||
const user = await login(username, password);
|
||||
const me = await getMe().catch(() => null);
|
||||
onAuth(user, me?.capabilities, me?.grantedSkills);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '注册失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (password !== confirmPassword) {
|
||||
setError('两次输入的密码不一致');
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
try {
|
||||
await resetPassword(username, email, password);
|
||||
setSuccess('密码已重置,请使用新密码登录');
|
||||
setPassword('');
|
||||
setConfirmPassword('');
|
||||
setTimeout(() => switchMode('login'), 1500);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : '重置失败');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const meta = legacyMode
|
||||
? { title: 'TKMind', desc: '内部访问入口' }
|
||||
: fromPlaza
|
||||
? {
|
||||
...MODE_META[mode],
|
||||
desc:
|
||||
mode === 'register'
|
||||
? '注册后将返回 Plaza 继续浏览'
|
||||
: '登录后将返回 Plaza 继续浏览',
|
||||
}
|
||||
: MODE_META[mode];
|
||||
|
||||
const plazaBackUrl = (() => {
|
||||
if (!returnTo) return resolvePlazaHomeUrl();
|
||||
try {
|
||||
return new URL(returnTo, window.location.origin).toString();
|
||||
} catch {
|
||||
return resolvePlazaHomeUrl();
|
||||
}
|
||||
})();
|
||||
|
||||
const onSubmit =
|
||||
mode === 'register' && !legacyMode
|
||||
? handleRegister
|
||||
: mode === 'reset' && !legacyMode
|
||||
? handleReset
|
||||
: handleLogin;
|
||||
|
||||
const submitLabel =
|
||||
submitting
|
||||
? '处理中…'
|
||||
: legacyMode
|
||||
? '解锁'
|
||||
: mode === 'register'
|
||||
? '注册并登录'
|
||||
: mode === 'reset'
|
||||
? '确认重置'
|
||||
: '登录';
|
||||
|
||||
const submitDisabled =
|
||||
submitting ||
|
||||
(legacyMode
|
||||
? !password.trim()
|
||||
: mode === 'login'
|
||||
? !username.trim() || !password.trim()
|
||||
: mode === 'register'
|
||||
? !username.trim() || !password.trim() || !email.trim()
|
||||
: !username.trim() ||
|
||||
!email.trim() ||
|
||||
!password.trim() ||
|
||||
!confirmPassword.trim());
|
||||
|
||||
return (
|
||||
<div className="login-container">
|
||||
<div className="login-card">
|
||||
{!legacyMode && fromPlaza && (
|
||||
<div className="login-plaza-back">
|
||||
<a href={plazaBackUrl}>← 返回广场</a>
|
||||
</div>
|
||||
)}
|
||||
{!legacyMode && (
|
||||
<div className="login-topbar">
|
||||
{mode === 'login' ? (
|
||||
<>
|
||||
<button type="button" className="login-link" onClick={() => switchMode('register')}>
|
||||
注册
|
||||
</button>
|
||||
<span className="login-topbar-sep" aria-hidden="true" />
|
||||
<button type="button" className="login-link" onClick={() => switchMode('reset')}>
|
||||
忘记密码
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button type="button" className="login-link" onClick={() => switchMode('login')}>
|
||||
返回登录
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form className="login-form" onSubmit={onSubmit}>
|
||||
<div className="login-brand">
|
||||
<TKMindAvatar size="md" className="login-brand-avatar" />
|
||||
<h1 className="login-title">{meta.title}</h1>
|
||||
<p className="login-desc">{meta.desc}</p>
|
||||
</div>
|
||||
|
||||
{showWechatLogin && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="login-wechat-btn"
|
||||
onClick={handleWechatLogin}
|
||||
disabled={submitting}
|
||||
>
|
||||
微信一键登录
|
||||
</button>
|
||||
<div className="login-divider" aria-hidden="true">
|
||||
<span>或</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="login-fields">
|
||||
{!legacyMode && (
|
||||
<input
|
||||
className={`login-input${error ? ' login-input-error' : ''}`}
|
||||
type="text"
|
||||
placeholder="用户名"
|
||||
value={username}
|
||||
autoComplete="username"
|
||||
autoFocus={!showWechatLogin}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{mode === 'register' && !legacyMode && (
|
||||
<>
|
||||
<input
|
||||
className="login-input"
|
||||
type="email"
|
||||
placeholder="邮箱"
|
||||
value={email}
|
||||
autoComplete="email"
|
||||
required
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
className="login-input"
|
||||
type="text"
|
||||
placeholder="显示名称(可选)"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{mode === 'reset' && !legacyMode && (
|
||||
<input
|
||||
className="login-input"
|
||||
type="email"
|
||||
placeholder="注册邮箱"
|
||||
value={email}
|
||||
autoComplete="email"
|
||||
required
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<input
|
||||
className={`login-input${error ? ' login-input-error' : ''}`}
|
||||
type="password"
|
||||
placeholder={mode === 'reset' ? '新密码(至少 6 位)' : '密码'}
|
||||
value={password}
|
||||
autoComplete={
|
||||
mode === 'register' || mode === 'reset' ? 'new-password' : 'current-password'
|
||||
}
|
||||
autoFocus={legacyMode}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
{mode === 'reset' && !legacyMode && (
|
||||
<input
|
||||
className={`login-input${error ? ' login-input-error' : ''}`}
|
||||
type="password"
|
||||
placeholder="确认新密码"
|
||||
value={confirmPassword}
|
||||
autoComplete="new-password"
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <p className="login-error">{error}</p>}
|
||||
{success && <p className="login-success">{success}</p>}
|
||||
|
||||
<button type="submit" className="login-btn" disabled={submitDisabled}>
|
||||
{submitLabel}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user