577 lines
23 KiB
TypeScript
577 lines
23 KiB
TypeScript
import { lazy, 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 { flushPendingGenerationRecords } from "./api/generationRecordClient";
|
||
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";
|
||
|
||
const EcommercePage = lazy(() => import("./features/ecommerce/EcommercePage"));
|
||
|
||
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;
|
||
}
|
||
|
||
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, 0);
|
||
const projectCount = 0;
|
||
const assetCount = 0;
|
||
|
||
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 local-profile-work-grid--empty">
|
||
<div className="local-profile-empty">
|
||
<strong>暂无代表作数据</strong>
|
||
<span>作品接口接入后,这里会显示你的真实生成内容。</span>
|
||
</div>
|
||
</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");
|
||
|
||
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 = actualWorkCount;
|
||
|
||
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");
|
||
};
|
||
|
||
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">
|
||
{session ? (
|
||
<div className="ecommerce-standalone__page" hidden={currentPage !== "profile"}>
|
||
<LocalProfilePage
|
||
session={session}
|
||
balance={balance}
|
||
imageCount={usage.imageUsed}
|
||
videoCount={usage.videoUsed}
|
||
onBack={handleOpenWorkspace}
|
||
onBugFeedback={handleBugFeedback}
|
||
onLogout={handleLogout}
|
||
/>
|
||
</div>
|
||
) : null}
|
||
{/* 工作台常驻挂载,仅用 hidden 切换。切到个人中心时不卸载,
|
||
生成任务、进度动画、已上传图片等本地状态全部保留,切回即继续。 */}
|
||
<div className="ecommerce-standalone__page" hidden={Boolean(session) && currentPage === "profile"}>
|
||
<ErrorBoundary>
|
||
<Suspense
|
||
fallback={
|
||
<div className="page-loading-center">
|
||
<div className="page-loading-spinner" />
|
||
<span className="page-loading-center__text">加载中...</span>
|
||
</div>
|
||
}
|
||
>
|
||
<EcommercePage
|
||
projects={[]}
|
||
isAuthenticated={Boolean(session)}
|
||
onStartCreate={() => undefined}
|
||
onOpenProject={() => undefined}
|
||
onDeleteProject={() => undefined}
|
||
onImportWorkflow={() => undefined}
|
||
onCreateTask={() => undefined}
|
||
onRequireLogin={() => openAuth("login")}
|
||
initialTemplate={null}
|
||
onInitialTemplateConsumed={() => undefined}
|
||
/>
|
||
</Suspense>
|
||
</ErrorBoundary>
|
||
</div>
|
||
</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;
|