feat: 邮箱注册验证 + 9项功能修复与优化
【认证系统】 - 新增邮箱验证码注册/登录流程 (sendEmailCode / verifyEmail / forgotPassword / resetPassword) - register-email 现在需要验证码 - 服务端新增 email_verification_codes 表 + patch-email-verification.js - App.tsx 登录后 emailVerified 检查提醒 - keyServerClient token 显式传递修复 401 错误 【电商模块】 - 自动推进: 策划完成后自动生成分镜图/视频 - 模特图选项 (性别/年龄/种族/体型/场景) 注入 AI 提示词 - 任务持久化指纹修复 (图片数量替代 blob URL) - 新增「视频换装」入口 (happyhorse-1.0-video-edit) 【剧本评分】 - 新增 .docx/.doc Word 文档支持 (ZIP解压+XML提取) - 历史记录支持点击查看/恢复评测结果 【画布】 - ReactFlow 节点禁止内置拖拽避免冲突 - 连接线拖拽弹窗优化 (预览线不消失, 弹窗跟踪鼠标) 【页面修复】 - 首页轮播图改为 aspect-ratio: 16/9 解决尺寸问题 - 资产库新增悬停删除按钮 - scriptEvalClient 改用服务端 /api/ai/chat 端点 - TokenUsagePage 未登录跳过 API 调用
This commit is contained in:
@@ -169,6 +169,14 @@ function ProfilePage({
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [smsCooldown, setSmsCooldown] = useState(0);
|
||||
const [isSendingSms, setIsSendingSms] = useState(false);
|
||||
const [emailCode, setEmailCode] = useState("");
|
||||
const [emailCooldown, setEmailCooldown] = useState(0);
|
||||
const [isSendingEmail, setIsSendingEmail] = useState(false);
|
||||
const [showForgotPassword, setShowForgotPassword] = useState(false);
|
||||
const [forgotStep, setForgotStep] = useState<"email" | "code" | "newPassword">("email");
|
||||
const [forgotEmail, setForgotEmail] = useState("");
|
||||
const [forgotCode, setForgotCode] = useState("");
|
||||
const [forgotPassword, setForgotPassword] = useState("");
|
||||
|
||||
const [activePanel, setActivePanel] = useState<ProfilePanel>("works");
|
||||
const [accountPanel, setAccountPanel] = useState<AccountPanel>("credits");
|
||||
@@ -245,6 +253,70 @@ function ProfilePage({
|
||||
return () => window.clearInterval(timer);
|
||||
}, [smsCooldown]);
|
||||
|
||||
useEffect(() => {
|
||||
if (emailCooldown <= 0) return;
|
||||
const timer = window.setInterval(() => {
|
||||
setEmailCooldown((current) => Math.max(0, current - 1));
|
||||
}, 1000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [emailCooldown]);
|
||||
|
||||
const handleSendEmailCode = async (purpose: "register" | "login" | "reset" = "register") => {
|
||||
const targetEmail = purpose === "reset" ? forgotEmail : email;
|
||||
if (emailCooldown > 0 || !targetEmail.trim() || isSendingEmail) return;
|
||||
if (purpose === "register" && !betaCode.trim()) {
|
||||
setNotice("请输入企业邀请码 / 内测码后再获取验证码");
|
||||
return;
|
||||
}
|
||||
setIsSendingEmail(true);
|
||||
setNotice(null);
|
||||
try {
|
||||
const result = await keyServerClient.sendEmailCode(targetEmail, purpose, betaCode);
|
||||
setEmailCooldown(result.cooldownSeconds || 60);
|
||||
if (result.devCode) {
|
||||
setNotice(`验证码已发送(开发模式: ${result.devCode})`);
|
||||
} else {
|
||||
setNotice("验证码已发送,请查收邮件");
|
||||
}
|
||||
} catch (error) {
|
||||
setNotice(error instanceof Error ? error.message : "验证码发送失败");
|
||||
} finally {
|
||||
setIsSendingEmail(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleForgotPassword = async () => {
|
||||
if (forgotStep === "email") {
|
||||
if (!forgotEmail.trim()) { setNotice("请输入邮箱"); return; }
|
||||
try {
|
||||
await keyServerClient.forgotPassword({ email: forgotEmail });
|
||||
setForgotStep("code");
|
||||
setNotice("重置验证码已发送到您的邮箱");
|
||||
await handleSendEmailCode("reset");
|
||||
} catch (error) {
|
||||
setNotice(error instanceof Error ? error.message : "发送失败");
|
||||
}
|
||||
} else if (forgotStep === "code") {
|
||||
if (!forgotCode.trim()) { setNotice("请输入验证码"); return; }
|
||||
setForgotStep("newPassword");
|
||||
setNotice(null);
|
||||
} else {
|
||||
if (forgotPassword.length < 6) { setNotice("密码至少 6 位"); return; }
|
||||
try {
|
||||
const result = await keyServerClient.resetPassword({ email: forgotEmail, code: forgotCode, newPassword: forgotPassword });
|
||||
setNotice(result.message || "密码重置成功,请重新登录");
|
||||
setShowForgotPassword(false);
|
||||
setForgotStep("email");
|
||||
setForgotEmail("");
|
||||
setForgotCode("");
|
||||
setForgotPassword("");
|
||||
setMode("login");
|
||||
} catch (error) {
|
||||
setNotice(error instanceof Error ? error.message : "重置失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendSms = async () => {
|
||||
if (smsCooldown > 0 || !phone.trim() || isSendingSms) return;
|
||||
if (mode === "register" && !betaCode.trim()) {
|
||||
@@ -289,6 +361,10 @@ function ProfilePage({
|
||||
if (!value.trim()) return "请输入验证码";
|
||||
if (value.length !== 6) return "验证码为 6 位数字";
|
||||
return "";
|
||||
case "emailCode":
|
||||
if (!value.trim()) return "请输入邮箱验证码";
|
||||
if (value.length !== 6) return "验证码为 6 位数字";
|
||||
return "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -328,6 +404,10 @@ function ProfilePage({
|
||||
if (emailErr) errors.email = emailErr;
|
||||
const pwErr = validateField("password", password);
|
||||
if (pwErr) errors.password = pwErr;
|
||||
if (mode === "register") {
|
||||
const codeErr = validateField("emailCode", emailCode);
|
||||
if (codeErr) errors.emailCode = codeErr;
|
||||
}
|
||||
} else {
|
||||
const userErr = validateField("username", username);
|
||||
if (userErr) errors.username = userErr;
|
||||
@@ -354,7 +434,7 @@ function ProfilePage({
|
||||
const nextSession =
|
||||
mode === "login"
|
||||
? await keyServerClient.loginEmail({ email, password })
|
||||
: await keyServerClient.registerEmail({ email, password, username: username.trim() || undefined, betaCode });
|
||||
: await keyServerClient.registerEmail({ email, password, code: emailCode, username: username.trim() || undefined, betaCode });
|
||||
await onAuthComplete?.(nextSession);
|
||||
} else if (mode === "login") {
|
||||
await onLogin(username.trim(), password);
|
||||
@@ -788,7 +868,31 @@ function ProfilePage({
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
{authTab === "password" ? (
|
||||
{showForgotPassword ? (
|
||||
<div className="auth-page__forgot-box">
|
||||
<p className="auth-page__forgot-title">重置密码</p>
|
||||
{forgotStep === "email" ? (
|
||||
<input value={forgotEmail} onChange={(e) => setForgotEmail(e.target.value)} placeholder="输入注册邮箱" type="email" className="auth-page__forgot-input" />
|
||||
) : forgotStep === "code" ? (
|
||||
<div className="auth-page__sms-row">
|
||||
<input value={forgotCode} onChange={(e) => setForgotCode(e.target.value)} placeholder="输入验证码" maxLength={6} />
|
||||
<button type="button" className="auth-page__sms-btn" disabled={emailCooldown > 0 || isSendingEmail} onClick={() => void handleSendEmailCode("reset")}>
|
||||
{isSendingEmail ? "发送中" : emailCooldown > 0 ? `${emailCooldown}s` : "重新发送"}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<input type="password" value={forgotPassword} onChange={(e) => setForgotPassword(e.target.value)} placeholder="输入新密码(至少 6 位)" className="auth-page__forgot-input" />
|
||||
)}
|
||||
<div className="auth-page__forgot-actions">
|
||||
<button type="button" className="auth-page__forgot-cancel" onClick={() => { setShowForgotPassword(false); setForgotStep("email"); setForgotEmail(""); setForgotCode(""); setForgotPassword(""); setNotice(null); }}>取消</button>
|
||||
<button type="button" className="auth-page__forgot-confirm" onClick={() => void handleForgotPassword()}>
|
||||
{forgotStep === "newPassword" ? "重置密码" : "下一步"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!showForgotPassword && authTab === "password" ? (
|
||||
<>
|
||||
<label className={`auth-page__field${fieldErrors.username ? " auth-page__field--error" : ""}`}>
|
||||
<span>
|
||||
@@ -819,13 +923,13 @@ function ProfilePage({
|
||||
</label>
|
||||
{mode === "login" ? (
|
||||
<div className="auth-page__forgot">
|
||||
<button type="button">忘记密码?</button>
|
||||
<button type="button" onClick={() => { setShowForgotPassword(true); setForgotStep("email"); }}>忘记密码?</button>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{authTab === "email" ? (
|
||||
{!showForgotPassword && authTab === "email" ? (
|
||||
<>
|
||||
{mode === "register" ? (
|
||||
<label className="auth-page__field">
|
||||
@@ -871,7 +975,7 @@ function ProfilePage({
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{authTab === "phone" ? (
|
||||
{!showForgotPassword && authTab === "phone" ? (
|
||||
<>
|
||||
<label className={`auth-page__field${fieldErrors.phone ? " auth-page__field--error" : ""}`}>
|
||||
<span>
|
||||
@@ -912,9 +1016,11 @@ function ProfilePage({
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{notice ? <p className="auth-page__notice">{notice}</p> : null}
|
||||
{!showForgotPassword ? (
|
||||
<>
|
||||
{notice ? <p className="auth-page__notice">{notice}</p> : null}
|
||||
|
||||
<button type="submit" className="auth-page__submit" disabled={isSubmitting}>
|
||||
<button type="submit" className="auth-page__submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "处理中..." : mode === "login" ? "登录" : "注册"}
|
||||
</button>
|
||||
|
||||
@@ -934,6 +1040,8 @@ function ProfilePage({
|
||||
<MobileOutlined />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
Reference in New Issue
Block a user