feat(*): 功能开放与页面布局 - 新增运维开关并优化后台及创作界面空间利用
This commit is contained in:
@@ -8,9 +8,11 @@ import { formatTaskKind, statusLabel } from "@/lib/generation-task-display";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import type { GenerationTask } from "@/services/api/task-center";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
|
||||
export function CanvasActiveTaskPanel({ tasks }: { tasks: GenerationTask[] }) {
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
const creditsEnabled = useUserStore((state) => state.features.creditsEnabled);
|
||||
const reducedMotion = useReducedMotion();
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -91,6 +93,7 @@ export function CanvasActiveTaskPanel({ tasks }: { tasks: GenerationTask[] }) {
|
||||
expanded={expandedTaskId === task.id}
|
||||
onToggle={() => setExpandedTaskId((current) => current === task.id ? null : task.id)}
|
||||
reducedMotion={Boolean(reducedMotion)}
|
||||
creditsEnabled={creditsEnabled}
|
||||
/>
|
||||
))}
|
||||
</motion.div>
|
||||
@@ -103,7 +106,7 @@ export function CanvasActiveTaskPanel({ tasks }: { tasks: GenerationTask[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ActiveTaskCard({ task, now, theme, expanded, onToggle, reducedMotion }: { task: GenerationTask; now: number; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; expanded: boolean; onToggle: () => void; reducedMotion: boolean }) {
|
||||
function ActiveTaskCard({ task, now, theme, expanded, onToggle, reducedMotion, creditsEnabled }: { task: GenerationTask; now: number; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; expanded: boolean; onToggle: () => void; reducedMotion: boolean; creditsEnabled: boolean }) {
|
||||
const progress = typeof task.progress === "number" ? Math.max(0, Math.min(100, Math.round(task.progress))) : task.status === "queued" ? 0 : undefined;
|
||||
const startedAt = task.startedAt || task.createdAt;
|
||||
const elapsedMs = Math.max(0, now - parseTime(startedAt));
|
||||
@@ -170,9 +173,9 @@ function ActiveTaskCard({ task, now, theme, expanded, onToggle, reducedMotion }:
|
||||
<span className="font-medium tabular-nums" style={{ color: statusTone }}>{progress}%</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-3 grid grid-cols-2 gap-2 text-[var(--fs-tiny)]" style={{ color: theme.node.muted }}>
|
||||
<div className={`mt-3 grid gap-2 text-[var(--fs-tiny)] ${creditsEnabled ? "grid-cols-2" : "grid-cols-1"}`} style={{ color: theme.node.muted }}>
|
||||
<span className="inline-flex min-w-0 items-center gap-1 truncate" title={durationLabel}><Clock3 className="size-3 shrink-0" />{durationLabel}</span>
|
||||
<span className="inline-flex min-w-0 items-center justify-end gap-1 truncate" title={billingLabel}><Coins className="size-3 shrink-0" />{billingLabel}</span>
|
||||
{creditsEnabled ? <span className="inline-flex min-w-0 items-center justify-end gap-1 truncate" title={billingLabel}><Coins className="size-3 shrink-0" />{billingLabel}</span> : null}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { BookOpenText, Bot, Clapperboard, Focus, Globe2, LayoutTemplate, Laptop,
|
||||
|
||||
import type { CanvasContextSummary } from "@/lib/canvas/canvas-context-summary";
|
||||
import type { CanvasTheme } from "@/lib/canvas-theme";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
import type { CanvasAgentMode } from "./canvas-agent-chat-ui";
|
||||
|
||||
export function AgentPanelChrome({
|
||||
@@ -90,6 +91,8 @@ const starterActions = [
|
||||
];
|
||||
|
||||
export function AgentChatEmptyState({ theme, nodeCount, onSelect }: { theme: CanvasTheme; nodeCount: number; onSelect: (value: string) => void }) {
|
||||
const shortDramaEnabled = useUserStore((state) => state.features.shortDramaEnabled);
|
||||
const visibleStarterActions = shortDramaEnabled ? starterActions : starterActions.filter((item) => item.label !== "搭建短剧工作流");
|
||||
return (
|
||||
<div className="flex h-full items-center px-5 py-8">
|
||||
<div className="mx-auto w-full max-w-[380px]">
|
||||
@@ -99,7 +102,7 @@ export function AgentChatEmptyState({ theme, nodeCount, onSelect }: { theme: Can
|
||||
</div>
|
||||
<h2 className="mt-3 text-[19px] font-semibold leading-6" style={{ color: theme.node.text }}>从当前画布开始</h2>
|
||||
<div className="mt-4 grid grid-cols-1 gap-1">
|
||||
{starterActions.map(({ label, icon: Icon }) => (
|
||||
{visibleStarterActions.map(({ label, icon: Icon }) => (
|
||||
<button key={label} type="button" className="group flex min-h-11 min-w-0 items-center gap-2.5 rounded-md px-2.5 text-left text-xs font-medium transition-colors" style={{ color: theme.node.text }} onMouseEnter={(event) => { event.currentTarget.style.background = theme.spatial.surface; }} onMouseLeave={(event) => { event.currentTarget.style.background = "transparent"; }} onFocus={(event) => { event.currentTarget.style.background = theme.spatial.surface; }} onBlur={(event) => { event.currentTarget.style.background = "transparent"; }} onClick={() => onSelect(label)}>
|
||||
<span className="grid size-7 shrink-0 place-items-center rounded-md" style={{ background: theme.spatial.surface, color: theme.node.muted }}><Icon className="size-3.5" /></span>
|
||||
<span className="min-w-0 truncate">{label}</span>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-generation-options";
|
||||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
import { CanvasImageSettingsPopover } from "./canvas-image-settings-popover";
|
||||
import { CanvasAudioSettingsPopover, type CanvasAudioSettingKey } from "./canvas-audio-settings-popover";
|
||||
import { CanvasVideoSettingsPopover } from "./canvas-video-settings-popover";
|
||||
@@ -40,6 +41,7 @@ const videoOperationOptions: Array<{ label: string; value: CanvasVideoEditOperat
|
||||
export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigChange, onGenerate, onStop, onComposerToggle, workspaceMode = "professional" }: CanvasConfigNodePanelProps) {
|
||||
const globalConfig = useEffectiveConfig();
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
const creditsEnabled = useUserStore((state) => state.features.creditsEnabled);
|
||||
const mode = node.metadata?.generationMode || "image";
|
||||
const simpleMode = workspaceMode === "simple";
|
||||
const config = buildNodeConfig(globalConfig, node, mode);
|
||||
@@ -47,7 +49,7 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC
|
||||
const count = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1)));
|
||||
const priceChannel = resolveModelChannel(config, config.model);
|
||||
const credits = requestCreditCost({ channelMode: priceChannel.scope === "system" ? "remote" : "local", modelCosts: priceChannel.modelCosts, model: modelOptionName(config.model), count: mode === "image" ? count : 1, seconds: mode === "video" ? config.videoSeconds : 1 });
|
||||
const hasPrice = credits !== null;
|
||||
const hasPrice = creditsEnabled && credits !== null;
|
||||
const chipStyle = { background: theme.node.fill, borderColor: theme.node.stroke, color: theme.node.text };
|
||||
const hasAnyInput = Boolean(inputSummary.textCount || inputSummary.imageCount || inputSummary.videoCount || inputSummary.audioCount);
|
||||
const hasComposerContent = Boolean((node.metadata?.composerContent ?? node.metadata?.prompt ?? "").trim());
|
||||
@@ -140,7 +142,7 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC
|
||||
<div className="mb-2 rounded-lg px-2 py-2 text-[var(--fs-label)]" style={{ background: theme.node.fill, color: theme.node.muted }}>将使用当前默认模型与生成参数</div>
|
||||
) : (
|
||||
<div className={`mb-2 grid min-w-0 cursor-default items-center gap-2 ${mode === "image" || mode === "video" || mode === "audio" ? "grid-cols-[minmax(0,1fr)_148px]" : "grid-cols-1"}`} onMouseDown={(event) => event.stopPropagation()}>
|
||||
<ModelPicker className="canvas-compact-control h-10" config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} capability={mode} onMissingConfig={() => navigateToSettings({ continueCreation: true })} fullWidth />
|
||||
<ModelPicker className="canvas-compact-control h-10" config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} capability={mode} onMissingConfig={() => navigateToSettings({ continueCreation: true })} fullWidth showSelectedPrice={creditsEnabled} />
|
||||
{mode === "video" ? (
|
||||
<CanvasVideoSettingsPopover config={config} placement="topRight" buttonClassName="canvas-compact-control !h-10 !w-full !justify-start !rounded-lg !px-2" onConfigChange={(key, value) => onConfigChange(node.id, videoConfigPatch(key, value))} />
|
||||
) : mode === "image" ? (
|
||||
@@ -168,7 +170,9 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{hasPrice ? (
|
||||
{!creditsEnabled ? (
|
||||
<span>生成</span>
|
||||
) : hasPrice ? (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<CreditSymbol />
|
||||
{credits.toLocaleString()}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-generation-options";
|
||||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
import { CanvasImageSettingsPopover } from "./canvas-image-settings-popover";
|
||||
import { CanvasAudioSettingsPopover, type CanvasAudioSettingKey } from "./canvas-audio-settings-popover";
|
||||
import { CanvasResourceMentionTextarea } from "./canvas-resource-mention-textarea";
|
||||
@@ -39,6 +40,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
const globalConfig = useEffectiveConfig();
|
||||
const themeName = useThemeStore((state) => state.theme);
|
||||
const theme = canvasThemes[themeName];
|
||||
const creditsEnabled = useUserStore((state) => state.features.creditsEnabled);
|
||||
const simpleMode = workspaceMode === "simple";
|
||||
const mode = defaultMode(node.type);
|
||||
const config = buildNodeConfig(globalConfig, node, mode);
|
||||
@@ -219,7 +221,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
) : mode === "audio" ? (
|
||||
<CanvasAudioSettingsPopover config={config} buttonClassName="!h-7 !w-[146px] !justify-start !rounded-full !border-0 !px-2.5 !text-[var(--fs-tiny)] !font-normal !shadow-none [&>span]:min-w-0 [&_.lucide]:!size-3" onConfigChange={(key, value) => onConfigChange(node.id, audioConfigPatch(key, value))} />
|
||||
) : null}
|
||||
<GenerationCostBadge credits={credits} theme={theme} />
|
||||
{creditsEnabled ? <GenerationCostBadge credits={credits} theme={theme} /> : null}
|
||||
<Button
|
||||
type="text"
|
||||
className="!inline-flex !h-8 !w-8 shrink-0 !items-center !justify-center !rounded-full !border !p-0 transition hover:!-translate-y-px hover:!brightness-110 motion-reduce:hover:!translate-y-0"
|
||||
|
||||
@@ -82,6 +82,21 @@ export function CanvasShortDramaEmptyState({ onCreatePipeline, onOpenAgent, onUp
|
||||
);
|
||||
}
|
||||
|
||||
export function CanvasFreeformEmptyState({ onUpload, onAddText }: { onUpload: () => void; onAddText: () => void }) {
|
||||
const theme = canvasThemes[useThemeStore((state) => state.theme)];
|
||||
return (
|
||||
<div className="pointer-events-none absolute inset-0 z-20 grid place-items-center px-4 pb-20 pt-24">
|
||||
<div className="pointer-events-auto w-full max-w-[440px] rounded-lg border p-4 shadow-sm backdrop-blur" data-canvas-no-zoom style={{ background: theme.toolbar.panel, borderColor: theme.toolbar.border, color: theme.node.text }}>
|
||||
<div className="text-center"><h2 className="text-base font-semibold">从空白画布开始</h2><p className="mt-1 text-xs" style={{ color: theme.node.muted }}>添加文本或导入已有素材。</p></div>
|
||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
||||
<button type="button" onClick={onAddText} className="inline-flex h-10 items-center justify-center gap-2 rounded-md border text-sm font-medium" style={{ borderColor: theme.node.stroke, background: theme.node.fill }}><Type className="size-4" />新建文本</button>
|
||||
<button type="button" onClick={onUpload} className="inline-flex h-10 items-center justify-center gap-2 rounded-md border text-sm font-medium" style={{ borderColor: theme.node.stroke, background: theme.node.fill }}><Upload className="size-4" />导入素材</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PathCard({ icon, title, description, action, accent, theme, focusStyle, onClick }: {
|
||||
icon: ReactNode;
|
||||
title: string;
|
||||
|
||||
Reference in New Issue
Block a user