fix: sync style pro toolbar and realtime controls
This commit is contained in:
parent
e8a3212b35
commit
f726c14397
@ -109,6 +109,7 @@ import {
|
||||
ENHANCE_PRO_KREA_SETTING_KEY,
|
||||
normalizeEnhanceProKreaConfig,
|
||||
} from "@/lib/enhance-pro/config";
|
||||
import { STYLE_PRO_CONFIG_KEY, normalizeStyleProConfig } from "@/lib/style-pro/config";
|
||||
import type { AgentCategoryTreeConfig } from "@/types/agent";
|
||||
|
||||
const HOME_VARIANT_KEY = "home_variant";
|
||||
@ -613,6 +614,7 @@ async function getScopedPublicSiteSettings(scope: PublicSiteSettingsScope) {
|
||||
VIDEO_CANVAS_ENABLED_KEY,
|
||||
VIDEO_CANVAS_SIDEBAR_ENABLED_KEY,
|
||||
ENHANCE_PRO_KREA_SETTING_KEY,
|
||||
STYLE_PRO_CONFIG_KEY,
|
||||
AGENT_WORKFLOW_UI_CONFIG_KEY,
|
||||
AGENT_WORKFLOW_BRAND_MODEL_CONFIG_KEY,
|
||||
AGENT_WORKFLOW_SHOWCASE_CONFIG_KEY,
|
||||
@ -654,6 +656,7 @@ async function getScopedPublicSiteSettings(scope: PublicSiteSettingsScope) {
|
||||
canvasToolbarConfig: normalizeCanvasToolbarConfig(
|
||||
valueByKey.get(CANVAS_TOOLBAR_CONFIG_SETTING_KEY)
|
||||
),
|
||||
styleProEnabled: normalizeStyleProConfig(valueByKey.get(STYLE_PRO_CONFIG_KEY)).enabled,
|
||||
layerSplitEnabled: normalizeLayerSplitEnabled(
|
||||
valueByKey.get(LAYER_SPLIT_CONFIG_KEY)
|
||||
),
|
||||
@ -710,6 +713,7 @@ async function getScopedPublicSiteSettings(scope: PublicSiteSettingsScope) {
|
||||
VIDEO_CANVAS_ENABLED_KEY,
|
||||
VIDEO_CANVAS_SIDEBAR_ENABLED_KEY,
|
||||
ENHANCE_PRO_KREA_SETTING_KEY,
|
||||
STYLE_PRO_CONFIG_KEY,
|
||||
]);
|
||||
const canvasFeatureAvailability = await readCanvasFeatureAvailability(valueByKey);
|
||||
const typedSettings = settings as Record<string, unknown> | undefined;
|
||||
@ -739,6 +743,7 @@ async function getScopedPublicSiteSettings(scope: PublicSiteSettingsScope) {
|
||||
canvasToolbarConfig: normalizeCanvasToolbarConfig(
|
||||
valueByKey.get(CANVAS_TOOLBAR_CONFIG_SETTING_KEY)
|
||||
),
|
||||
styleProEnabled: normalizeStyleProConfig(valueByKey.get(STYLE_PRO_CONFIG_KEY)).enabled,
|
||||
layerSplitEnabled: normalizeLayerSplitEnabled(
|
||||
valueByKey.get(LAYER_SPLIT_CONFIG_KEY)
|
||||
),
|
||||
|
||||
@ -1862,6 +1862,7 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
||||
angleEditEnabled?: boolean;
|
||||
realtimeCanvasEnabled?: boolean;
|
||||
realtimeCanvasServiceEnabled?: boolean;
|
||||
styleProEnabled?: boolean;
|
||||
enhanceProEnabled?: boolean;
|
||||
tripo3dEnabled?: boolean;
|
||||
worldlabs3dEnabled?: boolean;
|
||||
@ -1971,10 +1972,9 @@ export default function CanvasEditorLegacyPage({ params }: PageProps) {
|
||||
const isBatchProcessingEnabled =
|
||||
workflowPageFeatureSettings.batchProcessingEnabled !== false &&
|
||||
sceneFusionConfigData?.enabled !== false;
|
||||
const isStyleProToolbarEnabled = isCanvasToolbarItemEnabled(
|
||||
canvasToolbarConfig,
|
||||
"stylePro"
|
||||
);
|
||||
const isStyleProToolbarEnabled =
|
||||
isCanvasToolbarItemEnabled(canvasToolbarConfig, "stylePro") &&
|
||||
siteSettingsData?.styleProEnabled !== false;
|
||||
const sanitizedWorkflowSelections = useMemo(
|
||||
() =>
|
||||
sanitizeAgentWorkflowSelections({
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import {
|
||||
ArrowLeft,
|
||||
ChevronDown,
|
||||
Circle as CircleIcon,
|
||||
Download,
|
||||
Eraser,
|
||||
@ -205,7 +206,11 @@ const REALTIME_GENERATION_MODE_OPTIONS: Array<{
|
||||
points: 1,
|
||||
},
|
||||
];
|
||||
const REALTIME_ASPECT_RATIO_OPTIONS: RealtimeAspectRatio[] = ["1:1", "4:3", "2:3"];
|
||||
const REALTIME_ASPECT_RATIO_OPTIONS: RealtimeAspectRatio[] = [
|
||||
"1:1",
|
||||
"4:3",
|
||||
"2:3",
|
||||
];
|
||||
|
||||
function cloneImageLayers(layers: ImageLayer[]) {
|
||||
return layers.map((layer) => ({ ...layer }));
|
||||
@ -830,8 +835,7 @@ export function RealtimeCanvasDialog({
|
||||
defaultRealtimeConfig.transport.throttleInterval
|
||||
);
|
||||
const pumpFlushRef = useRef<(() => void) | null>(null);
|
||||
const realtimeGenerationModeRef =
|
||||
useRef<RealtimeGenerationMode>("manual");
|
||||
const realtimeGenerationModeRef = useRef<RealtimeGenerationMode>("manual");
|
||||
const manualGenerateRequestRef = useRef(false);
|
||||
const realtimeAspectRatioRef = useRef<RealtimeAspectRatio>("1:1");
|
||||
const warningMessageRef = useRef("");
|
||||
@ -893,6 +897,7 @@ export function RealtimeCanvasDialog({
|
||||
const [pendingProjectName, setPendingProjectName] = useState("");
|
||||
const [brushPopoverOpen, setBrushPopoverOpen] = useState(false);
|
||||
const [eraserPopoverOpen, setEraserPopoverOpen] = useState(false);
|
||||
const [aspectRatioPopoverOpen, setAspectRatioPopoverOpen] = useState(false);
|
||||
const [isDragImportActive, setIsDragImportActive] = useState(false);
|
||||
const [adaptiveThrottleMs, setAdaptiveThrottleMs] = useState<number>(
|
||||
defaultRealtimeConfig.transport.throttleInterval
|
||||
@ -1015,10 +1020,13 @@ export function RealtimeCanvasDialog({
|
||||
iconUrl: modeIconConfig.realtimeIconUrl,
|
||||
}
|
||||
);
|
||||
const realtimeModeEnhanceItem = resolveRealtimeModeItem("realtimeModeEnhance", {
|
||||
label: modeIconConfig.enhanceLabel,
|
||||
iconUrl: modeIconConfig.enhanceIconUrl,
|
||||
});
|
||||
const realtimeModeEnhanceItem = resolveRealtimeModeItem(
|
||||
"realtimeModeEnhance",
|
||||
{
|
||||
label: modeIconConfig.enhanceLabel,
|
||||
iconUrl: modeIconConfig.enhanceIconUrl,
|
||||
}
|
||||
);
|
||||
const realtimeReturnCanvasItem = getCanvasToolbarItem(
|
||||
canvasToolbarConfig,
|
||||
"realtimeReturnCanvas"
|
||||
@ -1095,14 +1103,18 @@ export function RealtimeCanvasDialog({
|
||||
if (!data || cancelled) return;
|
||||
setIsLayerSplitEnabled(data.layerSplitEnabled === true);
|
||||
setIsEnhanceProEnabled(data.enhanceProEnabled === true);
|
||||
setCanvasToolbarConfig(normalizeCanvasToolbarConfig(data.canvasToolbarConfig));
|
||||
setCanvasToolbarConfig(
|
||||
normalizeCanvasToolbarConfig(data.canvasToolbarConfig)
|
||||
);
|
||||
const iconConfig =
|
||||
data.realtimeCanvasModeIconConfig &&
|
||||
typeof data.realtimeCanvasModeIconConfig === "object"
|
||||
? data.realtimeCanvasModeIconConfig
|
||||
: {};
|
||||
const normalizeModeLabel = (value: unknown, fallback: string) => {
|
||||
const label = String(value || "").trim().slice(0, 32);
|
||||
const label = String(value || "")
|
||||
.trim()
|
||||
.slice(0, 32);
|
||||
return label || fallback;
|
||||
};
|
||||
setModeIconConfig({
|
||||
@ -1917,6 +1929,7 @@ export function RealtimeCanvasDialog({
|
||||
|
||||
const handleRealtimeAspectRatioChange = useCallback(
|
||||
(nextRatio: RealtimeAspectRatio) => {
|
||||
setAspectRatioPopoverOpen(false);
|
||||
if (nextRatio === realtimeAspectRatioRef.current) return;
|
||||
realtimeAspectRatioRef.current = nextRatio;
|
||||
setRealtimeAspectRatio(nextRatio);
|
||||
@ -3253,19 +3266,44 @@ export function RealtimeCanvasDialog({
|
||||
[onApplyResult, onOpenChange]
|
||||
);
|
||||
|
||||
const handleDownloadPreview = useCallback(() => {
|
||||
const handleDownloadPreview = useCallback(async () => {
|
||||
if (!previewUrl) {
|
||||
toast.error("请先生成预览结果");
|
||||
return;
|
||||
}
|
||||
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = /^https?:\/\//i.test(previewUrl)
|
||||
? buildImageProxyUrl(previewUrl)
|
||||
: previewUrl;
|
||||
anchor.download = `realtime-preview-${Date.now()}.png`;
|
||||
anchor.rel = "noopener";
|
||||
anchor.click();
|
||||
try {
|
||||
toast.info("正在准备下载...");
|
||||
const downloadUrl = /^https?:\/\//i.test(previewUrl)
|
||||
? buildImageProxyUrl(previewUrl, { delivery: "proxy" })
|
||||
: previewUrl;
|
||||
const response = await fetch(downloadUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败(${response.status})`);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const mimeType = blob.type.toLowerCase();
|
||||
const extension = mimeType.includes("jpeg")
|
||||
? "jpg"
|
||||
: mimeType.includes("webp")
|
||||
? "webp"
|
||||
: mimeType.includes("png")
|
||||
? "png"
|
||||
: "png";
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = blobUrl;
|
||||
anchor.download = `realtime-preview-${Date.now()}.${extension}`;
|
||||
anchor.rel = "noopener";
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
window.setTimeout(() => URL.revokeObjectURL(blobUrl), 1000);
|
||||
toast.success("下载成功");
|
||||
} catch (error) {
|
||||
console.error("[RealtimeCanvasDialog] 下载预览失败:", error);
|
||||
toast.error("下载失败,请稍后重试");
|
||||
}
|
||||
}, [previewUrl]);
|
||||
|
||||
const handleCloseRealtime = useCallback(() => {
|
||||
@ -3406,23 +3444,23 @@ export function RealtimeCanvasDialog({
|
||||
floatingSurfaceClass
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className="inline-flex h-12 shrink-0 items-center gap-3 rounded-[22px] border border-white/90 bg-white/80 px-4 text-left text-[#111111] shadow-[inset_0_1px_0_rgba(255,255,255,0.95)] transition hover:bg-white dark:border-white/12 dark:bg-white/8 dark:text-white dark:shadow-none dark:hover:bg-white/12"
|
||||
onClick={handleCloseRealtime}
|
||||
title={realtimeReturnCanvasItem.label}
|
||||
type="button"
|
||||
>
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-full border border-[#ececec] bg-white shadow-[0_8px_18px_rgba(0,0,0,0.06)] dark:border-white/12 dark:bg-white/10 dark:shadow-none">
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4 text-[#111111] dark:text-white"
|
||||
fallback={ArrowLeft}
|
||||
item={realtimeReturnCanvasItem}
|
||||
/>
|
||||
</span>
|
||||
<span className="font-semibold text-[14px] tracking-[-0.02em]">
|
||||
{realtimeReturnCanvasItem.label}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="inline-flex h-12 shrink-0 items-center gap-3 rounded-[22px] border border-white/90 bg-white/80 px-4 text-left text-[#111111] shadow-[inset_0_1px_0_rgba(255,255,255,0.95)] transition hover:bg-white dark:border-white/12 dark:bg-white/8 dark:text-white dark:shadow-none dark:hover:bg-white/12"
|
||||
onClick={handleCloseRealtime}
|
||||
title={realtimeReturnCanvasItem.label}
|
||||
type="button"
|
||||
>
|
||||
<span className="flex h-8 w-8 items-center justify-center rounded-full border border-[#ececec] bg-white shadow-[0_8px_18px_rgba(0,0,0,0.06)] dark:border-white/12 dark:bg-white/10 dark:shadow-none">
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4 text-[#111111] dark:text-white"
|
||||
fallback={ArrowLeft}
|
||||
item={realtimeReturnCanvasItem}
|
||||
/>
|
||||
</span>
|
||||
<span className="font-semibold text-[14px] tracking-[-0.02em]">
|
||||
{realtimeReturnCanvasItem.label}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div className="inline-flex h-12 min-w-0 items-center gap-1 rounded-[22px] border border-[#ebebeb] bg-[#fafafa] p-1 shadow-[inset_0_1px_0_rgba(255,255,255,0.9)] dark:border-white/12 dark:bg-white/8 dark:shadow-none">
|
||||
<button
|
||||
@ -3435,15 +3473,15 @@ export function RealtimeCanvasDialog({
|
||||
onClick={() => handleWorkspaceModeChange("realtime")}
|
||||
type="button"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
active={workspaceMode === "realtime"}
|
||||
className="h-4 w-4 transition group-hover:opacity-100"
|
||||
fallback={Paintbrush}
|
||||
item={realtimeModeRealtimeItem}
|
||||
/>
|
||||
<span className="max-w-[9rem] truncate">
|
||||
{realtimeModeRealtimeItem.label}
|
||||
</span>
|
||||
<CanvasToolbarIcon
|
||||
active={workspaceMode === "realtime"}
|
||||
className="h-4 w-4 transition group-hover:opacity-100"
|
||||
fallback={Paintbrush}
|
||||
item={realtimeModeRealtimeItem}
|
||||
/>
|
||||
<span className="max-w-[9rem] truncate">
|
||||
{realtimeModeRealtimeItem.label}
|
||||
</span>
|
||||
</button>
|
||||
{isEnhanceProEnabled ? (
|
||||
<button
|
||||
@ -3456,15 +3494,15 @@ export function RealtimeCanvasDialog({
|
||||
onClick={() => handleWorkspaceModeChange("enhance-pro")}
|
||||
type="button"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
active={workspaceMode === "enhance-pro"}
|
||||
className="h-4 w-4 transition group-hover:opacity-100"
|
||||
fallback={Wand2}
|
||||
item={realtimeModeEnhanceItem}
|
||||
/>
|
||||
<span className="max-w-[9rem] truncate">
|
||||
{realtimeModeEnhanceItem.label}
|
||||
</span>
|
||||
<CanvasToolbarIcon
|
||||
active={workspaceMode === "enhance-pro"}
|
||||
className="h-4 w-4 transition group-hover:opacity-100"
|
||||
fallback={Wand2}
|
||||
item={realtimeModeEnhanceItem}
|
||||
/>
|
||||
<span className="max-w-[9rem] truncate">
|
||||
{realtimeModeEnhanceItem.label}
|
||||
</span>
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
@ -3519,7 +3557,10 @@ export function RealtimeCanvasDialog({
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
className={cn("h-10 flex-1 rounded-2xl", primaryActionButtonClass)}
|
||||
className={cn(
|
||||
"h-10 flex-1 rounded-2xl",
|
||||
primaryActionButtonClass
|
||||
)}
|
||||
onClick={() => void confirmSaveCurrentProject()}
|
||||
type="button"
|
||||
>
|
||||
@ -3608,7 +3649,10 @@ export function RealtimeCanvasDialog({
|
||||
打开
|
||||
</Button>
|
||||
<Button
|
||||
className={cn("h-8 rounded-xl px-4", controlSurfaceClass)}
|
||||
className={cn(
|
||||
"h-8 rounded-xl px-4",
|
||||
controlSurfaceClass
|
||||
)}
|
||||
onClick={() => deleteSavedProject(project.id)}
|
||||
size="sm"
|
||||
type="button"
|
||||
@ -3768,18 +3812,18 @@ export function RealtimeCanvasDialog({
|
||||
floatingSurfaceClass
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-center border-[#e5e5e5] border-b pb-3 dark:border-white/12">
|
||||
<div
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full border border-[#ececec] bg-white text-[#111111] dark:border-white/12 dark:bg-white/10 dark:text-white"
|
||||
title={realtimeLayerPanelItem.label}
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4"
|
||||
fallback={Layers3}
|
||||
item={realtimeLayerPanelItem}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center border-[#e5e5e5] border-b pb-3 dark:border-white/12">
|
||||
<div
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full border border-[#ececec] bg-white text-[#111111] dark:border-white/12 dark:bg-white/10 dark:text-white"
|
||||
title={realtimeLayerPanelItem.label}
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4"
|
||||
fallback={Layers3}
|
||||
item={realtimeLayerPanelItem}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-1 flex-col items-center space-y-2 overflow-y-auto py-1">
|
||||
{imageLayers.length === 0 ? (
|
||||
<div className="rounded-2xl border border-[#e5e5e5] border-dashed px-1 py-4 text-center text-[10px] text-[#666666] leading-4 dark:border-white/12 dark:text-white/55">
|
||||
@ -3840,20 +3884,20 @@ export function RealtimeCanvasDialog({
|
||||
)}
|
||||
disabled={isLayerSplitImporting}
|
||||
onClick={() => splitUploadInputRef.current?.click()}
|
||||
title={realtimeLayerSplitImportItem.label}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{isLayerSplitImporting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4"
|
||||
fallback={Layers3}
|
||||
item={realtimeLayerSplitImportItem}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
title={realtimeLayerSplitImportItem.label}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
{isLayerSplitImporting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4"
|
||||
fallback={Layers3}
|
||||
item={realtimeLayerSplitImportItem}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
className={cn(
|
||||
@ -3862,16 +3906,16 @@ export function RealtimeCanvasDialog({
|
||||
)}
|
||||
disabled={isLayerSplitImporting}
|
||||
onClick={() => uploadInputRef.current?.click()}
|
||||
title={realtimeAddImageLayerItem.label}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4"
|
||||
fallback={Upload}
|
||||
item={realtimeAddImageLayerItem}
|
||||
/>
|
||||
</Button>
|
||||
title={realtimeAddImageLayerItem.label}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4 w-4"
|
||||
fallback={Upload}
|
||||
item={realtimeAddImageLayerItem}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -3906,25 +3950,25 @@ export function RealtimeCanvasDialog({
|
||||
"h-10 rounded-full px-4 font-medium text-sm",
|
||||
softActionButtonClass
|
||||
)}
|
||||
onClick={handleDownloadPreview}
|
||||
size="sm"
|
||||
type="button"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
active
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Download}
|
||||
item={realtimeDownloadPreviewItem}
|
||||
/>
|
||||
{realtimeDownloadPreviewItem.label}
|
||||
</Button>
|
||||
onClick={handleDownloadPreview}
|
||||
size="sm"
|
||||
type="button"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
active
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Download}
|
||||
item={realtimeDownloadPreviewItem}
|
||||
/>
|
||||
{realtimeDownloadPreviewItem.label}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="-translate-x-1/2 pointer-events-none absolute bottom-4 left-1/2 z-30 flex w-[min(600px,calc(100vw-2rem))] flex-col items-center gap-2.5">
|
||||
<div className="-translate-x-1/2 pointer-events-none absolute bottom-4 left-1/2 z-30 flex w-[min(760px,calc(100vw-2rem))] flex-col items-center gap-2.5">
|
||||
<TooltipProvider delayDuration={120}>
|
||||
<div
|
||||
className={cn(
|
||||
@ -3939,24 +3983,24 @@ export function RealtimeCanvasDialog({
|
||||
compactToolButtonClass,
|
||||
brushMode === "select" && compactToolButtonActiveClass
|
||||
)}
|
||||
onClick={activateSelectMode}
|
||||
size="icon"
|
||||
title={realtimeSelectToolItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
active={brushMode === "select"}
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={MousePointer2}
|
||||
item={realtimeSelectToolItem}
|
||||
/>
|
||||
</Button>
|
||||
onClick={activateSelectMode}
|
||||
size="icon"
|
||||
title={realtimeSelectToolItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
active={brushMode === "select"}
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={MousePointer2}
|
||||
item={realtimeSelectToolItem}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeSelectToolItem.label}
|
||||
</div>
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeSelectToolItem.label}
|
||||
</div>
|
||||
<div className="text-[#666666] text-xs">
|
||||
选择图层并拖动位置
|
||||
</div>
|
||||
@ -3975,19 +4019,19 @@ export function RealtimeCanvasDialog({
|
||||
brushMode === "brush" &&
|
||||
compactToolButtonActiveClass
|
||||
)}
|
||||
onClick={activateBrushMode}
|
||||
size="icon"
|
||||
title={realtimeBrushToolItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
active={brushMode === "brush"}
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={Paintbrush}
|
||||
item={realtimeBrushToolItem}
|
||||
/>
|
||||
</Button>
|
||||
onClick={activateBrushMode}
|
||||
size="icon"
|
||||
title={realtimeBrushToolItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
active={brushMode === "brush"}
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={Paintbrush}
|
||||
item={realtimeBrushToolItem}
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<PopoverContent
|
||||
@ -4049,9 +4093,9 @@ export function RealtimeCanvasDialog({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<TooltipContent>
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeBrushToolItem.label}
|
||||
</div>
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeBrushToolItem.label}
|
||||
</div>
|
||||
<div className="text-[#666666] text-xs">
|
||||
点击后直接调整颜色和粗细
|
||||
</div>
|
||||
@ -4070,19 +4114,19 @@ export function RealtimeCanvasDialog({
|
||||
brushMode === "eraser" &&
|
||||
compactToolButtonActiveClass
|
||||
)}
|
||||
onClick={activateEraserMode}
|
||||
size="icon"
|
||||
title={realtimeEraserToolItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
active={brushMode === "eraser"}
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={Eraser}
|
||||
item={realtimeEraserToolItem}
|
||||
/>
|
||||
</Button>
|
||||
onClick={activateEraserMode}
|
||||
size="icon"
|
||||
title={realtimeEraserToolItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
active={brushMode === "eraser"}
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={Eraser}
|
||||
item={realtimeEraserToolItem}
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<PopoverContent
|
||||
@ -4113,9 +4157,9 @@ export function RealtimeCanvasDialog({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<TooltipContent>
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeEraserToolItem.label}
|
||||
</div>
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeEraserToolItem.label}
|
||||
</div>
|
||||
<div className="text-[#666666] text-xs">
|
||||
点击后直接调整橡皮粗细
|
||||
</div>
|
||||
@ -4126,23 +4170,23 @@ export function RealtimeCanvasDialog({
|
||||
<TooltipTrigger asChild>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
className={cn(
|
||||
compactToolButtonClass,
|
||||
isShapeMode(brushMode) &&
|
||||
compactToolButtonActiveClass
|
||||
)}
|
||||
size="icon"
|
||||
title={realtimeShapeToolItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
active={isShapeMode(brushMode)}
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={Square}
|
||||
item={realtimeShapeToolItem}
|
||||
/>
|
||||
</Button>
|
||||
className={cn(
|
||||
compactToolButtonClass,
|
||||
isShapeMode(brushMode) &&
|
||||
compactToolButtonActiveClass
|
||||
)}
|
||||
size="icon"
|
||||
title={realtimeShapeToolItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
active={isShapeMode(brushMode)}
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={Square}
|
||||
item={realtimeShapeToolItem}
|
||||
/>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<PopoverContent
|
||||
@ -4198,9 +4242,9 @@ export function RealtimeCanvasDialog({
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<TooltipContent>
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeShapeToolItem.label}
|
||||
</div>
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeShapeToolItem.label}
|
||||
</div>
|
||||
<div className="text-[#666666] text-xs">
|
||||
矩形、圆形与直线
|
||||
</div>
|
||||
@ -4209,25 +4253,25 @@ export function RealtimeCanvasDialog({
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className={compactToolButtonClass}
|
||||
onClick={() => void saveCurrentProject()}
|
||||
size="icon"
|
||||
title={realtimeSaveProjectItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={Save}
|
||||
item={realtimeSaveProjectItem}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
className={compactToolButtonClass}
|
||||
onClick={() => void saveCurrentProject()}
|
||||
size="icon"
|
||||
title={realtimeSaveProjectItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={Save}
|
||||
item={realtimeSaveProjectItem}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeSaveProjectItem.label}
|
||||
</div>
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeSaveProjectItem.label}
|
||||
</div>
|
||||
<div className="text-[#666666] text-xs">
|
||||
保存当前画布状态
|
||||
</div>
|
||||
@ -4241,48 +4285,46 @@ export function RealtimeCanvasDialog({
|
||||
refreshSavedProjects();
|
||||
setProjectPanelOpen((openState) => !openState);
|
||||
}}
|
||||
size="icon"
|
||||
title={realtimeProjectListItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={FolderOpen}
|
||||
item={realtimeProjectListItem}
|
||||
/>
|
||||
</Button>
|
||||
size="icon"
|
||||
title={realtimeProjectListItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={FolderOpen}
|
||||
item={realtimeProjectListItem}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeProjectListItem.label}
|
||||
</div>
|
||||
<div className="text-[#666666] text-xs">
|
||||
打开已保存项目
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeProjectListItem.label}
|
||||
</div>
|
||||
<div className="text-[#666666] text-xs">打开已保存项目</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className={compactToolButtonClass}
|
||||
onClick={startBlankProject}
|
||||
size="icon"
|
||||
title={realtimeNewBlankItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={Plus}
|
||||
item={realtimeNewBlankItem}
|
||||
/>
|
||||
</Button>
|
||||
onClick={startBlankProject}
|
||||
size="icon"
|
||||
title={realtimeNewBlankItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={Plus}
|
||||
item={realtimeNewBlankItem}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeNewBlankItem.label}
|
||||
</div>
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeNewBlankItem.label}
|
||||
</div>
|
||||
<div className="text-[#666666] text-xs">
|
||||
清空并开始新项目
|
||||
</div>
|
||||
@ -4293,54 +4335,50 @@ export function RealtimeCanvasDialog({
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className={compactToolButtonClass}
|
||||
disabled={!canUndo}
|
||||
onClick={undo}
|
||||
size="icon"
|
||||
title={realtimeUndoItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={Undo2}
|
||||
item={realtimeUndoItem}
|
||||
/>
|
||||
</Button>
|
||||
disabled={!canUndo}
|
||||
onClick={undo}
|
||||
size="icon"
|
||||
title={realtimeUndoItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={Undo2}
|
||||
item={realtimeUndoItem}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeUndoItem.label}
|
||||
</div>
|
||||
<div className="text-[#666666] text-xs">
|
||||
回退上一步编辑
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeUndoItem.label}
|
||||
</div>
|
||||
<div className="text-[#666666] text-xs">回退上一步编辑</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
className={compactToolButtonClass}
|
||||
disabled={!canRedo}
|
||||
onClick={redo}
|
||||
size="icon"
|
||||
title={realtimeRedoItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={Redo2}
|
||||
item={realtimeRedoItem}
|
||||
/>
|
||||
</Button>
|
||||
disabled={!canRedo}
|
||||
onClick={redo}
|
||||
size="icon"
|
||||
title={realtimeRedoItem.label}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
className="h-4.5 w-4.5"
|
||||
fallback={Redo2}
|
||||
item={realtimeRedoItem}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeRedoItem.label}
|
||||
</div>
|
||||
<div className="text-[#666666] text-xs">
|
||||
恢复已撤销操作
|
||||
<div className="font-medium text-sm">
|
||||
{realtimeRedoItem.label}
|
||||
</div>
|
||||
<div className="text-[#666666] text-xs">恢复已撤销操作</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@ -4368,8 +4406,8 @@ export function RealtimeCanvasDialog({
|
||||
placeholder={FALLBACK_PROMPT}
|
||||
value={prompt}
|
||||
/>
|
||||
<div className="mt-3 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="mt-3 flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-2 overflow-x-auto">
|
||||
<div className="inline-flex h-9 items-center gap-1 rounded-full border border-[#e5e5e5] bg-white p-1 shadow-[0_6px_16px_rgba(0,0,0,0.04)] dark:border-white/12 dark:bg-white/8">
|
||||
{REALTIME_GENERATION_MODE_OPTIONS.map((option) => {
|
||||
const active = option.value === realtimeGenerationMode;
|
||||
@ -4393,27 +4431,51 @@ export function RealtimeCanvasDialog({
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="inline-flex h-9 items-center gap-1 rounded-full border border-[#e5e5e5] bg-white p-1 shadow-[0_6px_16px_rgba(0,0,0,0.04)] dark:border-white/12 dark:bg-white/8">
|
||||
{REALTIME_ASPECT_RATIO_OPTIONS.map((ratio) => {
|
||||
const active = ratio === realtimeAspectRatio;
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"h-7 rounded-full px-3 font-medium text-xs transition",
|
||||
active
|
||||
? "bg-[#111111] text-white dark:bg-white dark:text-[#111111]"
|
||||
: "text-[#666666] hover:bg-[#f4f4f4] dark:text-white/58 dark:hover:bg-white/10"
|
||||
)}
|
||||
key={ratio}
|
||||
onClick={() => handleRealtimeAspectRatioChange(ratio)}
|
||||
type="button"
|
||||
>
|
||||
{ratio}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex h-9 items-center rounded-full border border-[#e5e5e5] bg-white px-3 font-semibold text-[#111111] text-xs shadow-[0_6px_16px_rgba(0,0,0,0.04)] dark:border-white/12 dark:bg-white/8 dark:text-white">
|
||||
<Popover
|
||||
onOpenChange={setAspectRatioPopoverOpen}
|
||||
open={aspectRatioPopoverOpen}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
className="inline-flex h-9 shrink-0 items-center gap-1 rounded-full border border-[#e5e5e5] bg-white px-3 font-semibold text-[#111111] text-xs shadow-[0_6px_16px_rgba(0,0,0,0.04)] transition hover:bg-[#f4f4f4] dark:border-white/12 dark:bg-white/8 dark:text-white dark:hover:bg-white/10"
|
||||
title="切换图像比例"
|
||||
type="button"
|
||||
>
|
||||
{realtimeAspectRatio}
|
||||
<ChevronDown className="h-3.5 w-3.5 opacity-55" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="center"
|
||||
className="w-auto rounded-2xl border border-[#e5e5e5] bg-white p-1 shadow-[0_16px_36px_rgba(0,0,0,0.12)] dark:border-white/12 dark:bg-[#171717]"
|
||||
side="top"
|
||||
sideOffset={8}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
{REALTIME_ASPECT_RATIO_OPTIONS.map((ratio) => {
|
||||
const active = ratio === realtimeAspectRatio;
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
"h-8 rounded-full px-3 font-medium text-xs transition",
|
||||
active
|
||||
? "bg-[#111111] text-white dark:bg-white dark:text-[#111111]"
|
||||
: "text-[#666666] hover:bg-[#f4f4f4] dark:text-white/58 dark:hover:bg-white/10"
|
||||
)}
|
||||
key={ratio}
|
||||
onClick={() =>
|
||||
handleRealtimeAspectRatioChange(ratio)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
{ratio}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="flex h-9 shrink-0 items-center rounded-full border border-[#e5e5e5] bg-white px-3 font-semibold text-[#111111] text-xs shadow-[0_6px_16px_rgba(0,0,0,0.04)] dark:border-white/12 dark:bg-white/8 dark:text-white">
|
||||
{currentRealtimeModeOption.points} 积分
|
||||
</div>
|
||||
<div
|
||||
@ -4433,50 +4495,50 @@ export function RealtimeCanvasDialog({
|
||||
{statusBadge.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Button
|
||||
className={cn(
|
||||
"h-9 shrink-0 rounded-full px-4 font-medium",
|
||||
primaryActionButtonClass
|
||||
)}
|
||||
disabled={status === "connecting"}
|
||||
onClick={triggerGenerate}
|
||||
size="sm"
|
||||
title={realtimeGenerateItem.label}
|
||||
>
|
||||
{status === "connecting" ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<CanvasToolbarIcon
|
||||
active
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Sparkles}
|
||||
item={realtimeGenerateItem}
|
||||
/>
|
||||
{realtimeGenerateItem.label}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
disabled={status === "connecting"}
|
||||
onClick={triggerGenerate}
|
||||
size="sm"
|
||||
title={realtimeGenerateItem.label}
|
||||
>
|
||||
{status === "connecting" ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<CanvasToolbarIcon
|
||||
active
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Sparkles}
|
||||
item={realtimeGenerateItem}
|
||||
/>
|
||||
{realtimeGenerateItem.label}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
className={cn(
|
||||
"h-9 shrink-0 rounded-full px-4 font-medium",
|
||||
softActionButtonClass
|
||||
)}
|
||||
disabled={!previewUrl}
|
||||
onClick={handleApplyToCanvas}
|
||||
size="sm"
|
||||
title={realtimeSendToCanvasItem.label}
|
||||
variant="outline"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
active
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Send}
|
||||
item={realtimeSendToCanvasItem}
|
||||
/>
|
||||
{realtimeSendToCanvasItem.label}
|
||||
</Button>
|
||||
disabled={!previewUrl}
|
||||
onClick={handleApplyToCanvas}
|
||||
size="sm"
|
||||
title={realtimeSendToCanvasItem.label}
|
||||
variant="outline"
|
||||
>
|
||||
<CanvasToolbarIcon
|
||||
active
|
||||
className="mr-1.5 h-4 w-4"
|
||||
fallback={Send}
|
||||
item={realtimeSendToCanvasItem}
|
||||
/>
|
||||
{realtimeSendToCanvasItem.label}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Clock3, Loader2, Sparkles, Upload } from "lucide-react";
|
||||
import { Clock3, Loader2, LogOut, Sparkles, Upload } from "lucide-react";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import {
|
||||
type KeyboardEvent as ReactKeyboardEvent,
|
||||
type PointerEvent as ReactPointerEvent,
|
||||
@ -14,6 +15,12 @@ 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 {
|
||||
@ -55,6 +62,29 @@ type StyleProModelsResponse = {
|
||||
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;
|
||||
@ -194,6 +224,211 @@ function mergeStyleProState(
|
||||
};
|
||||
}
|
||||
|
||||
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 StyleProAccountActions() {
|
||||
const { data: session, status } = useSession();
|
||||
const [accountCenterOpen, setAccountCenterOpen] = useState(false);
|
||||
const isAuthenticated = status === "authenticated" && Boolean(session?.user);
|
||||
const { data: membershipData } = useSWR<StyleProMembershipSummaryResponse>(
|
||||
isAuthenticated ? "/api/user/membership-summary" : null,
|
||||
fetcher
|
||||
);
|
||||
const { data: accountCenterData, isLoading: accountCenterLoading } =
|
||||
useSWR<StyleProAccountCenterResponse>(
|
||||
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 (
|
||||
<div className="h-8 w-[96px] animate-pulse rounded-[14px] bg-black/8 dark:bg-white/8" />
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<button
|
||||
className="inline-flex h-8 items-center justify-center rounded-full border border-black/15 bg-white px-3 text-xs font-medium text-black/80 transition hover:bg-black/5 dark:border-white/12 dark:bg-[#161618] dark:text-white/80 dark:hover:bg-white/8"
|
||||
onClick={openLogin}
|
||||
type="button"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
aria-label="个人中心"
|
||||
className="inline-flex h-8 items-center gap-2 rounded-[14px] border border-black/10 bg-white px-2.5 text-[12px] font-medium text-black/72 shadow-sm transition hover:bg-black/5 hover:text-black dark:border-white/10 dark:bg-white/6 dark:text-white/72 dark:hover:bg-white/10 dark:hover:text-white"
|
||||
onClick={() => setAccountCenterOpen(true)}
|
||||
type="button"
|
||||
>
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full bg-[#d4551f] text-[11px] font-semibold text-white">
|
||||
{userInitials}
|
||||
</span>
|
||||
<span className="hidden sm:inline">个人中心</span>
|
||||
</button>
|
||||
|
||||
<Dialog open={accountCenterOpen} onOpenChange={setAccountCenterOpen}>
|
||||
<DialogContent className="w-[92vw] max-w-[520px] rounded-[22px] border-black/10 bg-[#f6f6f7]/95 p-0 shadow-2xl dark:border-white/10 dark:bg-[#101014]/95 dark:text-white">
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>个人中心</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="p-4">
|
||||
<div className="rounded-[18px] border border-black/10 bg-white p-4 dark:border-white/10 dark:bg-[#17171b]">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-full bg-[#d4551f] text-[14px] font-semibold text-white">
|
||||
{userInitials}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-[15px] font-semibold text-black dark:text-white">
|
||||
{accountProfile.name}
|
||||
</div>
|
||||
<div className="truncate text-[12px] text-black/45 dark:text-white/45">
|
||||
{accountProfile.email || "登录后查看账户邮箱"}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="h-8 rounded-xl border-black/10 bg-white px-3 text-[12px] text-black/75 hover:bg-black/5 dark:border-white/12 dark:bg-[#111115] dark:text-white/75 dark:hover:bg-white/6"
|
||||
onClick={() => {
|
||||
void handleLogout();
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<LogOut className="mr-1.5 h-3.5 w-3.5" />
|
||||
退出
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 grid grid-cols-2 gap-3">
|
||||
<div className="rounded-[16px] border border-black/10 bg-white p-3 dark:border-white/10 dark:bg-[#17171b]">
|
||||
<div className="text-[12px] text-black/45 dark:text-white/45">
|
||||
积分余额
|
||||
</div>
|
||||
<div className="mt-1 text-[20px] font-semibold text-black dark:text-white">
|
||||
{accountCenterLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
formatStyleProCredits(accountProfile.availablePoints)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[16px] border border-black/10 bg-white p-3 dark:border-white/10 dark:bg-[#17171b]">
|
||||
<div className="text-[12px] text-black/45 dark:text-white/45">
|
||||
当前套餐
|
||||
</div>
|
||||
<div className="mt-1 truncate text-[20px] font-semibold text-black dark:text-white">
|
||||
{accountProfile.planName}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[16px] border border-black/10 bg-white p-3 dark:border-white/10 dark:bg-[#17171b]">
|
||||
<div className="text-[12px] text-black/45 dark:text-white/45">
|
||||
每月更新
|
||||
</div>
|
||||
<div className="mt-1 text-[20px] font-semibold text-black dark:text-white">
|
||||
{formatStyleProCredits(accountProfile.monthlyCredits)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-[16px] border border-black/10 bg-white p-3 dark:border-white/10 dark:bg-[#17171b]">
|
||||
<div className="text-[12px] text-black/45 dark:text-white/45">
|
||||
每日赠送
|
||||
</div>
|
||||
<div className="mt-1 text-[20px] font-semibold text-black dark:text-white">
|
||||
{formatStyleProCredits(accountProfile.dailyFreeCredits)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ImagePreview({
|
||||
alt,
|
||||
className,
|
||||
@ -844,6 +1079,7 @@ export function StyleProWorkspace({
|
||||
|
||||
<main className="min-w-0 flex-1 overflow-y-auto">
|
||||
<BrandTopBar
|
||||
actions={<StyleProAccountActions />}
|
||||
logo={logo}
|
||||
onLogoClick={() => {
|
||||
window.location.href = "/";
|
||||
@ -1044,13 +1280,7 @@ export function StyleProWorkspace({
|
||||
<div className="space-y-1 p-2">
|
||||
<div className="flex items-center gap-1 text-[11px] text-gray-400">
|
||||
<Clock3 className="h-3 w-3" />
|
||||
{new Date(item.createdAt).toLocaleTimeString(
|
||||
"zh-CN",
|
||||
{
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}
|
||||
)}
|
||||
{formatStyleProHistoryDateTime(item.createdAt)}
|
||||
</div>
|
||||
<div className="line-clamp-1 text-[12px] text-black/70">
|
||||
{item.optionLabels[0] || "风格化结果"}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user