import { CameraOutlined, CheckOutlined, CheckCircleFilled, CloseOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, FileImageOutlined, FolderOpenOutlined, LockOutlined, MailOutlined, MobileOutlined, PhoneOutlined, PlayCircleOutlined, PlusOutlined, SafetyOutlined, ShareAltOutlined, UserOutlined, } from "@ant-design/icons"; import { useEffect, useRef, useState, type ChangeEvent, type FormEvent, type KeyboardEvent } from "react"; import { createPortal } from "react-dom"; 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 { ossAssets } from "../../data/ossAssets"; import type { WebAuthMode, WebGenerationPreviewTask, WebProjectSummary, WebUsageSummary, WebUserSession } from "../../types"; import type { SavedAssetItem } from "../assets/localAssetStore"; import { downloadResultAsset } from "../workbench/workbenchDownload"; 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; onOpenProject?: (project: WebProjectSummary) => void; onRemoveWork?: (task: WebGenerationPreviewTask) => void; } type AuthTab = "password" | "email" | "phone"; type ProfilePanel = "works" | "projects" | "assets" | "community"; type AccountPanel = "credits" | "tasks"; type ProfileDetailSelection = | { kind: "work"; item: WebGenerationPreviewTask } | { kind: "asset"; item: SavedAssetItem }; const PROFILE_LOCAL_STORAGE_PREFIX = "omniai-web-profile-ui"; const AUTH_LOGO_URL = ossAssets.brand.logo; const AUTH_SHOWCASE_VIDEO_URL = ossAssets.auth.showcaseVideo; 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 formatAssetType(type: SavedAssetItem["type"]): string { const labels: Record = { character: "角色", scene: "场景", prop: "道具", video: "视频", image: "图像", asset: "资产", other: "素材", }; return labels[type] || "素材"; } function ProfilePage({ session, usage, projects, tasks, pendingActionLabel, onLogin, onRegister, onAuthComplete, onSessionChange, onLogout, onOpenWorkbench, onOpenCommunity, onDeleteProject, onOpenProject, onRemoveWork, }: 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 [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("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 [detailSelection, setDetailSelection] = useState(null); const [detailNotice, setDetailNotice] = useState(null); const [isDeletingDetail, setIsDeletingDetail] = useState(false); const [isDownloadingDetail, setIsDownloadingDetail] = useState(false); 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 activePanelTitle = activePanel === "works" ? "代表作" : activePanel === "projects" ? "服务器项目" : activePanel === "assets" ? "我的资产" : "社区审核"; const activePanelDescription = activePanel === "works" ? "最近完成的高质量生成内容" : activePanel === "projects" ? "云端同步的创作项目" : activePanel === "assets" ? "可复用的图片、视频与素材" : "已提交社区的案例状态"; const activePanelCount = activePanel === "works" ? visibleWorks.length : activePanel === "projects" ? projects.length : activePanel === "assets" ? savedAssets.length : communityCases.length; 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]); 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()) { 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 ""; case "emailCode": 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; if (mode === "register") { const codeErr = validateField("emailCode", emailCode); if (codeErr) errors.emailCode = codeErr; } } 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, code: emailCode, 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 handleInteractiveCardKeyDown = (event: KeyboardEvent, action: () => void) => { if (event.target !== event.currentTarget) return; if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); action(); }; const openDetailSelection = (selection: ProfileDetailSelection) => { setDetailNotice(null); setIsDeletingDetail(false); setIsDownloadingDetail(false); setDetailSelection(selection); }; const closeDetailSelection = () => { if (isDeletingDetail || isDownloadingDetail) return; setDetailSelection(null); setDetailNotice(null); }; useEffect(() => { if (!detailSelection) return undefined; const handleKeyDown = (event: globalThis.KeyboardEvent) => { if (event.key === "Escape") closeDetailSelection(); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [detailSelection, isDeletingDetail, isDownloadingDetail]); useEffect(() => { if (!detailSelection || typeof document === "undefined") return undefined; const { body, documentElement } = document; const previousBodyOverflow = body.style.overflow; const previousRootOverscroll = documentElement.style.overscrollBehavior; body.style.overflow = "hidden"; documentElement.style.overscrollBehavior = "contain"; return () => { body.style.overflow = previousBodyOverflow; documentElement.style.overscrollBehavior = previousRootOverscroll; }; }, [detailSelection]); const handleDownloadSelectedDetail = async () => { if (!detailSelection || isDownloadingDetail) return; const isWork = detailSelection.kind === "work"; const item = detailSelection.item; const url = isWork ? item.outputUrl : item.imageUrl || item.url || ""; if (!url) { setDetailNotice("暂无可下载的媒体文件"); return; } const isVideo = isWork ? item.type === "video" : item.type === "video" || /\.(mp4|webm|mov|m4v)(\?|#|$)/i.test(url); const taskId = isWork ? item.id : item.sourceTaskId || undefined; const name = isWork ? item.title : item.name; setIsDownloadingDetail(true); setDetailNotice("正在准备下载..."); try { const status = await downloadResultAsset(url, name, isVideo, taskId); setDetailNotice(status === "saved" ? "已保存到本地" : "已开始保存到本地"); } catch (error) { if (error instanceof DOMException && error.name === "AbortError") { setDetailNotice("已取消下载"); } else { setDetailNotice(error instanceof Error ? error.message : "下载失败,请稍后重试"); } } finally { setIsDownloadingDetail(false); } }; const handleDeleteSelectedDetail = async () => { if (!detailSelection || isDeletingDetail) return; if (detailSelection.kind === "work") { onRemoveWork?.(detailSelection.item); setDetailNotice("已从当前代表作列表移除"); setDetailSelection(null); return; } setIsDeletingDetail(true); setDetailNotice(null); try { await assetClient.delete(detailSelection.item.id, { cleanupUserData: true }); setSavedAssets((current) => current.filter((asset) => asset.id !== detailSelection.item.id)); setDetailSelection(null); setAssetNotice(`已删除 ${detailSelection.item.name}`); } catch (error) { setDetailNotice(formatProfileLoadError(error, "资产删除失败")); } finally { setIsDeletingDetail(false); } }; const renderDetailMedia = (url: string | null | undefined, type: "image" | "video" | "asset") => { const mediaUrl = typeof url === "string" ? url.trim() : ""; const isVideoPreview = type === "video" || /\.(mp4|webm|mov|m4v)(\?|#|$)/i.test(mediaUrl); if (!mediaUrl) { return (
{type === "video" ? : } 暂无可预览内容
); } return isVideoPreview ? (