import { CameraOutlined, CheckOutlined, CheckCircleFilled, CloseOutlined, DeleteOutlined, EditOutlined, LockOutlined, MailOutlined, MobileOutlined, PhoneOutlined, PlusOutlined, SafetyOutlined, ShareAltOutlined, UserOutlined, } from "@ant-design/icons"; import { useEffect, useRef, useState, type ChangeEvent, type FormEvent } from "react"; import { aiGenerationClient } from "../../api/aiGenerationClient"; import { assetClient } from "../../api/assetClient"; import { communityClient, type ServerCommunityCase } from "../../api/communityClient"; import { keyServerClient } from "../../api/keyServerClient"; import { isServerRequestError } from "../../api/serverConnection"; import type { WebAuthMode, WebGenerationPreviewTask, WebProjectSummary, WebUsageSummary, WebUserSession } from "../../types"; import type { SavedAssetItem } from "../assets/localAssetStore"; interface ProfilePageProps { session: WebUserSession | null; usage: WebUsageSummary; projects: WebProjectSummary[]; tasks: WebGenerationPreviewTask[]; pendingActionLabel?: string | null; onLogin: (username: string, password: string) => Promise; onRegister: (username: string, password: string, betaCode: string) => Promise; onAuthComplete?: (session: WebUserSession) => Promise; onSessionChange?: (session: WebUserSession) => void; onLogout: () => void; onOpenWorkbench: () => void; onOpenCommunity: () => void; onDeleteProject?: (project: WebProjectSummary) => void; } type AuthTab = "password" | "email" | "phone"; type ProfilePanel = "works" | "projects" | "assets" | "community"; type AccountPanel = "credits" | "tasks"; const PROFILE_LOCAL_STORAGE_PREFIX = "omniai-web-profile-ui"; const AUTH_LOGO_URL = "https://stringtest.oss-cn-hangzhou.aliyuncs.com/logo.png"; const AUTH_SHOWCASE_VIDEO_URL = "https://stringtest.oss-cn-hangzhou.aliyuncs.com/test5.mp4"; function profileStorageKey(userId: string | number | undefined, field: "avatar" | "bio" | "background"): string { return `${PROFILE_LOCAL_STORAGE_PREFIX}:${userId ?? "guest"}:${field}`; } function readLocalProfileValue(userId: string | number | undefined, field: "avatar" | "bio" | "background"): string { if (typeof window === "undefined") return ""; try { return window.localStorage.getItem(profileStorageKey(userId, field)) || ""; } catch { return ""; } } function writeLocalProfileValue(userId: string | number | undefined, field: "avatar" | "bio" | "background", value: string): void { if (typeof window === "undefined") return; try { const key = profileStorageKey(userId, field); if (value) { window.localStorage.setItem(key, value); } else { window.localStorage.removeItem(key); } } catch { // Local decoration should never block the account page. } } function readFileAsDataUrl(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || "")); reader.onerror = () => reject(reader.error || new Error("File read failed")); reader.readAsDataURL(file); }); } function formatProfileSyncError(error: unknown, fallback: string): string { const raw = error instanceof Error ? error.message : String(error || ""); const message = raw.replace(/\s+/g, " ").trim(); if (!message) return fallback; if ( /OSS PUT failed|UserDisable|AccessDenied|InvalidAccessKeyId|SignatureDoesNotMatch|<\?xml|/i.test(message) ) { return "对象存储服务拒绝上传,请检查服务器 OSS 账号或权限配置。本地预览已保留。"; } if (/403|forbidden/i.test(message)) { return "当前账号没有上传权限,请检查登录状态或服务器权限。本地预览已保留。"; } if (/413|payload too large|too large/i.test(message)) { return "文件过大,暂时无法上传。本地预览已保留。"; } return `${fallback}:${message.slice(0, 90)}`; } function formatProfileLoadError(error: unknown, fallback: string): string { const raw = error instanceof Error ? error.message : String(error || ""); const message = raw.replace(/\s+/g, " ").trim(); if (!message || /^]|]|]|^<\?xml|]/i.test(message)) { return fallback; } if (isServerRequestError(error)) { if (error.status === 401 || error.status === 403) return "登录状态或权限不足,请重新登录后再试。"; if (error.status === 404) return `${fallback},服务端接口暂未开放或路径不存在。`; if (error.status && error.status >= 500) return `${fallback},服务器暂时异常。`; } if (/not found/i.test(message)) return `${fallback},服务端接口暂未开放或路径不存在。`; if (/forbidden|unauthorized/i.test(message)) return "登录状态或权限不足,请重新登录后再试。"; return `${fallback}:${message.slice(0, 90)}`; } function mapAssetToSavedItem(asset: Awaited>[number]): SavedAssetItem { return { ...asset, project: asset.sourceProjectId || "服务器资产库", version: "Server", ratio: String(asset.metadata?.ratio || "1:1"), tags: asset.tags?.length ? asset.tags : ["服务器素材"], thumbClass: asset.imageUrl ? "is-uploaded" : "is-station", }; } function formatProfileDate(value: string | null | undefined): string { if (!value) return "刚刚"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return new Intl.DateTimeFormat("zh-CN", { month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit", }).format(date); } function formatTaskType(type: WebGenerationPreviewTask["type"]): string { const labels: Record = { image: "图像", video: "视频", agent: "智能体", "digital-human": "数字人", "character-mix": "角色融合", }; return labels[type] || type; } function formatTaskStatus(status: WebGenerationPreviewTask["status"]): string { const labels: Record = { queued: "排队中", running: "生成中", completed: "已完成", failed: "失败", }; return labels[status] || status; } function formatAssetStatus(status: string | undefined): string { const normalized = String(status || "").toLowerCase(); if (normalized === "completed" || normalized === "ready" || normalized === "success") return "可用"; if (normalized === "running" || normalized === "processing") return "处理中"; if (normalized === "failed" || normalized === "error") return "失败"; return status || "资产"; } function ProfilePage({ session, usage, projects, tasks, pendingActionLabel, onLogin, onRegister, onAuthComplete, onSessionChange, onLogout, onOpenWorkbench, onOpenCommunity, onDeleteProject, }: ProfilePageProps) { const isLoggedIn = Boolean(session); const userId = session?.user.id; const displayName = session?.user.displayName || session?.user.username || "访客账号"; const avatarLabel = displayName.trim().slice(0, 1).toUpperCase() || "访"; const avatarInputRef = useRef(null); const bannerInputRef = useRef(null); const [mode, setMode] = useState("login"); const [authTab, setAuthTab] = useState("password"); const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [betaCode, setBetaCode] = useState(""); const [phone, setPhone] = useState(""); const [smsCode, setSmsCode] = useState(""); const [notice, setNotice] = useState(null); const [fieldErrors, setFieldErrors] = useState>({}); const [isSubmitting, setIsSubmitting] = useState(false); const [smsCooldown, setSmsCooldown] = useState(0); const [isSendingSms, setIsSendingSms] = useState(false); const [activePanel, setActivePanel] = useState("works"); const [accountPanel, setAccountPanel] = useState("credits"); const [savedAssets, setSavedAssets] = useState([]); const [assetNotice, setAssetNotice] = useState(null); const [communityCases, setCommunityCases] = useState([]); const [communityNotice, setCommunityNotice] = useState(null); const [profileNotice, setProfileNotice] = useState(null); const [localAvatarUrl, setLocalAvatarUrl] = useState(() => session?.user.avatarUrl || readLocalProfileValue(userId, "avatar")); const [profileBio, setProfileBio] = useState(() => session?.user.bio || readLocalProfileValue(userId, "bio")); const [isBioEditing, setIsBioEditing] = useState(false); const [bioEditBackup, setBioEditBackup] = useState(""); const [bioStatusNotice, setBioStatusNotice] = useState(null); const [bannerUrl, setBannerUrl] = useState(() => session?.user.backgroundUrl || readLocalProfileValue(userId, "background")); const completedTasks = tasks.filter((task) => task.status === "completed"); const visibleWorks = completedTasks.length ? completedTasks : tasks.slice(0, 6); const totalBalance = usage.balanceCents + (session?.user.enterpriseBalanceCents || 0); const packageLabel = session?.user.activePackages?.[0]?.name || "按量积分"; const avatarUrl = session?.user.avatarUrl || localAvatarUrl || null; const displayedBio = profileBio.trim() || "这个人还没有填写个性签名"; const emailLooksValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim()); const phoneLooksValid = /^1[3-9]\d{9}$/.test(phone.trim()); const passwordLooksReady = password.length >= (mode === "register" ? 6 : 1); useEffect(() => { setLocalAvatarUrl(session?.user.avatarUrl || readLocalProfileValue(userId, "avatar")); setProfileBio(session?.user.bio || readLocalProfileValue(userId, "bio")); setBannerUrl(session?.user.backgroundUrl || readLocalProfileValue(userId, "background")); }, [session?.user.avatarUrl, session?.user.backgroundUrl, session?.user.bio, userId]); useEffect(() => { if (!session) return; setCommunityNotice(null); setAssetNotice(null); let cancelled = false; void Promise.allSettled([assetClient.list(), communityClient.listMyCases()]).then((results) => { if (cancelled) return; const [assetResult, communityResult] = results; if (assetResult.status === "fulfilled") { setSavedAssets(assetResult.value.map(mapAssetToSavedItem)); } else { setSavedAssets([]); setAssetNotice(formatProfileLoadError(assetResult.reason, "资产记录暂时不可用")); } if (communityResult.status === "fulfilled") { setCommunityCases(communityResult.value); } else { setCommunityCases([]); setCommunityNotice(formatProfileLoadError(communityResult.reason, "社区记录暂时不可用")); } }); return () => { cancelled = true; }; }, [session]); useEffect(() => { if (!session || activePanel !== "assets") return; setAssetNotice(null); assetClient .list() .then((items) => setSavedAssets(items.map(mapAssetToSavedItem))) .catch((error) => { setSavedAssets([]); setAssetNotice(formatProfileLoadError(error, "资产记录暂时不可用")); }); }, [activePanel, session]); useEffect(() => { if (smsCooldown <= 0) return; const timer = window.setInterval(() => { setSmsCooldown((current) => Math.max(0, current - 1)); }, 1000); return () => window.clearInterval(timer); }, [smsCooldown]); const handleSendSms = async () => { if (smsCooldown > 0 || !phone.trim() || isSendingSms) return; if (mode === "register" && !betaCode.trim()) { setNotice("请输入企业邀请码 / 内测码后再获取验证码"); return; } setIsSendingSms(true); setNotice(null); try { const result = await keyServerClient.sendSmsCode(phone, mode === "login" ? "login" : "register", betaCode); setSmsCooldown(result.cooldownSeconds || 60); setNotice("验证码已发送,请查收短信"); } catch (error) { setNotice(error instanceof Error ? error.message : "验证码发送失败"); } finally { setIsSendingSms(false); } }; const validateField = (field: string, value: string): string => { switch (field) { case "username": if (!value.trim()) return "请输入用户名"; if (mode === "register" && value.trim().length < 2) return "用户名至少 2 个字符"; return ""; case "password": if (!value) return "请输入密码"; if (mode === "register" && value.length < 6) return "密码至少 6 位"; return ""; case "email": if (!value.trim()) return "请输入邮箱"; if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return "邮箱格式不正确"; return ""; case "phone": if (!value.trim()) return "请输入手机号"; if (!/^1[3-9]\d{9}$/.test(value)) return "手机号格式不正确"; return ""; case "betaCode": if (!value.trim()) return "请输入邀请码"; return ""; case "smsCode": if (!value.trim()) return "请输入验证码"; if (value.length !== 6) return "验证码为 6 位数字"; return ""; default: return ""; } }; const handleFieldBlur = (field: string, value: string) => { const error = validateField(field, value); setFieldErrors((prev) => ({ ...prev, [field]: error })); }; const clearFieldError = (field: string) => { if (fieldErrors[field]) { setFieldErrors((prev) => ({ ...prev, [field]: "" })); } }; const handleSubmit = async (event: FormEvent) => { event.preventDefault(); if (isSubmitting) return; const errors: Record = {}; if (mode === "register") { const betaErr = validateField("betaCode", betaCode); if (betaErr) errors.betaCode = betaErr; } if (authTab === "phone") { const phoneErr = validateField("phone", phone); if (phoneErr) errors.phone = phoneErr; const smsErr = validateField("smsCode", smsCode); if (smsErr) errors.smsCode = smsErr; if (mode === "register") { const pwErr = validateField("password", password); if (pwErr) errors.password = pwErr; } } else if (authTab === "email") { const emailErr = validateField("email", email); if (emailErr) errors.email = emailErr; const pwErr = validateField("password", password); if (pwErr) errors.password = pwErr; } else { const userErr = validateField("username", username); if (userErr) errors.username = userErr; const pwErr = validateField("password", password); if (pwErr) errors.password = pwErr; } if (Object.values(errors).some(Boolean)) { setFieldErrors(errors); return; } setIsSubmitting(true); setNotice(null); try { if (authTab === "phone") { const nextSession = mode === "login" ? await keyServerClient.loginPhone({ phone, code: smsCode }) : await keyServerClient.registerPhone({ phone, code: smsCode, password, betaCode }); await onAuthComplete?.(nextSession); } else if (authTab === "email") { const nextSession = mode === "login" ? await keyServerClient.loginEmail({ email, password }) : await keyServerClient.registerEmail({ email, password, username: username.trim() || undefined, betaCode }); await onAuthComplete?.(nextSession); } else if (mode === "login") { await onLogin(username.trim(), password); } else { await onRegister(username.trim(), password, betaCode); } } catch (error) { setNotice(error instanceof Error ? error.message : "操作失败,请稍后重试"); } finally { setIsSubmitting(false); } }; const updateProfileUser = (patch: Partial) => { if (!session) return; const nextUser = { ...session.user, ...patch }; const nextSession = keyServerClient.updateStoredSessionUser(nextUser) || { ...session, user: nextUser }; onSessionChange?.(nextSession); }; const syncProfilePatch = async (patch: Parameters[0]) => { if (!session) return; try { const user = await keyServerClient.updateProfile(patch); updateProfileUser(user); setProfileNotice(null); } catch (error) { setProfileNotice(formatProfileSyncError(error, "个人资料同步失败")); } }; const handleAvatarUpload = async (event: ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file || !file.type.startsWith("image/")) return; const dataUrl = await readFileAsDataUrl(file); try { const uploaded = await aiGenerationClient.uploadAsset({ dataUrl, name: file.name, mimeType: file.type, scope: "profile-avatar", }); const nextUrl = uploaded.url || dataUrl; setLocalAvatarUrl(nextUrl); writeLocalProfileValue(userId, "avatar", nextUrl); updateProfileUser({ avatarUrl: nextUrl }); await syncProfilePatch({ avatarOssKey: uploaded.ossKey || null, avatarUrl: uploaded.url || nextUrl }); } catch (error) { setLocalAvatarUrl(dataUrl); writeLocalProfileValue(userId, "avatar", dataUrl); updateProfileUser({ avatarUrl: dataUrl }); setProfileNotice(formatProfileSyncError(error, "头像上传失败,已先在本地预览")); } }; const handleBannerUpload = async (event: ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file || !file.type.startsWith("image/")) return; const dataUrl = await readFileAsDataUrl(file); try { const uploaded = await aiGenerationClient.uploadAsset({ dataUrl, name: file.name, mimeType: file.type, scope: "profile-background", }); const nextUrl = uploaded.url || dataUrl; setBannerUrl(nextUrl); writeLocalProfileValue(userId, "background", nextUrl); updateProfileUser({ backgroundUrl: nextUrl }); await syncProfilePatch({ backgroundUrl: nextUrl, profileBackgroundUrl: nextUrl }); } catch (error) { setBannerUrl(dataUrl); writeLocalProfileValue(userId, "background", dataUrl); updateProfileUser({ backgroundUrl: dataUrl }); setProfileNotice(formatProfileSyncError(error, "背景上传失败,已先在本地预览")); } }; const handleBioBlur = () => { const nextBio = profileBio.trim(); writeLocalProfileValue(userId, "bio", nextBio); updateProfileUser({ bio: nextBio || null }); void syncProfilePatch({ bio: nextBio || null }); }; const startBioEdit = () => { setBioEditBackup(profileBio); setBioStatusNotice(null); setIsBioEditing(true); }; const confirmBioEdit = () => { handleBioBlur(); setIsBioEditing(false); setBioStatusNotice("个性签名已保存"); }; const cancelBioEdit = () => { setProfileBio(bioEditBackup); setIsBioEditing(false); setBioStatusNotice(null); }; const renderEmptyState = (text: string, actionLabel: string, action: () => void) => (

{text}

); const renderActivePanel = () => { if (activePanel === "works") { return visibleWorks.length ? (
{visibleWorks.map((task) => (
{task.title} {formatTaskType(task.type)}

{task.prompt}

{formatTaskStatus(task.status)} {formatProfileDate(task.createdAt)}
))}
) : ( renderEmptyState("向全世界展示你最得意的创作。", "开始创作", onOpenWorkbench) ); } if (activePanel === "projects") { return projects.length ? (
{projects.map((project) => (
{project.name} {formatProfileDate(project.updatedAt)} {onDeleteProject ? ( ) : null}

{project.description || "最近更新的项目"}

{project.storyboardCount} 节点 {project.imageCount} 图 / {project.videoCount} 视频
))}
) : ( renderEmptyState("还没有同步到服务器的项目。", "进入工作台", onOpenWorkbench) ); } if (activePanel === "assets") { return savedAssets.length ? (
{savedAssets.map((asset) => (
{asset.name} {formatAssetStatus(asset.status)}

{asset.description}

{asset.type} {formatProfileDate(asset.updatedAt)}
))}
) : ( renderEmptyState(assetNotice || "服务器资产库暂无内容。", "去工作台生成", onOpenWorkbench) ); } return communityCases.length ? (
{communityCases.map((item) => (
{item.coverUrl ? : }
{item.title} {item.status === "approved" ? "已通过" : item.status === "rejected" ? "未通过" : "审核中"} {item.copyCount} 次复用
))}
) : ( renderEmptyState(communityNotice || "还没有发布到社区的内容。", "去社区发布", onOpenCommunity) ); }; if (isLoggedIn) { return (
void handleAvatarUpload(event)} /> void handleBannerUpload(event)} />