fix(*): 质量检查与版本发布 - 修复格式门禁和创作灵感兼容导出并发布 v1.6.2

This commit is contained in:
ddcat666 committed 2026-10-08 20:43:15 +08:00
1 parent 04689257d2
commit 9174aae013
86 files changed
+22711 -634

No files matched your search

+142 -91
View File
@@ -90,13 +90,21 @@ export default function InspirationsPage() {
let cancelled = false;
setLoadError("");
loadInspirationCatalog()
.then((loaded) => { if (!cancelled) setCatalog(loaded); })
.catch((error) => { if (!cancelled) setLoadError(error instanceof Error ? error.message : "灵感目录加载失败"); });
return () => { cancelled = true; };
.then((loaded) => {
if (!cancelled) setCatalog(loaded);
})
.catch((error) => {
if (!cancelled) setLoadError(error instanceof Error ? error.message : "灵感目录加载失败");
});
return () => {
cancelled = true;
};
}, [reloadKey]);
// 筛选条件一变就回第一页,否则会停在一个已经不存在的位置上。
useEffect(() => { setPage(1); }, [source, modeFilter, category, view, sort, debouncedSearch, pageSize]);
useEffect(() => {
setPage(1);
}, [source, modeFilter, category, view, sort, debouncedSearch, pageSize]);
const overlay = useMemo(() => ({ custom, overrides, hidden }), [custom, hidden, overrides]);
const resolved = useMemo(() => (catalog ? resolveInspirations(catalog.entries, overlay) : []), [catalog, overlay]);
@@ -136,9 +144,7 @@ export default function InspirationsPage() {
merged.set(option.id, { label: option.label, count: (current?.count ?? 0) + option.count });
}
}
return [...merged.entries()]
.sort((left, right) => right[1].count - left[1].count || left[1].label.localeCompare(right[1].label, "zh-Hans-CN"))
.map(([value, { label, count }]) => ({ value, label, count }));
return [...merged.entries()].sort((left, right) => right[1].count - left[1].count || left[1].label.localeCompare(right[1].label, "zh-Hans-CN")).map(([value, { label, count }]) => ({ value, label, count }));
}, [catalog, source]);
const sourceCounts = useMemo(() => {
@@ -194,75 +200,101 @@ export default function InspirationsPage() {
const paged = useMemo(() => visible.slice((page - 1) * pageSize, page * pageSize), [visible, page, pageSize]);
const handleUse = useCallback((item: CreationInspiration) => {
navigate("/create", { state: { inspirationPrompt: fillablePrompt(item), inspirationMode: item.mode } });
}, [navigate]);
const handleUse = useCallback(
(item: CreationInspiration) => {
navigate("/create", { state: { inspirationPrompt: fillablePrompt(item), inspirationMode: item.mode } });
},
[navigate],
);
const handleCopy = useCallback((text: string, label: string) => {
void navigator.clipboard.writeText(text).then(
() => message.success(`${label}已复制`),
() => message.error("复制失败,请手动选中文本"),
);
}, [message]);
const handleCopy = useCallback(
(text: string, label: string) => {
void navigator.clipboard.writeText(text).then(
() => message.success(`${label}已复制`),
() => message.error("复制失败,请手动选中文本"),
);
},
[message],
);
const handleDelete = useCallback((item: CreationInspiration) => {
const id = catalogIdOf(item);
const isCustom = isCustomInspiration(id);
modal.confirm({
className: "workspace-modal workspace-modal-compact",
title: isCustom ? "删除这条灵感?" : "从库中隐藏这条灵感?",
content: isCustom
? `「${item.title}」会从本地灵感库移除,此操作不可撤销。`
: `「${item.title}」来自外部目录,不能真正删除;隐藏后可用「已隐藏」视图恢复,重新导入外部目录也不会让它复现。`,
okText: isCustom ? "删除" : "隐藏",
okButtonProps: { danger: true },
cancelText: "保留",
onOk: () => {
removeEntry(id);
if (detail && catalogIdOf(detail) === id) setDetail(null);
message.success(isCustom ? "已删除" : "已隐藏");
},
});
}, [detail, message, modal, removeEntry]);
const handleDelete = useCallback(
(item: CreationInspiration) => {
const id = catalogIdOf(item);
const isCustom = isCustomInspiration(id);
modal.confirm({
className: "workspace-modal workspace-modal-compact",
title: isCustom ? "删除这条灵感?" : "从库中隐藏这条灵感?",
content: isCustom ? `「${item.title}」会从本地灵感库移除,此操作不可撤销。` : `「${item.title}」来自外部目录,不能真正删除;隐藏后可用「已隐藏」视图恢复,重新导入外部目录也不会让它复现。`,
okText: isCustom ? "删除" : "隐藏",
okButtonProps: { danger: true },
cancelText: "保留",
onOk: () => {
removeEntry(id);
if (detail && catalogIdOf(detail) === id) setDetail(null);
message.success(isCustom ? "已删除" : "已隐藏");
},
});
},
[detail, message, modal, removeEntry],
);
const handleSubmit = useCallback((draft: Parameters<typeof createEntry>[0]) => {
if (editing) {
updateEntry(catalogIdOf(editing), draft);
message.success("已保存修改");
setDetail((current) => (current && catalogIdOf(current) === catalogIdOf(editing) ? { ...current, ...draft } : current));
setEditing(null);
return;
}
createEntry(draft);
// 新建后把筛选整体清掉:自建条目的来源固定是「原创」、分类也常和当前筛选不符,
// 不清的话用户刚点完"创建"却看不到自己建的东西。
setSource("all");
setModeFilter("all");
setCategory("all");
setView("all");
setSearch("");
message.success("灵感已创建");
}, [createEntry, editing, message, updateEntry]);
const handleSubmit = useCallback(
(draft: Parameters<typeof createEntry>[0]) => {
if (editing) {
updateEntry(catalogIdOf(editing), draft);
message.success("已保存修改");
setDetail((current) => (current && catalogIdOf(current) === catalogIdOf(editing) ? { ...current, ...draft } : current));
setEditing(null);
return;
}
createEntry(draft);
// 新建后把筛选整体清掉:自建条目的来源固定是「原创」、分类也常和当前筛选不符,
// 不清的话用户刚点完"创建"却看不到自己建的东西。
setSource("all");
setModeFilter("all");
setCategory("all");
setView("all");
setSearch("");
message.success("灵感已创建");
},
[createEntry, editing, message, updateEntry],
);
const handleExport = useCallback(() => {
downloadText(exportPayload(), exportFileName());
message.success("已导出本地改动(自建条目 / 改写 / 隐藏 / 收藏)");
}, [exportPayload, message]);
const handleImportFile = useCallback(async (file: File) => {
try {
const result = importPayload(await file.text());
message.success(`导入完成:新增 ${result.created},覆盖 ${result.updated}${result.skipped ? `,跳过 ${result.skipped} 条无效数据` : ""}`);
} catch (error) {
message.error(error instanceof Error ? error.message : "导入失败");
}
}, [importPayload, message]);
const handleImportFile = useCallback(
async (file: File) => {
try {
const result = importPayload(await file.text());
message.success(`导入完成:新增 ${result.created},覆盖 ${result.updated}${result.skipped ? `,跳过 ${result.skipped} 条无效数据` : ""}`);
} catch (error) {
message.error(error instanceof Error ? error.message : "导入失败");
}
},
[importPayload, message],
);
const headerActions = (
<>
<Button icon={<Import className="size-4" />} onClick={() => importRef.current?.click()}>导入</Button>
<Button icon={<Download className="size-4" />} onClick={handleExport}>导出</Button>
<Button type="primary" icon={<Plus className="size-4" />} onClick={() => { setEditing(null); setEditorOpen(true); }}>新建灵感</Button>
<Button icon={<Import className="size-4" />} onClick={() => importRef.current?.click()}>
导入
</Button>
<Button icon={<Download className="size-4" />} onClick={handleExport}>
导出
</Button>
<Button
type="primary"
icon={<Plus className="size-4" />}
onClick={() => {
setEditing(null);
setEditorOpen(true);
}}
>
新建灵感
</Button>
<input
ref={importRef}
type="file"
@@ -282,11 +314,7 @@ export default function InspirationsPage() {
return (
<WorkspacePage>
<PageHeader
title="灵感库"
description={`${total} 条提示词,按来源、分类和创作类型挑,点开可以一键带去创作页`}
actions={headerActions}
/>
<PageHeader title="灵感库" description={`${total} 条提示词,按来源、分类和创作类型挑,点开可以一键带去创作页`} actions={headerActions} />
{loadError ? (
<WorkspaceErrorState title="灵感目录加载失败" description={loadError} onRetry={() => setReloadKey((key) => key + 1)} />
@@ -307,13 +335,7 @@ export default function InspirationsPage() {
<span className="inspiration-chip-count">{source === "all" ? resolved.length : (sourceCounts.get(source) ?? 0)}</span>
</button>
{categoryChoices.slice(0, 16).map((choice) => (
<button
key={choice.value}
type="button"
className="inspiration-chip"
aria-pressed={category === choice.value}
onClick={() => setCategory(category === choice.value ? "all" : choice.value)}
>
<button key={choice.value} type="button" className="inspiration-chip" aria-pressed={category === choice.value} onClick={() => setCategory(category === choice.value ? "all" : choice.value)}>
{choice.label}
<span className="inspiration-chip-count">{choice.count}</span>
</button>
@@ -321,14 +343,7 @@ export default function InspirationsPage() {
</div>
<div className="flex flex-wrap items-center gap-2">
<Input
allowClear
prefix={<Search className="size-4 text-foreground/40" />}
placeholder="搜标题、描述或标签"
value={search}
onChange={(event) => setSearch(event.target.value)}
className="max-w-xs"
/>
<Input allowClear prefix={<Search className="size-4 text-foreground/40" />} placeholder="搜标题、描述或标签" value={search} onChange={(event) => setSearch(event.target.value)} className="max-w-xs" />
<SegmentedControl options={VIEW_OPTIONS} value={view} onChange={(value) => setView(value as ViewFilter)} />
<Select
options={[
@@ -339,7 +354,10 @@ export default function InspirationsPage() {
})),
]}
value={source}
onChange={(value) => { setSource(value as SourceFilter); setCategory("all"); }}
onChange={(value) => {
setSource(value as SourceFilter);
setCategory("all");
}}
className="w-52"
/>
<Select options={SORT_OPTIONS} value={sort} onChange={(value) => setSort(value as SortKey)} className="w-32" />
@@ -370,13 +388,28 @@ export default function InspirationsPage() {
<span className="inspiration-card-source">{inspirationSourceOf(item).label}</span>
<div className="inspiration-card-actions">
<Tooltip title={favorite ? "取消收藏" : "收藏"}>
<button type="button" className="inspiration-card-action" aria-pressed={favorite} onClick={(event) => { event.stopPropagation(); toggleFavorite(id); }}>
<button
type="button"
className="inspiration-card-action"
aria-pressed={favorite}
onClick={(event) => {
event.stopPropagation();
toggleFavorite(id);
}}
>
<Heart className="size-3.5" fill={favorite ? "currentColor" : "none"} />
</button>
</Tooltip>
{view === "hidden" ? (
<Tooltip title="恢复显示">
<button type="button" className="inspiration-card-action" onClick={(event) => { event.stopPropagation(); restoreEntry(id); }}>
<button
type="button"
className="inspiration-card-action"
onClick={(event) => {
event.stopPropagation();
restoreEntry(id);
}}
>
<RotateCcw className="size-3.5" />
</button>
</Tooltip>
@@ -390,7 +423,10 @@ export default function InspirationsPage() {
],
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
if (key === "edit") { setEditing(item); setEditorOpen(true); }
if (key === "edit") {
setEditing(item);
setEditorOpen(true);
}
if (key === "delete") handleDelete(item);
},
}}
@@ -423,7 +459,15 @@ export default function InspirationsPage() {
/>
)}
<PaginationBar current={page} pageSize={pageSize} total={visible.length} onChange={(next, size) => { setPage(next); setPageSize(size); }} />
<PaginationBar
current={page}
pageSize={pageSize}
total={visible.length}
onChange={(next, size) => {
setPage(next);
setPageSize(size);
}}
/>
</div>
)}
@@ -435,7 +479,11 @@ export default function InspirationsPage() {
onUse={handleUse}
onCopy={handleCopy}
onToggleFavorite={(item) => toggleFavorite(catalogIdOf(item))}
onEdit={(item) => { setDetail(null); setEditing(item); setEditorOpen(true); }}
onEdit={(item) => {
setDetail(null);
setEditing(item);
setEditorOpen(true);
}}
onDelete={handleDelete}
/>
@@ -443,7 +491,10 @@ export default function InspirationsPage() {
open={editorOpen}
item={editing}
categoryOptions={categoryChoices}
onClose={() => { setEditorOpen(false); setEditing(null); }}
onClose={() => {
setEditorOpen(false);
setEditing(null);
}}
onSubmit={handleSubmit}
/>
</WorkspacePage>
@@ -41,14 +41,7 @@ export function InspirationDetailModal({
const custom = item ? isCustomInspiration(catalogIdOf(item)) : false;
return (
<AppModal
open={Boolean(item)}
onCancel={onClose}
width={880}
title={null}
footer={null}
styles={{ body: { padding: 0 } }}
>
<AppModal open={Boolean(item)} onCancel={onClose} width={880} title={null} footer={null}>
{item ? (
<div className="flex max-h-[76vh] flex-col">
<div className="relative shrink-0 overflow-hidden" style={{ borderRadius: "var(--r-xl, 14px)" }}>
@@ -68,22 +61,36 @@ export function InspirationDetailModal({
</button>
</Tooltip>
<Tooltip title="编辑">
<button type="button" className="inspiration-icon-button" onClick={() => onEdit(item)}><Pencil className="size-4" /></button>
<button type="button" className="inspiration-icon-button" onClick={() => onEdit(item)}>
<Pencil className="size-4" />
</button>
</Tooltip>
<Tooltip title={custom ? "删除" : "从库中隐藏"}>
<button type="button" className="inspiration-icon-button" onClick={() => onDelete(item)}><Trash2 className="size-4" /></button>
<button type="button" className="inspiration-icon-button" onClick={() => onDelete(item)}>
<Trash2 className="size-4" />
</button>
</Tooltip>
</div>
</div>
<div className="flex flex-col gap-1.5">
<MetaRow label="来源">{source?.name ?? "原创"}{item.credit ? ` · ${item.credit}` : ""}</MetaRow>
<MetaRow label="来源">
{source?.name ?? "原创"}
{item.credit ? ` · ${item.credit}` : ""}
</MetaRow>
<MetaRow label="分类">{categoryLabel(item)}</MetaRow>
<MetaRow label="类型">{modeLabels[item.mode]}{item.ratio ? ` · ${item.ratio}` : ""}</MetaRow>
<MetaRow label="类型">
{modeLabels[item.mode]}
{item.ratio ? ` · ${item.ratio}` : ""}
</MetaRow>
{item.tags?.length ? (
<MetaRow label="标签">
<span className="flex flex-wrap gap-1.5">
{item.tags.map((tag) => <span key={tag} className="inspiration-chip">{tag}</span>)}
{item.tags.map((tag) => (
<span key={tag} className="inspiration-chip">
{tag}
</span>
))}
</span>
</MetaRow>
) : null}
@@ -102,7 +109,9 @@ export function InspirationDetailModal({
<div className="flex shrink-0 items-center justify-end gap-2 border-t border-foreground/8 px-5 py-3">
<Button onClick={onClose}>关闭</Button>
<Button type="primary" icon={<Wand2 className="size-4" />} onClick={() => onUse(item)}>用这个创意创作</Button>
<Button type="primary" icon={<Wand2 className="size-4" />} onClick={() => onUse(item)}>
用这个创意创作
</Button>
</div>
</div>
) : null}
@@ -115,7 +124,9 @@ function PromptSection({ label, text, onCopy }: { label: string; text: string; o
<section className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-2">
<h3 className="text-xs font-medium text-foreground/58">{label}</h3>
<Button size="small" icon={<Copy className="size-3.5" />} onClick={() => onCopy(text, label)}>复制</Button>
<Button size="small" icon={<Copy className="size-3.5" />} onClick={() => onCopy(text, label)}>
复制
</Button>
</div>
<div className="inspiration-prompt-block">{text}</div>
</section>
@@ -77,12 +77,26 @@ export function InspirationEditorModal({
open={open}
title={item ? "编辑灵感" : "新建灵感"}
width={720}
onCancel={() => { form.resetFields(); onClose(); }}
afterOpenChange={(visible) => { if (visible) form.setFieldsValue(toFormValues(item)); }}
onCancel={() => {
form.resetFields();
onClose();
}}
afterOpenChange={(visible) => {
if (visible) form.setFieldsValue(toFormValues(item));
}}
footer={
<div className="flex justify-end gap-2">
<Button onClick={() => { form.resetFields(); onClose(); }}>取消</Button>
<Button type="primary" onClick={() => void handleOk()}>{item ? "保存" : "创建"}</Button>
<Button
onClick={() => {
form.resetFields();
onClose();
}}
>
取消
</Button>
<Button type="primary" onClick={() => void handleOk()}>
{item ? "保存" : "创建"}
</Button>
</div>
}
>
+8 -8
View File
@@ -20,7 +20,7 @@
text-align: left;
cursor: pointer;
transition:
transform var(--user-motion, 180ms) cubic-bezier(.2, .8, .3, 1),
transform var(--user-motion, 180ms) cubic-bezier(0.2, 0.8, 0.3, 1),
box-shadow var(--user-motion, 180ms) ease,
border-color var(--user-motion, 180ms) ease;
}
@@ -49,7 +49,7 @@
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 420ms cubic-bezier(.2, .8, .3, 1);
transition: transform 420ms cubic-bezier(0.2, 0.8, 0.3, 1);
}
.inspiration-card:hover .inspiration-card-cover img {
@@ -62,7 +62,7 @@
position: absolute;
inset: auto 0 0;
height: 46%;
background: linear-gradient(transparent, rgba(0, 0, 0, .58));
background: linear-gradient(transparent, rgba(0, 0, 0, 0.58));
pointer-events: none;
}
@@ -73,7 +73,7 @@
z-index: 1;
padding: 3px var(--space-2, 8px);
border-radius: var(--r-full, 999px);
background: rgba(20, 22, 26, .48);
background: rgba(20, 22, 26, 0.48);
color: #fff;
font-size: var(--fs-tiny, 10px);
line-height: 1.6;
@@ -104,8 +104,8 @@
width: 28px;
height: 28px;
color: #fff;
background: rgba(20, 22, 26, .5);
border: 1px solid rgba(255, 255, 255, .18);
background: rgba(20, 22, 26, 0.5);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: var(--r-full, 999px);
cursor: pointer;
-webkit-backdrop-filter: blur(10px);
@@ -114,7 +114,7 @@
}
.inspiration-card-action:hover {
background: rgba(20, 22, 26, .72);
background: rgba(20, 22, 26, 0.72);
}
.inspiration-card-action.is-on {
@@ -197,7 +197,7 @@
/* 标签里的计数:比正文弱一档,但仍要在两种底色上都读得出来。 */
.inspiration-chip-count {
font-variant-numeric: tabular-nums;
opacity: .58;
opacity: 0.58;
}
/* 卡片上的分类/署名标签是纯展示,不参与点击,去掉指针与悬停反馈。 */