fix: 支持追加队列批量上传PSD字体
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-03 19:50:49 +08:00
parent 3ee9701a50
commit 9362b25593

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useMemo, useState, type ChangeEvent } from "react"; import { useEffect, useMemo, useRef, useState, type ChangeEvent } from "react";
import useSWR from "swr"; import useSWR from "swr";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@ -104,6 +104,26 @@ function normalizeManagedBrandName(value?: string | null) {
return normalized; return normalized;
} }
function getQueuedFontFileKey(file: File) {
return [file.name, file.size, file.lastModified].join("__");
}
function mergeQueuedFontFiles(current: File[], incoming: File[]) {
const merged = [...current];
const seen = new Set(current.map((file) => getQueuedFontFileKey(file)));
for (const file of incoming) {
const key = getQueuedFontFileKey(file);
if (seen.has(key)) {
continue;
}
seen.add(key);
merged.push(file);
}
return merged;
}
function hasTemplateCategoryTag(tags: string[] | undefined, target: string) { function hasTemplateCategoryTag(tags: string[] | undefined, target: string) {
const normalizedTarget = String(target || "").trim().toLowerCase(); const normalizedTarget = String(target || "").trim().toLowerCase();
if (!normalizedTarget) { if (!normalizedTarget) {
@ -258,6 +278,7 @@ export function PsdTemplatesSettingsClient({
const [fontFamily, setFontFamily] = useState(""); const [fontFamily, setFontFamily] = useState("");
const [fontUploadBrandName, setFontUploadBrandName] = useState(""); const [fontUploadBrandName, setFontUploadBrandName] = useState("");
const [fontUploadFiles, setFontUploadFiles] = useState<File[]>([]); const [fontUploadFiles, setFontUploadFiles] = useState<File[]>([]);
const fontFileInputRef = useRef<HTMLInputElement | null>(null);
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [isCreatingAndParsing, setIsCreatingAndParsing] = useState(false); const [isCreatingAndParsing, setIsCreatingAndParsing] = useState(false);
const [templateUploadPercent, setTemplateUploadPercent] = useState(0); const [templateUploadPercent, setTemplateUploadPercent] = useState(0);
@ -487,9 +508,13 @@ export function PsdTemplatesSettingsClient({
function handleFontFileChange(event: ChangeEvent<HTMLInputElement>) { function handleFontFileChange(event: ChangeEvent<HTMLInputElement>) {
const nextFiles = Array.from(event.target.files || []); const nextFiles = Array.from(event.target.files || []);
setFontUploadFiles(nextFiles); if (nextFiles.length === 0) {
return;
}
if (nextFiles.length === 1) { setFontUploadFiles((current) => mergeQueuedFontFiles(current, nextFiles));
if (fontUploadFiles.length === 0 && nextFiles.length === 1) {
const normalizedName = nextFiles[0]?.name.replace(/\.[^.]+$/, "") || ""; const normalizedName = nextFiles[0]?.name.replace(/\.[^.]+$/, "") || "";
if (normalizedName && !fontUploadName.trim()) { if (normalizedName && !fontUploadName.trim()) {
setFontUploadName(normalizedName); setFontUploadName(normalizedName);
@ -498,6 +523,23 @@ export function PsdTemplatesSettingsClient({
setFontFamily(normalizedName); setFontFamily(normalizedName);
} }
} }
event.currentTarget.value = "";
}
function handleRemoveQueuedFontFile(file: File) {
setFontUploadFiles((current) =>
current.filter(
(item) => getQueuedFontFileKey(item) !== getQueuedFontFileKey(file)
)
);
}
function handleClearQueuedFontFiles() {
setFontUploadFiles([]);
if (fontFileInputRef.current) {
fontFileInputRef.current.value = "";
}
} }
async function saveCategoryTagConfig(nextTags: string[]) { async function saveCategoryTagConfig(nextTags: string[]) {
@ -734,6 +776,9 @@ export function PsdTemplatesSettingsClient({
setFontFamily(""); setFontFamily("");
setFontUploadBrandName(""); setFontUploadBrandName("");
setFontUploadFiles([]); setFontUploadFiles([]);
if (fontFileInputRef.current) {
fontFileInputRef.current.value = "";
}
await mutateFonts(); await mutateFonts();
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : "字体上传失败"); toast.error(error instanceof Error ? error.message : "字体上传失败");
@ -1435,11 +1480,15 @@ export function PsdTemplatesSettingsClient({
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="psdFontFile">字体文件</Label> <Label htmlFor="psdFontFile">字体文件</Label>
<Input <Input
ref={fontFileInputRef}
id="psdFontFile" id="psdFontFile"
type="file" type="file"
accept=".ttf,.otf,.woff,.woff2" accept=".ttf,.otf,.woff,.woff2"
multiple multiple
onChange={handleFontFileChange} onChange={handleFontFileChange}
onClick={(event) => {
event.currentTarget.value = "";
}}
/> />
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{fontUploadFiles.length > 0 {fontUploadFiles.length > 0
@ -1448,13 +1497,55 @@ export function PsdTemplatesSettingsClient({
: `已选择 ${fontUploadFiles.length} 个字体文件:${fontUploadFiles : `已选择 ${fontUploadFiles.length} 个字体文件:${fontUploadFiles
.map((file) => file.name) .map((file) => file.name)
.join("、")}` .join("、")}`
: "支持一次选择多个字体文件批量上传,建议按实际字重分别整理后再导入"} : "支持一次多选,也支持多次继续添加字体文件后再批量上传"}
</p> </p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{fontUploadFiles.length > 1 {fontUploadFiles.length > 1
? "批量上传时会使用每个文件名作为字体名称;如果填写了 CSS 字体族,会批量应用到这一批字体。" ? "批量上传时会使用每个文件名作为字体名称;如果填写了 CSS 字体族,会批量应用到这一批字体。"
: "单个上传时可自定义字体名称和 CSS 字体族。"} : "单个上传时可自定义字体名称和 CSS 字体族。"}
</p> </p>
{fontUploadFiles.length > 0 ? (
<div className="space-y-2 rounded-xl border border-dashed p-3">
<div className="flex items-center justify-between gap-3">
<div className="text-xs font-medium text-muted-foreground">
待上传文件队列
</div>
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 px-2 text-xs"
onClick={handleClearQueuedFontFiles}
>
清空已选
</Button>
</div>
<div className="space-y-2">
{fontUploadFiles.map((file) => (
<div
key={getQueuedFontFileKey(file)}
className="flex items-center justify-between gap-3 rounded-lg border bg-background/60 px-3 py-2"
>
<div className="min-w-0">
<div className="truncate text-sm">{file.name}</div>
<div className="text-[11px] text-muted-foreground">
{(file.size / 1024 / 1024).toFixed(2)} MB
</div>
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0"
onClick={() => handleRemoveQueuedFontFile(file)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
))}
</div>
</div>
) : null}
</div> </div>
<Button onClick={handleUploadFont} disabled={isUploadingFont} className="w-full" variant="outline"> <Button onClick={handleUploadFont} disabled={isUploadingFont} className="w-full" variant="outline">
{isUploadingFont ? ( {isUploadingFont ? (