fix: polish psd editor and settings layout
This commit is contained in:
parent
ab0dc8bce3
commit
a90e28d115
@ -3139,7 +3139,7 @@ export function AgentWorkflowSettingsClient({
|
||||
variants={containerVariants}
|
||||
className="grid gap-6 lg:grid-cols-2"
|
||||
>
|
||||
<motion.div variants={itemVariants}>
|
||||
<motion.div variants={itemVariants} className="lg:col-span-2">
|
||||
<Card>
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
@ -21,7 +21,6 @@ import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@ -107,6 +106,63 @@ const PREVIEW_STAGE_TARGET_HEIGHT = 760;
|
||||
const MIN_IMAGE_SCALE = 0.4;
|
||||
const MAX_IMAGE_SCALE = 3;
|
||||
|
||||
function getTemplateMetaLabel(
|
||||
template: AgentPsdTemplate,
|
||||
showSecondaryCategory: boolean
|
||||
) {
|
||||
const metaParts = [
|
||||
`${template.width} x ${template.height}`,
|
||||
showSecondaryCategory ? String(template.secondaryCategory || "").trim() : "",
|
||||
].filter(Boolean);
|
||||
return metaParts.join(" · ");
|
||||
}
|
||||
|
||||
function renderTemplateSelectContent(
|
||||
template: AgentPsdTemplate,
|
||||
params?: {
|
||||
showSecondaryCategory?: boolean;
|
||||
compact?: boolean;
|
||||
}
|
||||
) {
|
||||
const showSecondaryCategory = params?.showSecondaryCategory === true;
|
||||
const compact = params?.compact === true;
|
||||
const metaLabel = getTemplateMetaLabel(template, showSecondaryCategory);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
compact
|
||||
? "grid min-w-0 flex-1 grid-cols-[48px_minmax(0,1fr)] items-center gap-3"
|
||||
: "grid min-w-0 flex-1 grid-cols-[56px_minmax(0,1fr)] items-center gap-3"
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
compact
|
||||
? "flex h-12 w-12 shrink-0 overflow-hidden rounded-xl border border-white/10 bg-white/5"
|
||||
: "flex h-14 w-14 shrink-0 overflow-hidden rounded-xl border border-white/10 bg-white/5"
|
||||
}
|
||||
>
|
||||
<img
|
||||
src={template.previewUrl}
|
||||
alt={template.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 text-left">
|
||||
<div className={compact ? "truncate text-sm font-medium leading-tight" : "truncate text-base font-semibold leading-tight"}>
|
||||
{template.name}
|
||||
</div>
|
||||
{metaLabel ? (
|
||||
<div className={compact ? "mt-1 truncate text-[11px] text-white/45" : "mt-1 truncate text-xs text-white/45"}>
|
||||
{metaLabel}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function clampImageScale(value: number) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return 1;
|
||||
@ -507,10 +563,6 @@ export function AgentPsdTemplateEditorDialog({
|
||||
),
|
||||
[selectedTemplateLayers]
|
||||
);
|
||||
const fixedLayerCount = useMemo(() => {
|
||||
if (!selectedTemplate) return 0;
|
||||
return selectedTemplateLayers.filter((layer) => !layer.isEditable).length;
|
||||
}, [selectedTemplate, selectedTemplateLayers]);
|
||||
const orderedPreviewLayers = useMemo(
|
||||
() => [...selectedTemplateLayers].reverse(),
|
||||
[selectedTemplateLayers]
|
||||
@ -617,6 +669,81 @@ export function AgentPsdTemplateEditorDialog({
|
||||
const selectedTextColor = selectedTextLayer
|
||||
? effectiveTextColorAssignments[selectedTextLayer.id] || selectedTextLayer.textColor
|
||||
: "#111111";
|
||||
const selectedImageAdjustmentPanel =
|
||||
selectedImageLayer && effectiveImageAssignments[selectedImageLayer.id] ? (
|
||||
<div className="space-y-3 rounded-2xl border border-white/10 bg-white/[0.03] p-3.5">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Move className="h-3.5 w-3.5" />
|
||||
选中画布中的图片后可直接拖动位置
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<ZoomIn className="h-3.5 w-3.5" />
|
||||
缩放
|
||||
</div>
|
||||
<span className="rounded-full border border-emerald-300/35 bg-emerald-500/15 px-2 py-0.5 font-medium text-emerald-100">
|
||||
{Math.round(selectedImagePlacement.scale * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/12 bg-black/45 p-2.5">
|
||||
<div className="grid grid-cols-[34px_minmax(0,1fr)_34px] items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full border border-white/18 bg-white/[0.04] text-white transition-colors hover:bg-white/[0.12]"
|
||||
onClick={() => nudgeImageScale(selectedImageLayer.id, -0.05)}
|
||||
aria-label="缩小图片"
|
||||
title="缩小"
|
||||
>
|
||||
<ZoomOut className="h-4 w-4" />
|
||||
</button>
|
||||
<Slider
|
||||
min={MIN_IMAGE_SCALE}
|
||||
max={MAX_IMAGE_SCALE}
|
||||
step={0.01}
|
||||
value={[selectedImagePlacement.scale]}
|
||||
className="py-1"
|
||||
trackClassName="h-2.5 rounded-full border border-white/20 bg-[#0f1014]"
|
||||
rangeClassName="bg-gradient-to-r from-emerald-300 via-green-400 to-teal-400 shadow-[0_0_12px_rgba(16,185,129,0.38)]"
|
||||
thumbClassName="h-5 w-5 border-2 border-emerald-100 bg-[#0a0a0d] shadow-[0_0_0_5px_rgba(16,185,129,0.2)]"
|
||||
onValueChange={(value) => {
|
||||
const newScale = clampImageScale(value[0] || 1);
|
||||
updateImagePlacement(selectedImageLayer.id, (current) => ({
|
||||
...current,
|
||||
scale: newScale,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full border border-white/18 bg-white/[0.04] text-white transition-colors hover:bg-white/[0.12]"
|
||||
onClick={() => nudgeImageScale(selectedImageLayer.id, 0.05)}
|
||||
aria-label="放大图片"
|
||||
title="放大"
|
||||
>
|
||||
<ZoomIn className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between px-0.5 text-[10px] text-white/45">
|
||||
<span>40%</span>
|
||||
<span className="text-emerald-200">100%</span>
|
||||
<span>300%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="px-2 text-white/70 hover:text-white"
|
||||
onClick={() => resetImagePlacement(selectedImageLayer.id)}
|
||||
>
|
||||
重置位置与缩放
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
const fontFaceCss = useMemo(() => buildPreviewFontFaces(selectedTemplate), [selectedTemplate]);
|
||||
const selectedTemplateSelectValue = selectedTemplate?.id;
|
||||
const hasTemplateOptions = templates.length > 0;
|
||||
@ -1027,28 +1154,13 @@ export function AgentPsdTemplateEditorDialog({
|
||||
<Label>选择模板</Label>
|
||||
{hasTemplateOptions ? (
|
||||
<Select value={selectedTemplateSelectValue} onValueChange={setSelectedTemplateId}>
|
||||
<SelectTrigger className="h-[68px] items-center">
|
||||
<SelectTrigger className="h-auto min-h-[84px] items-center px-3 py-3 [&>span]:contents">
|
||||
{selectedTemplate ? (
|
||||
<span className="flex min-w-0 items-center gap-3">
|
||||
<span className="flex h-12 w-12 shrink-0 overflow-hidden rounded-xl border border-white/10 bg-white/5">
|
||||
<img
|
||||
src={selectedTemplate.previewUrl}
|
||||
alt={selectedTemplate.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</span>
|
||||
<span className="flex min-w-0 flex-col justify-center text-left leading-tight">
|
||||
{shouldGroupTemplatesBySecondaryCategory &&
|
||||
selectedTemplate.secondaryCategory ? (
|
||||
<span className="block truncate text-[11px] text-white/50">
|
||||
{selectedTemplate.secondaryCategory}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="block truncate text-base font-medium">
|
||||
{selectedTemplate.name}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<div className="min-w-0 flex-1 pr-3">
|
||||
{renderTemplateSelectContent(selectedTemplate, {
|
||||
showSecondaryCategory: shouldGroupTemplatesBySecondaryCategory,
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<SelectValue placeholder={isLoading ? "加载模板中..." : "请选择模板"} />
|
||||
)}
|
||||
@ -1064,18 +1176,10 @@ export function AgentPsdTemplateEditorDialog({
|
||||
value={template.id}
|
||||
className="py-2.5 pr-3"
|
||||
>
|
||||
<span className="flex items-center gap-3">
|
||||
<span className="flex h-12 w-12 shrink-0 overflow-hidden rounded-lg border border-black/10 bg-black/5">
|
||||
<img
|
||||
src={template.previewUrl}
|
||||
alt={template.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-sm font-medium">
|
||||
{template.name}
|
||||
</span>
|
||||
</span>
|
||||
{renderTemplateSelectContent(template, {
|
||||
showSecondaryCategory: shouldGroupTemplatesBySecondaryCategory,
|
||||
compact: true,
|
||||
})}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
@ -1086,18 +1190,7 @@ export function AgentPsdTemplateEditorDialog({
|
||||
value={template.id}
|
||||
className="py-2.5 pr-3"
|
||||
>
|
||||
<span className="flex items-center gap-3">
|
||||
<span className="flex h-12 w-12 shrink-0 overflow-hidden rounded-lg border border-black/10 bg-black/5">
|
||||
<img
|
||||
src={template.previewUrl}
|
||||
alt={template.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
</span>
|
||||
<span className="min-w-0 truncate text-sm font-medium">
|
||||
{template.name}
|
||||
</span>
|
||||
</span>
|
||||
{renderTemplateSelectContent(template, { compact: true })}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@ -1129,18 +1222,6 @@ export function AgentPsdTemplateEditorDialog({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge variant="secondary">{selectedTemplate.brandName}</Badge>
|
||||
<Badge variant="secondary">
|
||||
{selectedTemplate.width} x {selectedTemplate.height}
|
||||
</Badge>
|
||||
<Badge variant="outline">{editableImageLayers.length} 个可替换图片</Badge>
|
||||
<Badge variant="outline">{editableTextLayers.length} 个可编辑文字</Badge>
|
||||
{fixedLayerCount > 0 ? (
|
||||
<Badge variant="outline">{fixedLayerCount} 个固定图层</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleRender}
|
||||
disabled={isRendering}
|
||||
@ -1153,6 +1234,8 @@ export function AgentPsdTemplateEditorDialog({
|
||||
)}
|
||||
应用模板并导出
|
||||
</Button>
|
||||
|
||||
{selectedImageAdjustmentPanel}
|
||||
</>
|
||||
) : (
|
||||
<div className="rounded-2xl border border-dashed border-white/15 p-5 text-sm text-muted-foreground">
|
||||
@ -1606,144 +1689,78 @@ export function AgentPsdTemplateEditorDialog({
|
||||
<div className="flex min-h-0 flex-1 flex-col rounded-2xl border border-white/10 bg-white/[0.02] p-3.5 xl:p-4">
|
||||
{selectedImageLayer ? (
|
||||
<div className="flex h-full flex-col gap-3">
|
||||
{effectiveImageAssignments[selectedImageLayer.id] ? (
|
||||
<div className="space-y-3 rounded-xl border border-white/10 bg-white/[0.03] p-3">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Move className="h-3.5 w-3.5" />
|
||||
选中画布中的图片后可直接拖动位置
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<ZoomIn className="h-3.5 w-3.5" />
|
||||
缩放
|
||||
</div>
|
||||
<span className="rounded-full border border-emerald-300/35 bg-emerald-500/15 px-2 py-0.5 font-medium text-emerald-100">
|
||||
{Math.round(selectedImagePlacement.scale * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="rounded-xl border border-white/12 bg-black/45 p-2.5">
|
||||
<div className="grid grid-cols-[34px_minmax(0,1fr)_34px] items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full border border-white/18 bg-white/[0.04] text-white transition-colors hover:bg-white/[0.12]"
|
||||
onClick={() => nudgeImageScale(selectedImageLayer.id, -0.05)}
|
||||
aria-label="缩小图片"
|
||||
title="缩小"
|
||||
>
|
||||
<ZoomOut className="h-4 w-4" />
|
||||
</button>
|
||||
<Slider
|
||||
min={MIN_IMAGE_SCALE}
|
||||
max={MAX_IMAGE_SCALE}
|
||||
step={0.01}
|
||||
value={[selectedImagePlacement.scale]}
|
||||
className="py-1"
|
||||
trackClassName="h-2.5 rounded-full border border-white/20 bg-[#0f1014]"
|
||||
rangeClassName="bg-gradient-to-r from-emerald-300 via-green-400 to-teal-400 shadow-[0_0_12px_rgba(16,185,129,0.38)]"
|
||||
thumbClassName="h-5 w-5 border-2 border-emerald-100 bg-[#0a0a0d] shadow-[0_0_0_5px_rgba(16,185,129,0.2)]"
|
||||
onValueChange={(value) => {
|
||||
const newScale = clampImageScale(value[0] || 1);
|
||||
updateImagePlacement(selectedImageLayer.id, (current) => ({
|
||||
...current,
|
||||
scale: newScale,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-8 w-8 items-center justify-center rounded-full border border-white/18 bg-white/[0.04] text-white transition-colors hover:bg-white/[0.12]"
|
||||
onClick={() => nudgeImageScale(selectedImageLayer.id, 0.05)}
|
||||
aria-label="放大图片"
|
||||
title="放大"
|
||||
>
|
||||
<ZoomIn className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between px-0.5 text-[10px] text-white/45">
|
||||
<span>40%</span>
|
||||
<span className="text-emerald-200">100%</span>
|
||||
<span>300%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="rounded-xl border border-white/10 bg-white/[0.02] p-3">
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
{sourceImageUrl ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => resetImagePlacement(selectedImageLayer.id)}
|
||||
className="h-10 justify-center"
|
||||
onClick={() => {
|
||||
setImageAssignments((current) => ({
|
||||
...current,
|
||||
[selectedImageLayer.id]: sourceImageUrl,
|
||||
}));
|
||||
setImagePlacements((current) => ({
|
||||
...current,
|
||||
[selectedImageLayer.id]: getDefaultAgentPsdImagePlacement(),
|
||||
}));
|
||||
setImageSizes((current) => {
|
||||
const next = { ...current };
|
||||
delete next[selectedImageLayer.id];
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
>
|
||||
重置位置与缩放
|
||||
使用当前图片
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mt-auto flex flex-wrap gap-2">
|
||||
{sourceImageUrl ? (
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setImageAssignments((current) => ({
|
||||
...current,
|
||||
[selectedImageLayer.id]: sourceImageUrl,
|
||||
}));
|
||||
setImagePlacements((current) => ({
|
||||
...current,
|
||||
[selectedImageLayer.id]: getDefaultAgentPsdImagePlacement(),
|
||||
}));
|
||||
setImageSizes((current) => {
|
||||
const next = { ...current };
|
||||
delete next[selectedImageLayer.id];
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
className="h-10 justify-center"
|
||||
disabled={uploadingSlotId === selectedImageLayer.id}
|
||||
onClick={() => openImageUpload(selectedImageLayer.id)}
|
||||
>
|
||||
使用当前图片
|
||||
{uploadingSlotId === selectedImageLayer.id ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
上传替换图片
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={uploadingSlotId === selectedImageLayer.id}
|
||||
onClick={() => openImageUpload(selectedImageLayer.id)}
|
||||
>
|
||||
{uploadingSlotId === selectedImageLayer.id ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Upload className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
上传替换图片
|
||||
</Button>
|
||||
{effectiveImageAssignments[selectedImageLayer.id] ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setImageAssignments((current) => {
|
||||
const nextAssignments = { ...current };
|
||||
delete nextAssignments[selectedImageLayer.id];
|
||||
return nextAssignments;
|
||||
});
|
||||
setImagePlacements((current) => {
|
||||
const next = { ...current };
|
||||
delete next[selectedImageLayer.id];
|
||||
return next;
|
||||
});
|
||||
setImageSizes((current) => {
|
||||
const next = { ...current };
|
||||
delete next[selectedImageLayer.id];
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
>
|
||||
清空
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-end gap-2">
|
||||
{effectiveImageAssignments[selectedImageLayer.id] ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="px-2 text-white/70 hover:text-white"
|
||||
onClick={() => {
|
||||
setImageAssignments((current) => {
|
||||
const nextAssignments = { ...current };
|
||||
delete nextAssignments[selectedImageLayer.id];
|
||||
return nextAssignments;
|
||||
});
|
||||
setImagePlacements((current) => {
|
||||
const next = { ...current };
|
||||
delete next[selectedImageLayer.id];
|
||||
return next;
|
||||
});
|
||||
setImageSizes((current) => {
|
||||
const next = { ...current };
|
||||
delete next[selectedImageLayer.id];
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
>
|
||||
清空当前替换图
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : selectedTextLayer ? (
|
||||
|
||||
@ -54,6 +54,8 @@ export function SearchableSelect({
|
||||
className={cn(
|
||||
"h-10 w-full justify-between px-3 text-left font-normal",
|
||||
disabled && "cursor-not-allowed opacity-60",
|
||||
!isDark &&
|
||||
"border-slate-200 bg-white text-slate-900 shadow-sm hover:bg-slate-50 hover:text-slate-900",
|
||||
isDark &&
|
||||
"h-12 rounded-2xl border-white/14 bg-white/[0.05] text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] hover:bg-white/[0.08] hover:text-white"
|
||||
)}
|
||||
@ -82,6 +84,7 @@ export function SearchableSelect({
|
||||
align="start"
|
||||
className={cn(
|
||||
"w-[var(--radix-popover-trigger-width)] p-3",
|
||||
!isDark && "border-slate-200 bg-white text-slate-900 shadow-xl",
|
||||
isDark && "border-white/12 bg-[#0c0f11]/98 text-white backdrop-blur-xl"
|
||||
)}
|
||||
>
|
||||
@ -93,6 +96,8 @@ export function SearchableSelect({
|
||||
placeholder={searchPlaceholder}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
!isDark &&
|
||||
"border-slate-200 bg-white text-slate-900 placeholder:text-slate-400 focus-visible:ring-slate-300/80 focus-visible:ring-offset-0",
|
||||
isDark &&
|
||||
"border-white/12 bg-white/[0.04] text-white placeholder:text-white/28 focus-visible:ring-emerald-400/30 focus-visible:ring-offset-0"
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user