593 lines
24 KiB
TypeScript
593 lines
24 KiB
TypeScript
import { Suspense, useCallback, useEffect, useMemo, useState } from "react";
|
|
import {
|
|
BugOutlined,
|
|
CheckCircleFilled,
|
|
CloseOutlined,
|
|
HomeOutlined,
|
|
IdcardOutlined,
|
|
LockOutlined,
|
|
LoadingOutlined,
|
|
LoginOutlined,
|
|
LogoutOutlined,
|
|
MailOutlined,
|
|
MobileOutlined,
|
|
PictureOutlined,
|
|
SafetyOutlined,
|
|
UserOutlined,
|
|
VideoCameraOutlined,
|
|
WalletOutlined,
|
|
} from "@ant-design/icons";
|
|
import ErrorBoundary from "./components/ErrorBoundary";
|
|
import ToastContainer from "./components/toast/ToastContainer";
|
|
import { toast } from "./components/toast/toastStore";
|
|
import EcommercePage from "./features/ecommerce/EcommercePage";
|
|
import { flushPendingGenerationRecords } from "./api/generationRecordClient";
|
|
import { ossAssets } from "./data/ossAssets";
|
|
import { keyServerClient } from "./api/keyServerClient";
|
|
import { setUserMaxConcurrency } from "./api/generationConcurrency";
|
|
import {
|
|
SERVER_SESSION_EXPIRED_EVENT,
|
|
SERVER_SESSION_REPLACED_EVENT,
|
|
clearAllUserStorage,
|
|
type ServerSessionReplacedDetail,
|
|
} from "./api/serverConnection";
|
|
import { initNotificationPermission } from "./utils/generationNotifier";
|
|
import { reportError } from "./utils/errorReporting";
|
|
import { loadDarkGreenTheme } from "./styles/loadDarkGreenTheme";
|
|
import { useAppStore, useSessionStore } from "./stores";
|
|
import type { WebUserSession } from "./types";
|
|
import "./styles/ecommerce-standalone.css";
|
|
|
|
type AuthMode = "login" | "register";
|
|
type AuthMethod = "account" | "email" | "phone";
|
|
|
|
interface LocalProfilePageProps {
|
|
session: WebUserSession;
|
|
balance: number;
|
|
imageCount: number;
|
|
videoCount: number;
|
|
onBack: () => void;
|
|
onBugFeedback: () => void;
|
|
onLogout: () => void;
|
|
}
|
|
|
|
const profileWorks = [
|
|
{ title: "主图套图生成", desc: "电商主图与场景图自动生成", image: ossAssets.ecommerce.templateCases[0], type: "图像", time: "6/9 18:13" },
|
|
{ title: "A+详情页设计", desc: "产品卖点与长图详情版式", image: ossAssets.ecommerce.templateCases[1], type: "图像", time: "6/9 10:11" },
|
|
{ title: "短视频广告", desc: "产品展示短视频脚本与画面", image: ossAssets.ecommerce.productSet.hosting, type: "视频", time: "6/9 10:05" },
|
|
{ title: "模特图生成", desc: "服饰商品真人上身展示", image: ossAssets.ecommerce.tryOn.tryA, type: "图像", time: "6/9 10:03" },
|
|
{ title: "商品场景图", desc: "按平台比例输出营销素材", image: ossAssets.ecommerce.detail.gridA, type: "图像", time: "6/9 10:01" },
|
|
{ title: "高度复刻", desc: "参考图结构复刻与商品替换", image: ossAssets.ecommerce.detail.gridB, type: "图像", time: "6/9 09:39" },
|
|
{ title: "详情模块", desc: "功能卖点、参数和包装模块", image: ossAssets.ecommerce.detail.gridC, type: "图像", time: "6/8 21:20" },
|
|
{ title: "平台素材", desc: "淘宝/天猫投放图批量生成", image: ossAssets.ecommerce.detail.gridD, type: "图像", time: "6/8 18:26" },
|
|
];
|
|
|
|
function LocalAvatar({ session, size = "md" }: { session: WebUserSession; size?: "sm" | "md" | "lg" }) {
|
|
const displayName = session.user.displayName || session.user.username || "用户";
|
|
const label = displayName.trim().slice(0, 1).toUpperCase() || "用";
|
|
const avatarUrl = session.user.avatarUrl;
|
|
return (
|
|
<span className={`local-user-avatar local-user-avatar--${size}`}>
|
|
{avatarUrl ? <img src={avatarUrl} alt={displayName} /> : <span>{label}</span>}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function LocalProfilePage({ session, balance, imageCount, videoCount, onBack, onBugFeedback, onLogout }: LocalProfilePageProps) {
|
|
const displayName = session.user.displayName || session.user.username || "用户";
|
|
const workCount = Math.max(imageCount + videoCount, profileWorks.length);
|
|
const projectCount = Math.max(1, Math.round(workCount / 18));
|
|
const assetCount = Math.max(1, Math.round(workCount / 20));
|
|
|
|
return (
|
|
<section className="local-profile-page">
|
|
<div className="local-profile-page__hero">
|
|
<button type="button" className="local-profile-page__back" onClick={onBack}>
|
|
<HomeOutlined />
|
|
返回工作台
|
|
</button>
|
|
</div>
|
|
<div className="local-profile-page__body">
|
|
<aside className="local-profile-card">
|
|
<div className="local-profile-card__head">
|
|
<span className="local-profile-card__avatar-wrap">
|
|
<LocalAvatar session={session} size="lg" />
|
|
<CheckCircleFilled />
|
|
</span>
|
|
<strong>{displayName}</strong>
|
|
<span className="local-profile-card__uid">UID {session.user.id}</span>
|
|
</div>
|
|
|
|
<div className="local-profile-card__stats">
|
|
<span><strong>{projectCount}</strong><em>项目</em></span>
|
|
<span><strong>{workCount}</strong><em>作品</em></span>
|
|
<span><strong>{assetCount}</strong><em>资产</em></span>
|
|
</div>
|
|
|
|
<div className="local-profile-card__credits">
|
|
<span><em>可用积分</em><strong>{balance.toFixed(2)}</strong></span>
|
|
<span><em>图片作品</em><strong>{imageCount}</strong></span>
|
|
<span><em>视频作品</em><strong>{videoCount}</strong></span>
|
|
</div>
|
|
|
|
<div className="local-profile-card__meta">
|
|
<span><em>当前账号</em><strong>{displayName}</strong></span>
|
|
<span><em>积分余额</em><strong>{balance.toFixed(2)}</strong></span>
|
|
</div>
|
|
|
|
<button type="button" className="local-profile-card__primary" onClick={onBack}>
|
|
<HomeOutlined />
|
|
进入工作台
|
|
</button>
|
|
<button type="button" className="local-profile-card__secondary" onClick={onBugFeedback}>
|
|
<BugOutlined />
|
|
Bug 反馈
|
|
</button>
|
|
<button type="button" className="local-profile-card__danger" onClick={onLogout}>
|
|
<LogoutOutlined />
|
|
退出登录
|
|
</button>
|
|
</aside>
|
|
|
|
<main className="local-profile-main">
|
|
<div className="local-profile-tabs" role="tablist" aria-label="个人主页分类">
|
|
{["我的作品", "我的项目", "我的资产", "社区发布"].map((item, index) => (
|
|
<button key={item} type="button" className={index === 0 ? "is-active" : ""}>
|
|
{item}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<section className="local-profile-works">
|
|
<header>
|
|
<div>
|
|
<strong>代表作</strong>
|
|
<span>最近完成的高质量生成内容</span>
|
|
</div>
|
|
<em>{workCount} 项</em>
|
|
</header>
|
|
<div className="local-profile-work-grid">
|
|
{profileWorks.map((work) => (
|
|
<article key={`${work.title}-${work.time}`} className="local-profile-work-card">
|
|
<img src={work.image} alt="" />
|
|
<div>
|
|
<span>{work.type}</span>
|
|
<strong>{work.title}</strong>
|
|
<p>{work.desc}</p>
|
|
<em>已完成 · {work.time}</em>
|
|
</div>
|
|
</article>
|
|
))}
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function App() {
|
|
const session = useSessionStore((s) => s.session);
|
|
const setSession = useSessionStore((s) => s.setSession);
|
|
const clearSessionState = useSessionStore((s) => s.clearSession);
|
|
const usage = useAppStore((s) => s.usage);
|
|
const setUsage = useAppStore((s) => s.setUsage);
|
|
|
|
const [authOpen, setAuthOpen] = useState(false);
|
|
const [authMode, setAuthMode] = useState<AuthMode>("login");
|
|
const [authMethod, setAuthMethod] = useState<AuthMethod>("account");
|
|
const [username, setUsername] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [betaCode, setBetaCode] = useState("");
|
|
const [authSubmitting, setAuthSubmitting] = useState(false);
|
|
const [authError, setAuthError] = useState<string | null>(null);
|
|
const [sessionNotice, setSessionNotice] = useState<string | null>(null);
|
|
const [profileMenuOpen, setProfileMenuOpen] = useState(false);
|
|
const [currentPage, setCurrentPage] = useState<"workspace" | "profile">("workspace");
|
|
const [workspaceKey, setWorkspaceKey] = useState(0);
|
|
|
|
useEffect(() => {
|
|
void loadDarkGreenTheme();
|
|
document.documentElement.dataset.theme = "dark";
|
|
document.documentElement.dataset.uiTheme = "dark-green";
|
|
document.documentElement.style.colorScheme = "dark";
|
|
document.body.classList.add("ecommerce-standalone-body");
|
|
return () => {
|
|
document.body.classList.remove("ecommerce-standalone-body");
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const splash = document.getElementById("app-boot-splash");
|
|
if (splash) {
|
|
splash.style.opacity = "0";
|
|
const timer = window.setTimeout(() => splash.remove(), 350);
|
|
return () => window.clearTimeout(timer);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
initNotificationPermission();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!session) return;
|
|
void flushPendingGenerationRecords();
|
|
}, [session]);
|
|
|
|
useEffect(() => {
|
|
const handleUnhandled = (event: ErrorEvent) => {
|
|
reportError(event.error || new Error(event.message), "unhandled");
|
|
};
|
|
const handleRejection = (event: PromiseRejectionEvent) => {
|
|
reportError(event.reason instanceof Error ? event.reason : new Error(String(event.reason)), "rejection");
|
|
};
|
|
window.addEventListener("error", handleUnhandled);
|
|
window.addEventListener("unhandledrejection", handleRejection);
|
|
return () => {
|
|
window.removeEventListener("error", handleUnhandled);
|
|
window.removeEventListener("unhandledrejection", handleRejection);
|
|
};
|
|
}, []);
|
|
|
|
const refreshUsage = useCallback(async () => {
|
|
try {
|
|
setUsage(await keyServerClient.getUsageSummary());
|
|
} catch {
|
|
// Usage is helpful but should not block the standalone generator.
|
|
}
|
|
}, [setUsage]);
|
|
|
|
const completeAuth = useCallback(
|
|
async (nextSession: WebUserSession) => {
|
|
setSession(nextSession);
|
|
setUserMaxConcurrency(nextSession.user.maxConcurrency);
|
|
setAuthOpen(false);
|
|
setAuthError(null);
|
|
await refreshUsage();
|
|
if (nextSession.user.email && !nextSession.user.emailVerified) {
|
|
toast.info("邮箱尚未验证,部分功能可能受限");
|
|
}
|
|
},
|
|
[refreshUsage, setSession],
|
|
);
|
|
|
|
const clearAuthenticatedState = useCallback(() => {
|
|
clearAllUserStorage();
|
|
clearSessionState();
|
|
setUserMaxConcurrency(null);
|
|
setUsage({
|
|
balanceCents: 0,
|
|
imageUsed: 0,
|
|
videoUsed: 0,
|
|
textUsed: 0,
|
|
source: "preview",
|
|
});
|
|
}, [clearSessionState, setUsage]);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
const loadSession = async () => {
|
|
try {
|
|
const nextSession = await keyServerClient.getCurrentSession();
|
|
if (cancelled) return;
|
|
setSession(nextSession);
|
|
setUserMaxConcurrency(nextSession?.user.maxConcurrency);
|
|
if (nextSession) await refreshUsage();
|
|
} catch {
|
|
if (!cancelled) clearAuthenticatedState();
|
|
}
|
|
};
|
|
|
|
void loadSession();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [clearAuthenticatedState, refreshUsage, setSession]);
|
|
|
|
useEffect(() => {
|
|
const handleSessionInvalid = (event: Event) => {
|
|
const detail = (event as CustomEvent<ServerSessionReplacedDetail>).detail;
|
|
clearAuthenticatedState();
|
|
setSessionNotice(detail?.message || "登录状态已失效,请重新登录。");
|
|
setAuthOpen(true);
|
|
};
|
|
|
|
window.addEventListener(SERVER_SESSION_REPLACED_EVENT, handleSessionInvalid);
|
|
window.addEventListener(SERVER_SESSION_EXPIRED_EVENT, handleSessionInvalid);
|
|
return () => {
|
|
window.removeEventListener(SERVER_SESSION_REPLACED_EVENT, handleSessionInvalid);
|
|
window.removeEventListener(SERVER_SESSION_EXPIRED_EVENT, handleSessionInvalid);
|
|
};
|
|
}, [clearAuthenticatedState]);
|
|
|
|
const openAuth = useCallback((mode: AuthMode = "login") => {
|
|
setAuthMode(mode);
|
|
setAuthError(null);
|
|
setSessionNotice(null);
|
|
setAuthOpen(true);
|
|
}, []);
|
|
|
|
const handleSubmitAuth = async () => {
|
|
if (!username.trim() || !password) {
|
|
setAuthError(authMethod === "email" ? "请输入邮箱和密码" : authMethod === "phone" ? "请输入手机号和验证码/密码" : "请输入用户名和密码");
|
|
return;
|
|
}
|
|
setAuthSubmitting(true);
|
|
setAuthError(null);
|
|
try {
|
|
const nextSession =
|
|
authMode === "login"
|
|
? await keyServerClient.login({ username, password })
|
|
: await keyServerClient.register({ username, password, betaCode });
|
|
await completeAuth(nextSession);
|
|
} catch (error) {
|
|
setAuthError(error instanceof Error ? error.message : "登录失败,请稍后重试。");
|
|
} finally {
|
|
setAuthSubmitting(false);
|
|
}
|
|
};
|
|
|
|
const handleLogout = () => {
|
|
setProfileMenuOpen(false);
|
|
setCurrentPage("workspace");
|
|
clearAuthenticatedState();
|
|
toast.info("已退出登录");
|
|
};
|
|
|
|
const balance = Math.max(usage.balanceCents, 0) / 100;
|
|
const displayName = session?.user.displayName || session?.user.username || "用户";
|
|
const actualWorkCount = Math.max(usage.imageUsed + usage.videoUsed, 0);
|
|
const shownWorkCount = Math.max(actualWorkCount, profileWorks.length);
|
|
|
|
const avatarMenuStats = useMemo(
|
|
() => [
|
|
{ icon: <IdcardOutlined />, label: "UID", value: session?.user.id ?? "-" },
|
|
{ icon: <WalletOutlined />, label: "积分", value: `${balance.toFixed(2)} 积分` },
|
|
{ icon: <PictureOutlined />, label: "图片", value: usage.imageUsed },
|
|
{ icon: <VideoCameraOutlined />, label: "视频", value: usage.videoUsed },
|
|
{ icon: <PictureOutlined />, label: "作品", value: shownWorkCount },
|
|
],
|
|
[balance, session?.user.id, shownWorkCount, usage.imageUsed, usage.videoUsed],
|
|
);
|
|
|
|
const handleOpenProfile = () => {
|
|
setProfileMenuOpen(false);
|
|
setCurrentPage("profile");
|
|
};
|
|
|
|
const handleOpenWorkspace = () => {
|
|
setProfileMenuOpen(false);
|
|
setCurrentPage("workspace");
|
|
setWorkspaceKey((k) => k + 1);
|
|
};
|
|
|
|
const handleBugFeedback = () => {
|
|
setProfileMenuOpen(false);
|
|
toast.info("Bug 反馈入口已保留,后续可接入反馈页面。");
|
|
};
|
|
|
|
return (
|
|
<div className="ecommerce-standalone web-shell" data-theme="dark" data-ui-theme="dark-green" data-view="ecommerce">
|
|
<header className="ecommerce-standalone__topbar">
|
|
<button type="button" className="ecommerce-standalone__brand" onClick={handleOpenWorkspace}>
|
|
<span className="ecommerce-standalone__logo" aria-hidden="true">
|
|
<img src="https://stringtest.oss-cn-hangzhou.aliyuncs.com/logo.png" alt="" />
|
|
</span>
|
|
<strong>OmniAI 电商智能体</strong>
|
|
</button>
|
|
<div className="ecommerce-standalone__account">
|
|
{session ? (
|
|
<div className="ecommerce-profile-menu">
|
|
<span className="ecommerce-standalone__credits">
|
|
{(Math.max(usage.balanceCents, 0) / 100).toFixed(2)} 积分
|
|
</span>
|
|
<button
|
|
type="button"
|
|
className="ecommerce-profile-menu__trigger"
|
|
onClick={() => setProfileMenuOpen((open) => !open)}
|
|
aria-haspopup="dialog"
|
|
aria-expanded={profileMenuOpen}
|
|
>
|
|
<LocalAvatar session={session} size="sm" />
|
|
<span>{displayName}</span>
|
|
</button>
|
|
{profileMenuOpen ? (
|
|
<>
|
|
<button
|
|
type="button"
|
|
className="ecommerce-profile-popover__backdrop"
|
|
aria-label="关闭账户信息"
|
|
onClick={() => setProfileMenuOpen(false)}
|
|
/>
|
|
<section className="ecommerce-profile-popover" role="dialog" aria-label="账户信息">
|
|
<div className="ecommerce-profile-popover__head">
|
|
<LocalAvatar session={session} size="md" />
|
|
<div>
|
|
<strong>{displayName}</strong>
|
|
<span>{session.user.username}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<dl className="ecommerce-profile-popover__stats">
|
|
{avatarMenuStats.map((item) => (
|
|
<div key={item.label}>
|
|
<dt>{item.icon}{item.label}</dt>
|
|
<dd>{item.value}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
|
|
<div className="ecommerce-profile-popover__actions">
|
|
<button type="button" className="is-primary" onClick={handleOpenProfile}>
|
|
<UserOutlined />
|
|
个人中心
|
|
</button>
|
|
<button type="button" onClick={handleBugFeedback}>
|
|
<BugOutlined />
|
|
Bug 反馈
|
|
</button>
|
|
<button type="button" className="is-danger" onClick={handleLogout}>
|
|
<LogoutOutlined />
|
|
退出
|
|
</button>
|
|
</div>
|
|
</section>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
) : (
|
|
<button type="button" onClick={() => openAuth("login")}>
|
|
<LoginOutlined />
|
|
<span>登录 / 注册</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
<main className="ecommerce-standalone__content">
|
|
{currentPage === "profile" && session ? (
|
|
<LocalProfilePage
|
|
session={session}
|
|
balance={balance}
|
|
imageCount={usage.imageUsed}
|
|
videoCount={usage.videoUsed}
|
|
onBack={handleOpenWorkspace}
|
|
onBugFeedback={handleBugFeedback}
|
|
onLogout={handleLogout}
|
|
/>
|
|
) : (
|
|
<ErrorBoundary>
|
|
<Suspense
|
|
fallback={
|
|
<div className="page-loading-center">
|
|
<div className="page-loading-spinner" />
|
|
<span className="page-loading-center__text">加载中...</span>
|
|
</div>
|
|
}
|
|
>
|
|
<EcommercePage
|
|
key={workspaceKey}
|
|
projects={[]}
|
|
isAuthenticated={Boolean(session)}
|
|
onStartCreate={() => undefined}
|
|
onOpenProject={() => undefined}
|
|
onDeleteProject={() => undefined}
|
|
onImportWorkflow={() => undefined}
|
|
onCreateTask={() => undefined}
|
|
onRequireLogin={() => openAuth("login")}
|
|
initialTemplate={null}
|
|
onInitialTemplateConsumed={() => undefined}
|
|
/>
|
|
</Suspense>
|
|
</ErrorBoundary>
|
|
)}
|
|
</main>
|
|
|
|
{authOpen ? (
|
|
<div className="ecommerce-auth-modal" role="dialog" aria-modal="true" aria-labelledby="ecommerce-auth-title">
|
|
<button
|
|
type="button"
|
|
className="ecommerce-auth-modal__scrim"
|
|
aria-label="关闭登录弹窗"
|
|
onClick={() => setAuthOpen(false)}
|
|
/>
|
|
<section className="ecommerce-auth-modal__panel">
|
|
<button
|
|
type="button"
|
|
className="ecommerce-auth-modal__close"
|
|
aria-label="关闭"
|
|
onClick={() => setAuthOpen(false)}
|
|
>
|
|
<CloseOutlined />
|
|
</button>
|
|
<span className="ecommerce-auth-modal__logo" aria-hidden="true">
|
|
<img src="https://stringtest.oss-cn-hangzhou.aliyuncs.com/logo.png" alt="" />
|
|
</span>
|
|
<h2 id="ecommerce-auth-title">{authMode === "login" ? "欢迎回来" : "创建账号"}</h2>
|
|
<p className="ecommerce-auth-modal__subtitle">{authMode === "login" ? "登录后继续你的 AI 创作之旅" : "注册即可免费体验全部功能"}</p>
|
|
{sessionNotice ? <p className="ecommerce-auth-modal__notice">{sessionNotice}</p> : null}
|
|
|
|
<div className="ecommerce-auth-modal__tabs" role="tablist" aria-label="账号操作">
|
|
<button type="button" className={authMode === "login" ? "is-active" : ""} onClick={() => { setAuthMode("login"); setAuthError(null); }}>登录</button>
|
|
<button type="button" className={authMode === "register" ? "is-active" : ""} onClick={() => { setAuthMode("register"); setAuthError(null); }}>注册</button>
|
|
</div>
|
|
|
|
<div className="ecommerce-auth-modal__methods" role="tablist" aria-label="登录方式">
|
|
<button type="button" className={authMethod === "account" ? "is-active" : ""} onClick={() => setAuthMethod("account")}><UserOutlined />账号密码</button>
|
|
<button type="button" className={authMethod === "email" ? "is-active" : ""} onClick={() => setAuthMethod("email")}><MailOutlined />邮箱</button>
|
|
<button type="button" className={authMethod === "phone" ? "is-active" : ""} onClick={() => setAuthMethod("phone")}><MobileOutlined />手机</button>
|
|
</div>
|
|
|
|
{authMode === "register" ? (
|
|
<label className="ecommerce-auth-field">
|
|
<span><SafetyOutlined />企业邀请码 / 内测码</span>
|
|
<input value={betaCode} onChange={(event) => setBetaCode(event.target.value)} placeholder="请输入企业邀请码或内测码" />
|
|
</label>
|
|
) : null}
|
|
|
|
{authMethod === "phone" ? (
|
|
<>
|
|
<label className="ecommerce-auth-field">
|
|
<span><MobileOutlined />手机号</span>
|
|
<div className="ecommerce-auth-phone-row">
|
|
<b>+86</b>
|
|
<input value={username} onChange={(event) => setUsername(event.target.value)} autoComplete="tel" placeholder="输入手机号" />
|
|
</div>
|
|
</label>
|
|
<label className="ecommerce-auth-field">
|
|
<span><SafetyOutlined />验证码</span>
|
|
<div className="ecommerce-auth-code-row">
|
|
<input value={password} onChange={(event) => setPassword(event.target.value)} inputMode="numeric" placeholder="输入 6 位验证码" />
|
|
<button type="button" disabled>获取验证码</button>
|
|
</div>
|
|
</label>
|
|
</>
|
|
) : (
|
|
<>
|
|
<label className="ecommerce-auth-field">
|
|
<span>{authMethod === "email" ? <MailOutlined /> : <UserOutlined />}{authMode === "register" ? authMethod === "email" ? "邮箱" : "设置用户名" : authMethod === "email" ? "邮箱" : "用户名"}</span>
|
|
<input
|
|
value={username}
|
|
onChange={(event) => setUsername(event.target.value)}
|
|
autoComplete="username"
|
|
inputMode={authMethod === "email" ? "email" : "text"}
|
|
placeholder={authMethod === "email" ? "输入邮箱地址" : authMode === "register" ? "输入用户名或邮箱" : "输入用户名或邮箱"}
|
|
/>
|
|
</label>
|
|
<label className="ecommerce-auth-field">
|
|
<span><LockOutlined />{authMode === "register" ? "设置密码" : "密码"}</span>
|
|
<input
|
|
value={password}
|
|
onChange={(event) => setPassword(event.target.value)}
|
|
type="password"
|
|
autoComplete={authMode === "login" ? "current-password" : "new-password"}
|
|
placeholder={authMode === "register" ? "至少 6 位" : "输入密码"}
|
|
/>
|
|
</label>
|
|
</>
|
|
)}
|
|
|
|
{authError ? <p className="ecommerce-auth-modal__error" role="alert">{authError}</p> : null}
|
|
{authMode === "login" && authMethod !== "phone" ? <button type="button" className="ecommerce-auth-modal__forgot">忘记密码?</button> : null}
|
|
<button type="button" className="ecommerce-auth-modal__submit" onClick={handleSubmitAuth} disabled={authSubmitting}>
|
|
{authSubmitting ? <LoadingOutlined /> : null}
|
|
{authMode === "login" ? "登录" : "注册"}
|
|
</button>
|
|
<p className="ecommerce-auth-modal__agreement">{authMode === "login" ? "登录" : "注册"}即表示同意 <a href="#">《用户协议》</a> 和 <a href="#">《隐私政策》</a></p>
|
|
<div className="ecommerce-auth-modal__divider"><span>其他方式</span></div>
|
|
<button type="button" className="ecommerce-auth-modal__mobile-alt" onClick={() => setAuthMethod("phone")} aria-label="手机登录">
|
|
<MobileOutlined />
|
|
</button>
|
|
</section>
|
|
</div>
|
|
) : null}
|
|
|
|
<ToastContainer />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|