fix: improve PSD template management and clipping masks
This commit is contained in:
parent
db1fc8c3df
commit
5461cff1db
@ -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">
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user