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:
John
2026-06-15 15:04:43 -07:00
commit 2e14873f2d
272 changed files with 64133 additions and 0 deletions
+335
View File
@@ -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>
);
}