diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx
index cb4b45db9..fce8c581e 100644
--- a/app/(admin)/settings/admin-settings-client.tsx
+++ b/app/(admin)/settings/admin-settings-client.tsx
@@ -192,6 +192,8 @@ import {
type CanvasToolbarConfig,
type CanvasToolbarItemKey,
DEFAULT_CANVAS_TOOLBAR_ITEMS,
+ MAX_SELECTED_IMAGE_INLINE_ACTION_COUNT,
+ MIN_SELECTED_IMAGE_INLINE_ACTION_COUNT,
defaultCanvasToolbarConfig,
getDefaultCanvasToolbarItem,
normalizeCanvasToolbarConfig,
@@ -3691,6 +3693,7 @@ export function AdminSettingsClient() {
return {
...prev,
canvasToolbarConfig: {
+ ...normalized,
items: {
...normalized.items,
[key]: {
@@ -12468,6 +12471,54 @@ export function AdminSettingsClient() {
恢复全部默认
+
+
+
+
+ 控制画布中选中图片后,快捷弹窗主条直接显示几个功能按钮;超出的功能自动收进“更多”菜单。
+
+
+
{
+ const normalized =
+ normalizeCanvasToolbarConfig(
+ siteSettings.canvasToolbarConfig
+ );
+ const parsed = Number(event.target.value);
+ const nextValue = Number.isFinite(parsed)
+ ? Math.min(
+ MAX_SELECTED_IMAGE_INLINE_ACTION_COUNT,
+ Math.max(
+ MIN_SELECTED_IMAGE_INLINE_ACTION_COUNT,
+ Math.round(parsed)
+ )
+ )
+ : normalized.selectedImageInlineActionCount;
+ setSiteSettings({
+ ...siteSettings,
+ canvasToolbarConfig: {
+ ...normalized,
+ selectedImageInlineActionCount: nextValue,
+ },
+ });
+ }}
+ type="number"
+ value={
+ normalizeCanvasToolbarConfig(
+ siteSettings.canvasToolbarConfig
+ ).selectedImageInlineActionCount
+ }
+ />
+
{Array.from(
new Set(
DEFAULT_CANVAS_TOOLBAR_ITEMS.map(
diff --git a/app/api/site-settings/route.ts b/app/api/site-settings/route.ts
index 7d9bbcb5b..9e50c0c46 100644
--- a/app/api/site-settings/route.ts
+++ b/app/api/site-settings/route.ts
@@ -1894,11 +1894,13 @@ const updateSchema = z.object({
.optional(),
canvasToolbarConfig: z
.object({
+ selectedImageInlineActionCount: z.number().int().min(3).max(12).optional(),
items: z
.record(
z.object({
label: z.string().trim().max(40).optional(),
iconUrl: z.string().trim().max(2000).optional(),
+ enabled: z.boolean().optional(),
})
)
.optional(),
diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx
index 962825fd0..b8e23c9fa 100644
--- a/components/canvas/nodes/ImageNode.tsx
+++ b/components/canvas/nodes/ImageNode.tsx
@@ -170,6 +170,7 @@ import {
getScaledNodeContentStyle,
} from "@/lib/canvas/node-content-scale";
import {
+ getSelectedImageInlineActionCount,
getCanvasToolbarItem,
isCanvasToolbarItemEnabled,
type CanvasToolbarConfig,
@@ -1180,6 +1181,47 @@ export function ImageNode({
);
const imageLayerSplitEnabled =
isLayerSplitEnabled && imageLayerSplitEnabledByToolbar;
+ const selectedImageInlineActionCount =
+ getSelectedImageInlineActionCount(canvasToolbarConfig);
+ const selectedImageToolbarActionCandidates = [
+ { key: "pickObject", enabled: imagePickObjectEnabled },
+ { key: "enhance", enabled: imageEnhanceEnabled },
+ { key: "removeBackground", enabled: imageRemoveBackgroundEnabled },
+ { key: "aiWashQuick", enabled: imageAiWashQuickEnabled },
+ { key: "textEdit", enabled: imageTextEditEnabled },
+ { key: "boxCutout", enabled: imageBoxCutoutEnabled },
+ { key: "localEdit", enabled: imageLocalEditEnabled },
+ { key: "removeWatermark", enabled: imageRemoveWatermarkEnabled },
+ { key: "resize", enabled: imageResizeEnabled },
+ { key: "multiAngle", enabled: imageMultiAngleEnabled },
+ { key: "generate3dModel", enabled: imageGenerate3dModelEnabled },
+ { key: "generate3dWorld", enabled: imageGenerate3dWorldEnabled },
+ { key: "layerSplit", enabled: imageLayerSplitEnabled },
+ { key: "extend", enabled: imageExtendEnabled },
+ { key: "crop", enabled: imageCropEnabled },
+ { key: "adjust", enabled: imageAdjustEnabled },
+ { key: "flipRotate", enabled: imageFlipRotateEnabled },
+ { key: "promptRegenerate", enabled: imagePromptRegenerateEnabled },
+ { key: "note", enabled: imageNoteEnabled },
+ ] as const;
+ type SelectedImageToolbarActionKey =
+ (typeof selectedImageToolbarActionCandidates)[number]["key"];
+ const enabledSelectedImageToolbarActions =
+ selectedImageToolbarActionCandidates.filter((action) => action.enabled);
+ const selectedImageInlineActionKeys = new Set(
+ enabledSelectedImageToolbarActions
+ .slice(0, selectedImageInlineActionCount)
+ .map((action) => action.key)
+ );
+ const selectedImageMoreActionKeys = new Set(
+ enabledSelectedImageToolbarActions
+ .slice(selectedImageInlineActionCount)
+ .map((action) => action.key)
+ );
+ const isSelectedImageActionInline = (key: SelectedImageToolbarActionKey) =>
+ selectedImageInlineActionKeys.has(key);
+ const isSelectedImageActionInMore = (key: SelectedImageToolbarActionKey) =>
+ selectedImageMoreActionKeys.has(key);
const hasHoverImageMorePrimaryActions =
imageRemoveBackgroundEnabled ||
imageRemoveWatermarkEnabled ||
@@ -1201,29 +1243,12 @@ export function ImageNode({
imageNodeMoreEnabled &&
(hasHoverImageMorePrimaryActions || hasHoverImageMoreSecondaryActions);
const hasSelectedImageMorePrimaryActions =
- imageBoxCutoutEnabled ||
- imageLocalEditEnabled ||
- imageRemoveWatermarkEnabled ||
- imageResizeEnabled ||
- imageMultiAngleEnabled ||
- imageGenerate3dModelEnabled ||
- imageGenerate3dWorldEnabled ||
- imageLayerSplitEnabled ||
- imageExtendEnabled ||
- imageCropEnabled ||
- imageAdjustEnabled ||
- imageFlipRotateEnabled ||
- imagePromptRegenerateEnabled ||
- imageNoteEnabled;
+ selectedImageMoreActionKeys.size > 0;
const hasSelectedImageMoreMenu =
hasSelectedImageMorePrimaryActions ||
(imageNodeMoreEnabled && imageDeleteEnabled);
const hasSelectedImagePrimaryToolbarActions =
- imagePickObjectEnabled ||
- imageEnhanceEnabled ||
- imageRemoveBackgroundEnabled ||
- imageTextEditEnabled ||
- imageAiWashQuickEnabled;
+ selectedImageInlineActionKeys.size > 0;
const hasSelectedImageSecondaryToolbarActions =
hasSelectedImageMoreMenu || imageDownloadEnabled;
const contentScale = useMemo(
@@ -8648,7 +8673,7 @@ export function ImageNode({
typeof document !== "undefined" &&
createPortal(
event.stopPropagation()}
onMouseDown={(event) => event.stopPropagation()}
@@ -8659,26 +8684,24 @@ export function ImageNode({
width: aiWashQuickPanelPosition.width,
maxHeight: aiWashQuickPanelPosition.maxHeight,
overflowY: "auto",
- scrollbarWidth: "thin",
+ scrollbarWidth: "none",
}}
>
-
-
-
+
+
+
-
-
- {aiWashQuickModeLabel}
-
+
+ {aiWashQuickModeLabel}
{aiWashConfigLoading ? (
-
+
正在加载 AI洗图 配置...
) : null}
-
-
-
-
-
+
+
+
+
+
目标
-
+
{getAiWashStatusLabel(aiWashQuickStatus)}
@@ -8712,7 +8735,7 @@ export function ImageNode({
}}
value={selectedAiWashQuickPreset?.id || aiWashQuickPresetId}
>
-
+
@@ -8724,26 +8747,26 @@ export function ImageNode({
{selectedAiWashQuickPreset?.description ? (
-
+
{selectedAiWashQuickPreset.description}
) : null}
-
-
+
+
参数
{shouldShowAiWashQuickModelSelector ? (
-
-
) : selectedAiWashQuickModel ? (
-
+
{selectedAiWashQuickModel.displayName}
{selectedAiWashQuickModel.unitCost > 0
? ` · ${selectedAiWashQuickModel.unitCost} 积分/张`
@@ -8788,7 +8811,7 @@ export function ImageNode({
) : null}
-
+
{aiWashQuickControls.map((control) => {
const selectedIndex =
aiWashQuickLevels[control.id] ?? control.defaultLevel;
@@ -8798,14 +8821,14 @@ export function ImageNode({
null;
return (
-
-
+
+
{control.label}
-
+
{activeLevel?.label || selectedIndex + 1}
@@ -8821,8 +8844,8 @@ export function ImageNode({
}
rangeClassName="bg-slate-950 dark:bg-white"
step={1}
- thumbClassName="h-3 w-3 border border-slate-300 bg-white shadow-[0_0_0_3px_rgba(15,23,42,0.06)]"
- trackClassName="h-1 bg-slate-200 dark:bg-white/14"
+ thumbClassName="h-3.5 w-3.5 border border-slate-300 bg-white shadow-[0_0_0_4px_rgba(15,23,42,0.06)]"
+ trackClassName="h-1.5 bg-slate-200 dark:bg-white/14"
value={[selectedIndex]}
/>
@@ -8831,8 +8854,9 @@ export function ImageNode({
+
,
document.body
@@ -8921,7 +8946,7 @@ export function ImageNode({
) : null}
- {imagePickObjectEnabled ? (
+ {isSelectedImageActionInline("pickObject") ? (