* feat(create): 创作首页与灵感库 - 纵深画廊、灵感页面、外部提示词池 创作首页空态换成直线纵深图片画廊:11 张横版卡片沿 Z 轴前后排队,当前一张 在最前最大,滚轮、左右按钮或自动轮播切换,卡片带玻璃描边和"行动点 / 标题 / 描述"三段信息,跟随输入框的创作类型联动换队。队列是随机取样并记住最近出现过的 条目,走完一轮自动换批,不再反复看到同样那几张。 左侧「创作」下方新增灵感页:1341 条提示词按来源、分类和创作类型筛选, 支持搜索、收藏,以及自建灵感的新建、编辑、删除和 JSON 导入导出。 新增 scripts/import-inspirations.mjs,从 awesome-seedance、好好学 AI 和 YouMind 两个姊妹库同步条目,并为画廊池生成本地 webp 缩略图(560px / q72, 约 35KB 一张)。上游封面平均 400KB~700KB,直接引用一屏 11 张要拉 5MB 以上, 换成本地缩略图后约 400KB。 画廊池改走 public/inspirations/gallery-pool.json 运行时拉取:几百条提示词正文 压在首屏 JS 上是白背的,而这份数据只服务画廊一处。缩略图等距抽取,横跨各来源 的全部分类;只收描述非空的条目,保证卡片三行版式不缺角。 * docs(create): 功能清单与地图 - 补充纵深画廊与灵感库 功能清单新增「创作首页纵深画廊与灵感库」一节,并在功能地图里记下画廊、 灵感页、运行时目录加载和导入脚本各自的代码入口;待测试清单记下浏览器验收结果 与仍未确认的边界(弱网退回、窄屏、文本条目偏少、外部来源署名)。
59 lines
2.7 KiB
TypeScript
59 lines
2.7 KiB
TypeScript
/**
|
||
* 灵感目录的合成规则:内置目录是只读的,用户的增删改以「覆盖 / 隐藏 / 新增」三种形式叠在上面。
|
||
* 三个集合都不改内置数据本身,这样外部目录重新导入后用户的改动仍然对得上 id。
|
||
*/
|
||
import { catalogIdOf, type CreationInspiration } from "./catalog";
|
||
|
||
/** 用户可以改写的字段。id 与 sourceId 是归属标识,不参与改写。 */
|
||
export type InspirationOverride = Partial<Pick<CreationInspiration, "title" | "description" | "image" | "mode" | "prompt" | "promptZh" | "category" | "tags" | "ratio">>;
|
||
|
||
export type InspirationOverlay = {
|
||
/** 用户新建的条目,id 一定是自定义前缀。 */
|
||
custom: CreationInspiration[];
|
||
/** 按 id 覆盖内置条目。 */
|
||
overrides: Record<string, InspirationOverride>;
|
||
/** 被用户隐藏的内置条目 id。 */
|
||
hidden: string[];
|
||
};
|
||
|
||
export const EMPTY_OVERLAY: InspirationOverlay = { custom: [], overrides: {}, hidden: [] };
|
||
|
||
/** 自定义条目的 id 前缀,用来区分「可以真删」和「只能隐藏」。 */
|
||
export const CUSTOM_ID_PREFIX = "custom:";
|
||
|
||
export function isCustomInspiration(id: string): boolean {
|
||
return id.startsWith(CUSTOM_ID_PREFIX);
|
||
}
|
||
|
||
function withoutUndefined<T extends object>(value: T): T {
|
||
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "")) as T;
|
||
}
|
||
|
||
/**
|
||
* 把用户的改动叠到内置目录上,并按用户顺序把新建条目插在最前。
|
||
* 隐藏只对内置条目生效:新建条目直接删除,没必要留一个隐藏态。
|
||
*/
|
||
export function resolveInspirations(builtIn: CreationInspiration[], overlay: InspirationOverlay): CreationInspiration[] {
|
||
const hidden = new Set(overlay.hidden.filter((id) => !isCustomInspiration(id)));
|
||
const resolved = builtIn.flatMap((item) => {
|
||
const id = catalogIdOf(item);
|
||
if (hidden.has(id)) return [];
|
||
const override = overlay.overrides[id];
|
||
return [override ? { ...item, ...withoutUndefined(override) } : item];
|
||
});
|
||
return [...overlay.custom, ...resolved];
|
||
}
|
||
|
||
/** 当前生效条目里属于某个来源的部分,用于灵感页按来源筛选。 */
|
||
export function itemsBySource(items: CreationInspiration[], sourceId: string): CreationInspiration[] {
|
||
return items.filter((item) => (item.sourceId ?? "original") === sourceId);
|
||
}
|
||
|
||
/**
|
||
* 一键填入创作时优先用中文正文:外部图片提示词的原文是英文,
|
||
* 中文界面下把英文整段塞进编辑器是坏体验,双语来源就用中文那版。
|
||
*/
|
||
export function fillablePrompt(item: CreationInspiration): string {
|
||
return (item.promptZh || item.prompt || "").trim();
|
||
}
|