fix: improve PSD template management and clipping masks

This commit is contained in:
Your Name 2026-06-01 19:40:22 +08:00
parent db1fc8c3df
commit 5461cff1db
4 changed files with 95 additions and 10 deletions

View File

@ -95,6 +95,7 @@ const SYSTEM_FONT_OPTIONS = [
const EMPTY_CATEGORY_SELECT_VALUE = "__none__";
const EMPTY_BRAND_SELECT_VALUE = "__unassigned__";
const ALL_TEMPLATE_BRANDS_SELECT_VALUE = "__all_template_brands__";
const UNASSIGNED_BRAND_LABEL = "未分类";
const MAX_PSD_TEMPLATE_FILE_SIZE = 80 * 1024 * 1024;
const PSD_TEMPLATE_MODE_OPTIONS: Array<{
@ -366,6 +367,7 @@ export function PsdTemplatesSettingsClient({
const [templateBrandDrafts, setTemplateBrandDrafts] = useState<Record<string, string>>({});
const [fontBrandDrafts, setFontBrandDrafts] = useState<Record<string, string>>({});
const [parsingTemplateIds, setParsingTemplateIds] = useState<string[]>([]);
const [templateBrandFilter, setTemplateBrandFilter] = useState(ALL_TEMPLATE_BRANDS_SELECT_VALUE);
const selectedTemplate = useMemo(
() => templates.find((template) => template.id === selectedTemplateId) || null,
@ -444,9 +446,42 @@ export function PsdTemplatesSettingsClient({
const currentBrand = normalizeManagedBrandName(draftTemplate?.brandName);
return Array.from(new Set([...managedBrandOptions, ...(currentBrand ? [currentBrand] : [])]));
}, [draftTemplate?.brandName, managedBrandOptions]);
const templateBrandFilterOptions = useMemo(() => {
const brands = new Set<string>();
let hasUnassigned = false;
templates.forEach((template) => {
const brand = normalizeManagedBrandName(template.brandName);
if (brand) {
brands.add(brand);
} else {
hasUnassigned = true;
}
});
return {
brands: Array.from(brands).sort((left, right) => left.localeCompare(right, "zh-CN")),
hasUnassigned,
};
}, [templates]);
const filteredTemplates = useMemo(() => {
if (templateBrandFilter === ALL_TEMPLATE_BRANDS_SELECT_VALUE) {
return templates;
}
if (templateBrandFilter === EMPTY_BRAND_SELECT_VALUE) {
return templates.filter((template) => !normalizeManagedBrandName(template.brandName));
}
return templates.filter(
(template) =>
normalizeManagedBrandName(template.brandName).toLowerCase() ===
templateBrandFilter.toLowerCase()
);
}, [templateBrandFilter, templates]);
const groupedTemplates = useMemo(() => {
const grouped = new Map<string, AgentPsdTemplate[]>();
templates.forEach((template) => {
filteredTemplates.forEach((template) => {
const label = normalizeManagedBrandName(template.brandName) || UNASSIGNED_BRAND_LABEL;
const current = grouped.get(label) || [];
current.push(template);
@ -463,7 +498,7 @@ export function PsdTemplatesSettingsClient({
label,
items,
}));
}, [templates]);
}, [filteredTemplates]);
const groupedFonts = useMemo(() => {
const grouped = new Map<string, AgentPsdFont[]>();
fonts.forEach((font) => {
@ -2960,9 +2995,44 @@ export function PsdTemplatesSettingsClient({
<Card>
<CardHeader>
<CardTitle className="text-base">模板列表</CardTitle>
<CardDescription>按品牌查看模板,选择一个模板继续配置槽位。</CardDescription>
<CardDescription>按品牌筛选模板,选择一个模板继续配置槽位。</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex flex-col gap-3 rounded-2xl border bg-muted/20 p-3 lg:flex-row lg:items-center lg:justify-between">
<div className="space-y-1">
<div className="text-sm font-medium">品牌筛选</div>
<div className="text-xs text-muted-foreground">
当前显示 {filteredTemplates.length} / {templates.length} 个模板
</div>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Select value={templateBrandFilter} onValueChange={setTemplateBrandFilter}>
<SelectTrigger className="w-full sm:w-[260px]">
<SelectValue placeholder="选择品牌" />
</SelectTrigger>
<SelectContent>
<SelectItem value={ALL_TEMPLATE_BRANDS_SELECT_VALUE}>全部品牌</SelectItem>
{templateBrandFilterOptions.hasUnassigned ? (
<SelectItem value={EMPTY_BRAND_SELECT_VALUE}>未分类</SelectItem>
) : null}
{templateBrandFilterOptions.brands.map((brand) => (
<SelectItem key={`template-brand-filter-${brand}`} value={brand}>
{brand}
</SelectItem>
))}
</SelectContent>
</Select>
{templateBrandFilter !== ALL_TEMPLATE_BRANDS_SELECT_VALUE ? (
<Button
type="button"
variant="outline"
onClick={() => setTemplateBrandFilter(ALL_TEMPLATE_BRANDS_SELECT_VALUE)}
>
清除筛选
</Button>
) : null}
</div>
</div>
{templateListErrorMessage && templates.length > 0 ? (
<div className="flex items-start gap-3 rounded-xl border border-amber-300/30 bg-amber-500/5 p-4 text-sm text-amber-100">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
@ -3008,6 +3078,10 @@ export function PsdTemplatesSettingsClient({
<div className="rounded-xl border border-dashed p-4 text-sm text-muted-foreground">
还没有 PSD 模板,先上传一个模板开始配置。
</div>
) : filteredTemplates.length === 0 ? (
<div className="rounded-xl border border-dashed p-4 text-sm text-muted-foreground">
当前品牌下没有 PSD 模板,可以切换品牌或清除筛选。
</div>
) : (
groupedTemplates.map((group) => (
<div key={group.label} className="space-y-3 rounded-2xl border bg-background/50 p-3">

View File

@ -391,6 +391,20 @@ function buildPsdLayerPreviewUrl(
});
}
function buildPsdLayerMaskUrl(maskUrl: string, layerWidth: number) {
const normalizedUrl = String(maskUrl || "").trim();
if (!normalizedUrl) {
return "";
}
return buildImageProxyUrl(normalizedUrl, {
width: Math.max(96, Math.min(1280, Math.round(layerWidth || 720))),
quality: 100,
format: "png",
delivery: "proxy",
});
}
function getPsdLayerPreviewSourceUrl(
layer: Pick<AgentPsdTemplateLayerSlot, "sourceUrl" | "previewUrl">
) {
@ -1482,10 +1496,7 @@ function getAssignedImageMaskStyle(
const normalizedMaskUrl = String(layer.maskUrl || fallbackMaskUrl || "").trim();
const hasMask = Boolean(normalizedMaskUrl);
const maskImageUrl = hasMask
? `url(${buildPsdLayerPreviewUrl(normalizedMaskUrl, {
width: layer.width,
quality: 72,
})})`
? `url(${buildPsdLayerMaskUrl(normalizedMaskUrl, layer.width)})`
: undefined;
return {
WebkitMaskImage: maskImageUrl,

View File

@ -271,7 +271,7 @@ export function TopNavbar({
};
const navContent = (
<nav className="fixed top-0 bottom-0 left-0 z-[100] flex w-16 flex-col items-center border-gray-200 border-r bg-white/90 text-gray-900 shadow-sm backdrop-blur-sm dark:border-white/10 dark:bg-black/90 dark:text-white">
<nav className="fixed top-0 bottom-0 left-0 z-[100] flex w-16 flex-col items-center overflow-x-hidden border-gray-200 border-r bg-white/90 text-gray-900 shadow-sm backdrop-blur-sm dark:border-white/10 dark:bg-black/90 dark:text-white">
{/* 顶部:LOGO + 网站名称 */}
<a
className="mt-3 flex flex-col items-center gap-1 transition-opacity hover:opacity-80"
@ -294,7 +294,7 @@ export function TopNavbar({
</a>
{/* 侧边功能区 */}
<div className="mt-12 flex w-full flex-1 flex-col items-center gap-3 overflow-y-auto px-2 pb-4">
<div className="mt-12 flex w-full min-w-0 flex-1 flex-col items-center gap-3 overflow-x-hidden overflow-y-auto overscroll-contain px-2 pb-4 [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
{/* 工具箱1 (如果启用且有链接) */}
{toolbox1Enabled &&
customLinks.filter((l) => (l.toolboxGroup || 1) === 1).length > 0 && (

View File

@ -994,7 +994,7 @@ export async function renderAgentPsdTemplate(params: {
}
const layerPositionOffset = getLayerPositionOffset(layer.id);
if (!layer.maskUrl) {
if (!layer.maskUrl && !layer.forceClipMask) {
const didAppend = await appendBufferOverlay({
input: slotOverlay.input,
left: