feat(assets): 文件夹主题 - 新增多主题皮肤并同步主线
This commit is contained in:
@@ -2,11 +2,10 @@ import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Dropdown } from "antd";
|
||||
import { FileAudio, FileText, MoreHorizontal, Pencil, Plus, SlidersHorizontal, Sparkles, Video } from "lucide-react";
|
||||
|
||||
import type { CanvasFolderStyle, CanvasNodeData } from "@/types/canvas";
|
||||
import { CANVAS_FOLDER_THEME_OPTIONS, resolveCanvasFolderTheme, resolveCanvasFolderThemeCover } from "@/lib/canvas/canvas-folder-theme";
|
||||
import type { CanvasFolderStyle, CanvasFolderTheme, CanvasNodeData } from "@/types/canvas";
|
||||
import { CanvasNodeType } from "@/types/canvas";
|
||||
|
||||
const DEFAULT_FOLDER_COVER = "/images/canvas/folder-default-cover.png";
|
||||
|
||||
const FOLDER_STYLE_OPTIONS: Array<{ key: CanvasFolderStyle; label: string }> = [
|
||||
{ key: "glass", label: "流光玻璃" },
|
||||
{ key: "stacked", label: "内容陈列" },
|
||||
@@ -25,6 +24,7 @@ export const CanvasFolderPreview = React.memo(function CanvasFolderPreview({
|
||||
onToggleCollapsed,
|
||||
onTitleChange,
|
||||
onStyleChange,
|
||||
onThemeChange,
|
||||
}: {
|
||||
data: CanvasNodeData;
|
||||
childNodes: CanvasNodeData[];
|
||||
@@ -34,13 +34,34 @@ export const CanvasFolderPreview = React.memo(function CanvasFolderPreview({
|
||||
onToggleCollapsed: (nodeId: string) => void;
|
||||
onTitleChange: (nodeId: string, title: string) => void;
|
||||
onStyleChange: (nodeId: string, style: CanvasFolderStyle) => void;
|
||||
onThemeChange: (nodeId: string, theme: CanvasFolderTheme) => void;
|
||||
}) {
|
||||
const style = data.metadata?.folder?.style || "glass";
|
||||
const theme = resolveCanvasFolderTheme(data.metadata?.folder?.theme);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [title, setTitle] = useState(data.title);
|
||||
const previewNodes = useMemo(() => childNodes.filter(canPreviewFolderNode).slice(0, 3), [childNodes]);
|
||||
const themeCover = data.metadata?.folder?.themeCover || DEFAULT_FOLDER_COVER;
|
||||
const themeCover = resolveCanvasFolderThemeCover(theme, data.metadata?.folder?.themeCover);
|
||||
const showAdd = style === "glass" || childNodes.length === 0;
|
||||
const folderMenu = {
|
||||
selectedKeys: [`style:${style}`, `theme:${theme}`],
|
||||
items: [
|
||||
{
|
||||
key: "styles",
|
||||
label: "文件夹样式",
|
||||
children: FOLDER_STYLE_OPTIONS.map((item) => ({ key: `style:${item.key}`, label: item.label })),
|
||||
},
|
||||
{
|
||||
key: "themes",
|
||||
label: "主题皮肤",
|
||||
children: CANVAS_FOLDER_THEME_OPTIONS.map((item) => ({ key: `theme:${item.key}`, label: item.label })),
|
||||
},
|
||||
],
|
||||
onClick: ({ key }: { key: string }) => {
|
||||
if (key.startsWith("style:")) onStyleChange(data.id, key.slice(6) as CanvasFolderStyle);
|
||||
if (key.startsWith("theme:")) onThemeChange(data.id, key.slice(6) as CanvasFolderTheme);
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => setTitle(data.title), [data.title]);
|
||||
|
||||
@@ -130,17 +151,13 @@ export const CanvasFolderPreview = React.memo(function CanvasFolderPreview({
|
||||
) : (
|
||||
<Dropdown
|
||||
trigger={["click"]}
|
||||
menu={{
|
||||
selectedKeys: [style],
|
||||
items: FOLDER_STYLE_OPTIONS.map((item) => ({ key: item.key, label: item.label })),
|
||||
onClick: ({ key }) => onStyleChange(data.id, key as CanvasFolderStyle),
|
||||
}}
|
||||
menu={folderMenu}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="canvas-folder-action canvas-folder-options"
|
||||
aria-label={`文件夹选项,当前 ${childNodes.length} 项`}
|
||||
title="切换文件夹样式"
|
||||
title="切换文件夹样式与主题"
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
@@ -153,17 +170,13 @@ export const CanvasFolderPreview = React.memo(function CanvasFolderPreview({
|
||||
{!readOnly && showAdd ? (
|
||||
<Dropdown
|
||||
trigger={["click"]}
|
||||
menu={{
|
||||
selectedKeys: [style],
|
||||
items: FOLDER_STYLE_OPTIONS.map((item) => ({ key: item.key, label: item.label })),
|
||||
onClick: ({ key }) => onStyleChange(data.id, key as CanvasFolderStyle),
|
||||
}}
|
||||
menu={folderMenu}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="canvas-folder-style-trigger"
|
||||
aria-label="切换文件夹样式"
|
||||
title="切换文件夹样式"
|
||||
aria-label="切换文件夹样式与主题"
|
||||
title="切换文件夹样式与主题"
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
|
||||
@@ -7,7 +7,7 @@ import { CanvasFolderPreview } from "@/components/canvas/canvas-folder-preview";
|
||||
import { FRAME_HEADER_HEIGHT, FRAME_PADDING, isCanvasFolderNode } from "@/lib/canvas/canvas-frame";
|
||||
import { canvasThemes, type CanvasTheme } from "@/lib/canvas-theme";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { CanvasNodeType, type CanvasFolderStyle, type CanvasNodeData, type Position } from "@/types/canvas";
|
||||
import { CanvasNodeType, type CanvasFolderStyle, type CanvasFolderTheme, type CanvasNodeData, type Position } from "@/types/canvas";
|
||||
|
||||
type ResizeCorner = "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
||||
|
||||
@@ -22,6 +22,7 @@ export const CanvasFrameNode = React.memo(function CanvasFrameNode({
|
||||
onResize,
|
||||
onToggleCollapsed,
|
||||
onFolderStyleChange,
|
||||
onFolderThemeChange = () => undefined,
|
||||
onTitleChange,
|
||||
onContextMenu,
|
||||
readOnly = false,
|
||||
@@ -38,6 +39,7 @@ export const CanvasFrameNode = React.memo(function CanvasFrameNode({
|
||||
onResize: (nodeId: string, width: number, height: number, position?: Position) => void;
|
||||
onToggleCollapsed: (nodeId: string) => void;
|
||||
onFolderStyleChange: (nodeId: string, style: CanvasFolderStyle) => void;
|
||||
onFolderThemeChange?: (nodeId: string, theme: CanvasFolderTheme) => void;
|
||||
onTitleChange: (nodeId: string, title: string) => void;
|
||||
onContextMenu: (event: ReactMouseEvent, nodeId: string) => void;
|
||||
readOnly?: boolean;
|
||||
@@ -194,6 +196,7 @@ export const CanvasFrameNode = React.memo(function CanvasFrameNode({
|
||||
onToggleCollapsed={onToggleCollapsed}
|
||||
onTitleChange={onTitleChange}
|
||||
onStyleChange={onFolderStyleChange}
|
||||
onThemeChange={onFolderThemeChange}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -255,6 +255,7 @@ export const CanvasResourceMentionTextarea = forwardRef<HTMLTextAreaElement, Pro
|
||||
replaceEditableSelection(event.clipboardData.getData("text/plain"));
|
||||
}}
|
||||
onKeyDown={(event: KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === "Enter" && (event.nativeEvent.isComposing || composingRef.current)) return;
|
||||
if (mention && candidates.length) {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
@@ -339,7 +340,16 @@ export const CanvasResourceMentionTextarea = forwardRef<HTMLTextAreaElement, Pro
|
||||
syncMention(next, event.target.selectionStart);
|
||||
reportContentSize(event.currentTarget);
|
||||
}}
|
||||
onCompositionStart={(event) => {
|
||||
composingRef.current = true;
|
||||
props.onCompositionStart?.(event);
|
||||
}}
|
||||
onCompositionEnd={(event) => {
|
||||
composingRef.current = false;
|
||||
props.onCompositionEnd?.(event);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && (event.nativeEvent.isComposing || composingRef.current)) return;
|
||||
if (mention && candidates.length) {
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -195,8 +195,8 @@ export function CanvasToolbar({
|
||||
|
||||
<AnimatePresence>
|
||||
{appearanceOpen ? (
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: aceternityMotion.duration.instant }} className="pointer-events-auto absolute bottom-[var(--canvas-dock-popover-offset)] z-[var(--dock-z-popover)] w-[224px] max-w-[calc(100vw-24px)] -translate-x-1/2" style={{ left: panelX || "50%" }}>
|
||||
<SpotlightSurface spotlightColor={theme.toolbar.itemHover} initial={{ y: 6, scale: 0.97 }} animate={{ y: 0, scale: 1 }} exit={{ y: 4, scale: 0.98 }} transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }} className="aceternity-floating-panel overflow-hidden rounded-[var(--panel-radius)] border p-2.5 backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.toolbar.item }} onWheel={(event) => event.stopPropagation()}>
|
||||
<motion.div initial={{ opacity: 0, scaleY: 0.9, y: 8 }} animate={{ opacity: 1, scaleY: 1, y: 0 }} exit={{ opacity: 0, scaleY: 0.92, y: 6 }} transition={{ duration: aceternityMotion.duration.panel, ease: aceternityMotion.easing.enter }} className="pointer-events-auto absolute bottom-[var(--canvas-dock-popover-offset)] z-[var(--dock-z-popover)] w-[224px] max-w-[calc(100vw-24px)]" style={{ left: panelX || "50%", transformOrigin: "bottom center", x: "-50%" }}>
|
||||
<SpotlightSurface spotlightColor={theme.toolbar.itemHover} initial={{ opacity: 0, scale: 0.97 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.97, transition: { duration: 0 } }} transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }} className="aceternity-floating-panel overflow-hidden rounded-[var(--panel-radius)] border p-2.5 backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.toolbar.item }} onWheel={(event) => event.stopPropagation()}>
|
||||
<PanelHeading icon={<Palette className="size-4" />} title="画布外观" subtitle="调整整个创作空间" theme={theme} />
|
||||
<div className="mt-3 text-[var(--fs-micro)] font-semibold uppercase opacity-45">主题模式</div>
|
||||
<div className="mt-1 grid grid-cols-2 gap-1 rounded-[var(--dock-item-radius-labeled)] border p-1" style={{ background: theme.spatial.surface, borderColor: theme.toolbar.border }}>
|
||||
@@ -234,8 +234,8 @@ function AddNodeMenu({ x, theme, commands }: {
|
||||
commands: CanvasCreateCommand[];
|
||||
}) {
|
||||
return (
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: aceternityMotion.duration.instant }} className="pointer-events-auto absolute bottom-[var(--canvas-dock-popover-offset)] z-[var(--dock-z-popover)] w-[260px] max-w-[calc(100vw-24px)] -translate-x-1/2" style={{ left: x || "50%" }}>
|
||||
<SpotlightSurface spotlightColor={theme.toolbar.itemHover} initial={{ y: 6, scale: 0.97 }} animate={{ y: 0, scale: 1 }} exit={{ y: 4, scale: 0.98 }} transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }} className="aceternity-floating-panel overflow-hidden rounded-[var(--panel-radius)] border p-2 backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text }} onWheel={(event) => event.stopPropagation()}>
|
||||
<motion.div initial={{ opacity: 0, scaleY: 0.9, y: 8 }} animate={{ opacity: 1, scaleY: 1, y: 0 }} exit={{ opacity: 0, scaleY: 0.92, y: 6 }} transition={{ duration: aceternityMotion.duration.panel, ease: aceternityMotion.easing.enter }} className="pointer-events-auto absolute bottom-[var(--canvas-dock-popover-offset)] z-[var(--dock-z-popover)] w-[260px] max-w-[calc(100vw-24px)]" style={{ left: x || "50%", transformOrigin: "bottom center", x: "-50%" }}>
|
||||
<SpotlightSurface spotlightColor={theme.toolbar.itemHover} initial={{ opacity: 0, scale: 0.97 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.97, transition: { duration: 0 } }} transition={{ duration: aceternityMotion.duration.instant, ease: aceternityMotion.easing.enter }} className="aceternity-floating-panel overflow-hidden rounded-[var(--panel-radius)] border p-2 backdrop-blur-2xl" style={{ background: theme.spatial.elevated, borderColor: theme.toolbar.border, color: theme.node.text }} onWheel={(event) => event.stopPropagation()}>
|
||||
<CanvasCreateMenu commands={commands} />
|
||||
</SpotlightSurface>
|
||||
</motion.div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "antd";
|
||||
|
||||
import { VideoSettingsPanel, videoResolutionLabel, videoSecondsLabel, videoSizeLabel } from "@/components/video-settings-panel";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { modelCapabilityConfigFor } from "@/lib/model-capabilities";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
|
||||
@@ -21,7 +22,12 @@ export function CanvasVideoSettingsPopover({ config, onConfigChange, buttonClass
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [buttonRect, setButtonRect] = useState<DOMRect | null>(null);
|
||||
const summary = `${videoResolutionLabel(config.vquality)} · ${videoSizeLabel(config.size)} · ${videoSecondsLabel(config.videoSeconds)}`;
|
||||
const resolutionSupported = Boolean(modelCapabilityConfigFor(config, config.model).video?.resolutions.length);
|
||||
const summary = [
|
||||
...(resolutionSupported ? [videoResolutionLabel(config.vquality)] : []),
|
||||
videoSizeLabel(config.size),
|
||||
videoSecondsLabel(config.videoSeconds),
|
||||
].join(" · ");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
Reference in New Issue
Block a user