"use client"; import { Clock3, Loader2, LogOut, Sparkles, Upload } from "lucide-react"; import { signOut, useSession } from "next-auth/react"; import { type CSSProperties, type KeyboardEvent as ReactKeyboardEvent, type PointerEvent as ReactPointerEvent, useCallback, useEffect, useMemo, useRef, useState, } from "react"; import useSWR from "swr"; import { toast } from "sonner"; import { BrandTopBar } from "@/components/layout/brand-top-bar"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { imageGenerationService } from "@/services/canvas"; import { cn, fetcher } from "@/lib/utils"; import type { PublicStyleProCategory, PublicStyleProConfig, StyleProGenerationMode, StyleProHistoryItem, StyleProPersistedState, StyleProSelectionMap, } from "@/lib/style-pro/config"; type BrandingSettings = { siteName?: string; siteLogo?: string | null; siteLogoDark?: string | null; }; type StyleProConfigResponse = { success: boolean; config?: PublicStyleProConfig; error?: string; }; type StyleProModelOption = { id: string; modelId: string; displayName: string; avatarUrl?: string | null; providerName: string; providerType: string; isDefault: boolean; unitCost: number; creditDiscountLabel?: string | null; }; type StyleProModelsResponse = { success: boolean; defaultModelId?: string | null; realisticUnitCost?: number | null; models?: StyleProModelOption[]; error?: string; }; type StyleProMembershipSummaryResponse = { currentPlan?: { name?: string; monthlyCredits?: number; dailyFreeCredits?: number; }; summary?: { currentCredits?: number; dailyFreeCredits?: number; }; }; type StyleProAccountCenterResponse = { profile?: { name?: string; email?: string; currentPlanName?: string; availablePoints?: number; monthlyCredits?: number; dailyFreeCredits?: number; }; }; type StyleProGenerateResponse = { success: boolean; error?: string; taskId?: string; status?: string; queueStatus?: { status?: "pending" | "running" | "succeeded" | "failed"; queuePosition?: number; sameModelPendingCount?: number; sameModelRunningCount?: number; concurrencyLimit?: number; estimatedWaitMs?: number; source?: "model_queue" | "batch_queue"; }; task?: { id: string; status: "pending" | "running" | "succeeded" | "failed"; attempts: number; error?: string | null; createdAt?: string | Date | null; startedAt?: string | Date | null; completedAt?: string | Date | null; queueStatus?: StyleProGenerateResponse["queueStatus"]; result?: { imageUrl: string; thumbnailUrl?: string; model?: string; modelRef?: string; generationMode?: StyleProGenerationMode; chargedPoints?: number; selections: StyleProSelectionMap; optionLabels: string[]; } | null; }; }; type StyleProWorkspaceProps = { canvasId: string; initialSourceImageUrl?: string | null; persistedState?: StyleProPersistedState | null; onAddToCanvas?: (item: StyleProHistoryItem) => void | Promise; onBack?: () => void; onPersistState?: (state: StyleProPersistedState) => void | Promise; }; const STYLE_PRO_HISTORY_LIMIT = 4; const STYLE_PRO_TASK_POLL_INTERVAL_MS = 1600; const STYLE_PRO_TASK_TIMEOUT_MS = 4 * 60 * 1000; type StyleProTaskState = { taskId: string; status: "pending" | "running" | "succeeded" | "failed"; queueStatus?: StyleProGenerateResponse["queueStatus"]; error?: string | null; generationMode: StyleProGenerationMode; }; function getHistoryId() { if (typeof crypto !== "undefined" && "randomUUID" in crypto) { return crypto.randomUUID(); } return `style-pro-${Date.now()}-${Math.random().toString(36).slice(2)}`; } function createStyleProRequestId() { if (typeof crypto !== "undefined" && "randomUUID" in crypto) { return crypto.randomUUID(); } return undefined; } function loadImageDimensions(url: string) { return new Promise<{ width?: number; height?: number }>((resolve) => { if (typeof window === "undefined" || !url) { resolve({}); return; } const image = new window.Image(); image.onload = () => { resolve({ width: image.naturalWidth || undefined, height: image.naturalHeight || undefined, }); }; image.onerror = () => resolve({}); image.src = url; }); } function sleep(ms: number) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } function normalizeHistory(history: StyleProHistoryItem[] | undefined) { return Array.isArray(history) ? history.slice(0, STYLE_PRO_HISTORY_LIMIT) : []; } function getLocalStyleProStateKey(canvasId: string) { return `style-pro-state:${canvasId || "draft"}`; } function readLocalStyleProState( canvasId: string ): StyleProPersistedState | null { if (typeof window === "undefined") return null; try { const raw = window.localStorage.getItem(getLocalStyleProStateKey(canvasId)); if (!raw) return null; const parsed = JSON.parse(raw) as StyleProPersistedState; return { sourceImageUrl: typeof parsed.sourceImageUrl === "string" ? parsed.sourceImageUrl : undefined, history: normalizeHistory( Array.isArray(parsed.history) ? parsed.history.filter( (item) => item && typeof item.sourceImageUrl === "string" && typeof item.resultImageUrl === "string" ) : [] ), }; } catch { return null; } } function writeLocalStyleProState( canvasId: string, state: StyleProPersistedState ) { if (typeof window === "undefined") return; try { window.localStorage.setItem( getLocalStyleProStateKey(canvasId), JSON.stringify({ sourceImageUrl: state.sourceImageUrl, history: normalizeHistory(state.history), }) ); } catch { // Local persistence is a safety net; server persistence remains authoritative. } } function mergeStyleProHistory( ...historyGroups: Array ) { const byKey = new Map(); for (const history of historyGroups) { for (const item of history || []) { if (!item?.sourceImageUrl || !item?.resultImageUrl) continue; const key = item.id || `${item.sourceImageUrl}::${item.resultImageUrl}`; if (!byKey.has(key)) { byKey.set(key, item); } } } return Array.from(byKey.values()) .sort((left, right) => { const leftTime = Date.parse(left.createdAt || "") || 0; const rightTime = Date.parse(right.createdAt || "") || 0; return rightTime - leftTime; }) .slice(0, STYLE_PRO_HISTORY_LIMIT); } function mergeStyleProState( ...states: Array ): StyleProPersistedState { return { sourceImageUrl: states.find((state) => String(state?.sourceImageUrl || "").trim()) ?.sourceImageUrl || undefined, history: mergeStyleProHistory(...states.map((state) => state?.history)), }; } function getStyleProUserInitials(name?: string | null, email?: string | null) { const source = String(name || email || "mu").trim(); return (source || "mu").slice(0, 2).toLowerCase(); } function formatStyleProCredits(value?: number | null) { const numeric = Number(value ?? 0); if (!Number.isFinite(numeric)) return "0"; return Math.max(0, Math.round(numeric)).toLocaleString("zh-CN"); } function formatStyleProHistoryDateTime(value?: string | null) { const date = new Date(value || ""); if (Number.isNaN(date.getTime())) return "时间未知"; const dateText = date .toLocaleDateString("zh-CN", { day: "2-digit", month: "2-digit", year: "numeric", }) .replace(/\//g, "-"); const timeText = date.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", }); return `${dateText} ${timeText}`; } function formatStyleProEstimatedWait(estimatedWaitMs?: number | null) { const numeric = Number(estimatedWaitMs || 0); if (!Number.isFinite(numeric) || numeric <= 0) { return "预计即将开始"; } const seconds = Math.max(1, Math.round(numeric / 1000)); if (seconds < 60) { return `预计 ${seconds} 秒`; } const minutes = Math.floor(seconds / 60); const remainSeconds = seconds % 60; return remainSeconds > 0 ? `预计 ${minutes} 分 ${remainSeconds} 秒` : `预计 ${minutes} 分钟`; } function StyleProAccountActions() { const { data: session, status } = useSession(); const [accountCenterOpen, setAccountCenterOpen] = useState(false); const isAuthenticated = status === "authenticated" && Boolean(session?.user); const { data: membershipData } = useSWR( isAuthenticated ? "/api/user/membership-summary" : null, fetcher ); const { data: accountCenterData, isLoading: accountCenterLoading } = useSWR( isAuthenticated && accountCenterOpen ? "/api/user/account-center" : null, fetcher ); const accountProfile = useMemo(() => { const profile = accountCenterData?.profile; return { name: profile?.name || session?.user?.name || "用户", email: profile?.email || session?.user?.email || "", planName: profile?.currentPlanName || membershipData?.currentPlan?.name || "Free", availablePoints: profile?.availablePoints ?? membershipData?.summary?.currentCredits ?? 0, monthlyCredits: profile?.monthlyCredits ?? membershipData?.currentPlan?.monthlyCredits ?? 0, dailyFreeCredits: profile?.dailyFreeCredits ?? membershipData?.summary?.dailyFreeCredits ?? membershipData?.currentPlan?.dailyFreeCredits ?? 0, }; }, [ accountCenterData?.profile, membershipData?.currentPlan?.dailyFreeCredits, membershipData?.currentPlan?.monthlyCredits, membershipData?.currentPlan?.name, membershipData?.summary?.currentCredits, membershipData?.summary?.dailyFreeCredits, session?.user?.email, session?.user?.name, ]); const userInitials = getStyleProUserInitials( session?.user?.name, session?.user?.email ); const openLogin = useCallback(() => { if (typeof window === "undefined") { return; } const callbackUrl = `${window.location.pathname}${window.location.search}`; window.location.href = `/login?callbackUrl=${encodeURIComponent(callbackUrl)}`; }, []); const handleLogout = useCallback(async () => { try { await fetch("/api/auth/logout", { method: "POST" }); await signOut({ callbackUrl: "/login", redirect: false }); } finally { window.location.replace("/login"); } }, []); if (status === "loading") { return (
); } if (!isAuthenticated) { return ( ); } return ( <> 个人中心
{userInitials}
{accountProfile.name}
{accountProfile.email || "登录后查看账户邮箱"}
积分余额
{accountCenterLoading ? ( ) : ( formatStyleProCredits(accountProfile.availablePoints) )}
当前套餐
{accountProfile.planName}
每月更新
{formatStyleProCredits(accountProfile.monthlyCredits)}
每日赠送
{formatStyleProCredits(accountProfile.dailyFreeCredits)}
); } function ImagePreview({ alt, className, imageUrl, }: { alt: string; className?: string; imageUrl?: string | null; }) { if (!imageUrl) { return (
等待图片
); } return ( {alt} ); } function GeneratingImagePlaceholder({ taskState, }: { taskState?: StyleProTaskState | null; }) { const queueStatus = taskState?.queueStatus; const queuePosition = Math.max(0, Number(queueStatus?.queuePosition || 0)); const sameModelRunningCount = Math.max( 0, Number(queueStatus?.sameModelRunningCount || 0) ); const concurrencyLimit = Math.max( 0, Number(queueStatus?.concurrencyLimit || 0) ); const isQueued = taskState?.status === "pending"; const isFailed = taskState?.status === "failed"; const title = isFailed ? "本次生成失败" : isQueued ? `排队中,前方 ${queuePosition} 个` : taskState?.generationMode === "realistic" ? "正在生成实景化结果" : "正在生成图片"; const description = isFailed ? taskState?.error || "请调整参数后重试。" : isQueued ? `当前模型运行 ${sameModelRunningCount}/${concurrencyLimit || "?"},${formatStyleProEstimatedWait( queueStatus?.estimatedWaitMs )}` : taskState?.generationMode === "realistic" ? "系统会先完成基础实景化,再合成最终风格结果。" : "模型已开始执行,结果完成后会自动回填。"; return (
{title}
{description}
{!isFailed ? : null} {isFailed ? "失败" : isQueued ? "排队中" : "生成中"}
); } function BeforeAfterSlider({ afterUrl, beforeUrl, taskState, }: { afterUrl?: string | null; beforeUrl?: string | null; taskState?: StyleProTaskState | null; }) { const showTaskState = taskState?.status === "pending" || taskState?.status === "running"; const [position, setPosition] = useState(50); const sliderRef = useRef(null); const updatePositionFromClientX = useCallback((clientX: number) => { const rect = sliderRef.current?.getBoundingClientRect(); if (!rect || rect.width <= 0) return; const next = ((clientX - rect.left) / rect.width) * 100; setPosition(Math.min(96, Math.max(4, Math.round(next * 10) / 10))); }, []); const handlePointerDown = useCallback( (event: ReactPointerEvent) => { event.currentTarget.setPointerCapture(event.pointerId); updatePositionFromClientX(event.clientX); }, [updatePositionFromClientX] ); const handlePointerMove = useCallback( (event: ReactPointerEvent) => { if (!event.currentTarget.hasPointerCapture(event.pointerId)) return; updatePositionFromClientX(event.clientX); }, [updatePositionFromClientX] ); const handlePointerEnd = useCallback( (event: ReactPointerEvent) => { if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } }, [] ); const handleSliderKeyDown = useCallback( (event: ReactKeyboardEvent) => { const step = event.shiftKey ? 10 : 3; if (event.key === "ArrowLeft" || event.key === "ArrowDown") { event.preventDefault(); setPosition((current) => Math.max(4, current - step)); } else if (event.key === "ArrowRight" || event.key === "ArrowUp") { event.preventDefault(); setPosition((current) => Math.min(96, current + step)); } else if (event.key === "Home") { event.preventDefault(); setPosition(4); } else if (event.key === "End") { event.preventDefault(); setPosition(96); } }, [] ); if (showTaskState || taskState?.status === "failed") { return ; } if (!beforeUrl && !afterUrl) { return (
上传图片后开始生成
); } if (!afterUrl) { return (
); } return (
对比
原图
结果
); } export function StyleProWorkspace({ canvasId, initialSourceImageUrl, onAddToCanvas, onPersistState, persistedState, }: StyleProWorkspaceProps) { const fileInputRef = useRef(null); const activeGenerateRequestRef = useRef(null); const { data: brandingData } = useSWR( "/api/site-settings?scope=branding", fetcher ); const { data: configData, isLoading: configLoading } = useSWR("/api/style-pro/config", fetcher); const { data: modelsData, isLoading: modelsLoading } = useSWR("/api/style-pro/model-options", fetcher); const config = configData?.config; const modelOptions = useMemo( () => (modelsData?.models || []).filter((model) => Boolean(model.id)), [modelsData?.models] ); const enabledCategories = useMemo( () => (config?.categories || []) .filter((category) => category.enabled) .map((category) => ({ ...category, options: category.options.filter((option) => option.enabled), })) .filter((category) => category.options.length > 0), [config?.categories] ); const [sourceImageUrl, setSourceImageUrl] = useState( () => mergeStyleProState( persistedState, readLocalStyleProState(canvasId), initialSourceImageUrl ? { sourceImageUrl: initialSourceImageUrl, history: [] } : null ).sourceImageUrl || "" ); const [currentResult, setCurrentResult] = useState(() => { const merged = mergeStyleProState( persistedState, readLocalStyleProState(canvasId) ); return merged.history[0] || null; }); const [history, setHistory] = useState( () => mergeStyleProState(persistedState, readLocalStyleProState(canvasId)) .history ); const [selections, setSelections] = useState({}); const [generationMode, setGenerationMode] = useState("style"); const [selectedModelId, setSelectedModelId] = useState(""); const [uploading, setUploading] = useState(false); const [generationTaskState, setGenerationTaskState] = useState(null); const generating = generationTaskState?.status === "pending" || generationTaskState?.status === "running"; useEffect(() => { if (modelOptions.length === 0) { setSelectedModelId(""); return; } setSelectedModelId((current) => { if (current && modelOptions.some((model) => model.id === current)) { return current; } return modelsData?.defaultModelId || modelOptions[0]?.id || ""; }); }, [modelOptions, modelsData?.defaultModelId]); useEffect(() => { const localState = readLocalStyleProState(canvasId); const nextSource = persistedState?.sourceImageUrl || localState?.sourceImageUrl || initialSourceImageUrl || ""; setSourceImageUrl((current) => current || nextSource); setHistory((currentHistory) => mergeStyleProHistory( currentHistory, persistedState?.history, localState?.history ) ); setCurrentResult( (current) => current || mergeStyleProHistory(persistedState?.history, localState?.history)[0] || null ); }, [ canvasId, initialSourceImageUrl, persistedState?.history, persistedState?.sourceImageUrl, ]); const siteName = brandingData?.siteName || "由壹点AI"; const logo = brandingData?.siteLogoDark || brandingData?.siteLogo || ""; const selectedOptionLabels = useMemo(() => { const labels: string[] = []; for (const category of enabledCategories) { const selected = selections[category.id] || []; for (const optionId of selected) { const option = category.options.find((item) => item.id === optionId); if (option) { labels.push(`${category.title}:${option.label}`); } } } return labels; }, [enabledCategories, selections]); const selectedModel = useMemo( () => modelOptions.find((model) => model.id === selectedModelId) || modelOptions[0] || null, [modelOptions, selectedModelId] ); const selectedModelCostLabel = selectedModel ? selectedModel.unitCost > 0 ? `${selectedModel.unitCost} 积分/张` : "未设置积分" : modelsLoading ? "加载价格" : "暂无模型"; const realisticUnitCost = Math.max(0, Number(modelsData?.realisticUnitCost || 0)); const currentCostLabel = generationMode === "realistic" ? realisticUnitCost > 0 ? `${realisticUnitCost} 积分/张` : "后台模型未计费" : selectedModelCostLabel; const currentUnitCost = generationMode === "realistic" ? realisticUnitCost : selectedModel?.unitCost || 0; const missingRequiredCategories = useMemo( () => enabledCategories.filter((category) => { if (!category.required && category.minSelections <= 0) return false; const selectedCount = selections[category.id]?.length || 0; return ( selectedCount < Math.max(category.required ? 1 : 0, category.minSelections) ); }), [enabledCategories, selections] ); const persistState = useCallback( async (nextState: StyleProPersistedState) => { const normalizedState = { sourceImageUrl: nextState.sourceImageUrl, history: normalizeHistory(nextState.history), }; writeLocalStyleProState(canvasId, normalizedState); await onPersistState?.(normalizedState); }, [canvasId, onPersistState] ); const updateSourceImage = useCallback( async (url: string) => { const nextUrl = String(url || "").trim(); setSourceImageUrl(nextUrl); setCurrentResult(null); setGenerationTaskState(null); await persistState({ sourceImageUrl: nextUrl, history }); }, [history, persistState] ); const handleFile = useCallback( async (file: File | null | undefined) => { if (!file) return; if (!file.type.startsWith("image/")) { toast.error("请上传图片文件"); return; } setUploading(true); try { const result = await imageGenerationService.uploadImage(file); if (!result.success || !result.data?.url) { throw new Error(result.error || "上传失败"); } await updateSourceImage(result.data.url); toast.success("图片已上传"); } catch (error) { toast.error(error instanceof Error ? error.message : "上传失败"); } finally { setUploading(false); } }, [updateSourceImage] ); const toggleOption = useCallback( (category: PublicStyleProCategory, optionId: string) => { setSelections((current) => { const previous = current[category.id] || []; const selected = previous.includes(optionId); let nextSelected = selected ? previous.filter((id) => id !== optionId) : [...previous, optionId]; if (!selected && category.maxSelections <= 1) { nextSelected = [optionId]; } if (nextSelected.length > category.maxSelections) { nextSelected = nextSelected.slice(-category.maxSelections); } return { ...current, [category.id]: nextSelected, }; }); }, [] ); const handleGenerate = useCallback(async () => { if (activeGenerateRequestRef.current || generating) { toast.info("当前图片正在生成中,请稍候"); return; } if (!sourceImageUrl) { toast.error("请先上传一张图片"); return; } if (!config?.enabled) { toast.error("风格化 PRO 暂未启用"); return; } if (missingRequiredCategories.length > 0) { toast.error(`请完成必选项:${missingRequiredCategories[0].title}`); return; } const requestId = createStyleProRequestId(); const localRequestKey = requestId || `local-${Date.now()}`; activeGenerateRequestRef.current = localRequestKey; setGenerationTaskState({ taskId: localRequestKey, status: "pending", error: null, generationMode, }); try { const response = await fetch("/api/style-pro/generate", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ canvasId, ...(requestId ? { requestId } : {}), generationMode, modelId: generationMode === "style" ? selectedModel?.id || selectedModelId || undefined : undefined, sourceImageUrl, selections, }), }); const payload = (await response.json().catch(() => ({}))) as | StyleProGenerateResponse | undefined; if (!response.ok || !payload?.success) { throw new Error(payload?.error || "生成失败"); } const taskId = String(payload.taskId || "").trim(); if (!taskId) { throw new Error("生成任务创建失败"); } setGenerationTaskState({ taskId, status: payload.status === "running" ? "running" : "pending", queueStatus: payload.queueStatus, error: null, generationMode, }); const deadline = Date.now() + STYLE_PRO_TASK_TIMEOUT_MS; let taskResult: | NonNullable["result"]> | null = null; let lastError = ""; while (Date.now() < deadline) { await sleep(STYLE_PRO_TASK_POLL_INTERVAL_MS); const statusResponse = await fetch( `/api/style-pro/generate?taskId=${encodeURIComponent(taskId)}`, { cache: "no-store" } ); const statusPayload = (await statusResponse.json().catch(() => ({}))) as | StyleProGenerateResponse | undefined; if (!statusResponse.ok || !statusPayload?.success || !statusPayload.task) { lastError = statusPayload?.error || `查询任务失败(${statusResponse.status})`; continue; } const task = statusPayload.task; setGenerationTaskState({ taskId: task.id, status: task.status, queueStatus: task.queueStatus, error: task.error || null, generationMode, }); if (task.status === "succeeded") { taskResult = task.result || null; break; } if (task.status === "failed") { throw new Error(task.error || "生成失败"); } } if (!taskResult?.imageUrl) { throw new Error(lastError || "生成超时,请稍后重试"); } const dimensions = await loadImageDimensions(taskResult.imageUrl); const item: StyleProHistoryItem = { id: getHistoryId(), sourceImageUrl, resultImageUrl: taskResult.imageUrl, selections: taskResult.selections || selections, optionLabels: taskResult.optionLabels || selectedOptionLabels, createdAt: new Date().toISOString(), generationMode: taskResult.generationMode || generationMode, model: taskResult.model, modelRef: taskResult.modelRef, width: dimensions.width, height: dimensions.height, }; const nextHistory = [ item, ...history.filter((entry) => entry.id !== item.id), ].slice(0, 4); setHistory(nextHistory); setCurrentResult(item); await persistState({ sourceImageUrl, history: nextHistory }); setGenerationTaskState(null); toast.success("风格化 PRO 生成完成"); } catch (error) { setGenerationTaskState((current) => current ? { ...current, status: "failed", error: error instanceof Error ? error.message : "生成失败", } : null ); toast.error(error instanceof Error ? error.message : "生成失败"); } finally { activeGenerateRequestRef.current = null; } }, [ canvasId, config?.enabled, generationMode, generating, history, missingRequiredCategories, persistState, selectedOptionLabels, selectedModel?.id, selectedModelId, selections, sourceImageUrl, ]); const restoreHistoryItem = useCallback((item: StyleProHistoryItem) => { setSourceImageUrl(item.sourceImageUrl); setSelections(item.selections || {}); setGenerationMode(item.generationMode || "style"); setCurrentResult(item); setGenerationTaskState(null); }, []); const disabledReason = !sourceImageUrl ? "请先上传图片" : generationMode === "realistic" && config?.realisticModeAvailable === false ? "请先配置实景化模型" : generationMode === "style" && modelsData?.success === true && modelOptions.length === 0 ? "暂无可用生图模型" : missingRequiredCategories.length > 0 ? `还差:${missingRequiredCategories[0].title}` : ""; const promptDisplayMode = config?.promptDisplayMode || "stacked"; const themeStyle = { "--style-pro-title-color": config?.titleTextColor || "#2563eb", "--style-pro-button-bg": config?.buttonBackgroundColor || "#ffffff", "--style-pro-button-text": config?.buttonTextColor || "#374151", "--style-pro-selected-button-bg": config?.selectedButtonBackgroundColor || "#eff6ff", "--style-pro-selected-button-text": config?.selectedButtonTextColor || "#2563eb", } as CSSProperties & Record; const renderPromptCategory = ( category: PublicStyleProCategory, layout: "stacked" | "carousel" = "stacked" ) => { const selectedIds = selections[category.id] || []; return (
{category.title}
{category.options.map((option) => { const selected = selectedIds.includes(option.id); return ( ); })}
); }; return (
} logo={logo} onLogoClick={() => { window.location.href = "/"; }} position="sticky" siteName={siteName} />
上传图片 支持拖拽上传
{ void handleFile(event.target.files?.[0]); event.currentTarget.value = ""; }} ref={fileInputRef} type="file" />
{configLoading ? (
正在加载配置
) : !config?.enabled ? (
风格化 PRO 暂未启用,请联系管理员开启。
) : promptDisplayMode === "carousel" ? (
{enabledCategories.map((category) => (
{renderPromptCategory(category, "carousel")}
))}
) : ( enabledCategories.map((category) => (
{renderPromptCategory(category)}
)) )}
生图结果

生成后拖动滑块查看原图与结果图。

模式
{generationMode === "style" ? (
模型 {currentCostLabel} {selectedModel?.creditDiscountLabel ? ( {selectedModel.creditDiscountLabel} ) : null}
) : null} {generationMode === "realistic" ? (
{currentCostLabel}
) : null}
历史记录 最近 4 条
{history.length > 0 ? ( history.map((item) => ( )) ) : (
生成后会在这里保存历史。
)}
); }