设计与实现:从 OK 确认弹窗提案到复用型公告栈的落地)
Civitai 生成器公告Generator Announcements设计与实现从 OK 确认弹窗提案到复用型公告栈的落地【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai导读本文围绕 Civitai 仓库中的 generator-announcements.md 设计提案展开讲解如何让审核员moderator发布的公告直接出现在生成器generator //generate页面内部以必须点击 OK 才能关闭的阻塞式模态框形式呈现。文章将依次剖析为什么要复用而不是重建现有公告栈、如何通过 metadata 上的placement字段仓库实际演进为type枚举零迁移地扩展公告作用域、OK 点击如何复用 dismissal store 完成免后端确认以及GeneratorAnnouncementGate组件的挂载位置如何天然决定触发时机。读完你不仅能复现这套方案还能理解其背后的数据模型、缓存与 SSR 播种机制。背景与目标公告不只是全站横幅Civitai 的公告系统目前主要渲染在全站横幅区域sitewide banner area。提案要解决的是一个更具体的问题当生成器出现特殊说明例如模型、工作流或生态系统的临时变更时需要有一个用户必须主动确认的窗口而不是一条可以被忽略的横幅。ClickUp 的原始诉求很直白——一个人们必须为特殊公告点击 OK 的窗口。提案刻意做了三个边界界定不是 generator messagesgenerator-messages.md 已经上线那是生成器底部非阻塞的页脚文案来自独立的 store本提案是阻塞式 OK 确认模态框两者互不替代。没有 severity/level 分级也没有内联横幅——一个 generator announcement就是那个确认弹窗本身。不新增任何表、不做后端 ack 追踪——公告是短生命周期的客户端确认已经足够。为什么复用而不是重建现有的公告栈已经足够完整提案的核心工程判断是仓库已经拥有一整套公告基础设施从模型、审核员 CRUD、Redis 缓存、时间窗口过滤到领域 受众定向、渲染组件和确认 store全部就位。要做的事只有两件把公告作用域限定到生成器并以模态框形态渲染。组件清单如下组成部分仓库位置Announcement数据模型packages/civitai-db-schema/prisma/schema.full.prismametadata Zod schemasrc/server/schema/announcement.schema.ts服务层过滤 / 缓存src/server/services/announcement.service.ts路由公开 审核员src/server/routers/announcement.router.ts渲染组件src/components/Announcements/Announcement.tsx客户端拉取 确认 storesrc/components/Announcements/announcements.utils.ts生成器布局src/components/generation_v2/GenerationLayout.tsx在 schema.full.prisma 中可以看到Announcement模型本身已经具备title、content、emoji、color、domainDomainColor[]默认[all]、startsAt/endsAt时间窗口、metadata Json?、disabled等字段——生成器公告无需触碰这张表的任何列。数据模型变更一个 metadata 字段搞定作用域提案给出的方案是零迁移的不新增Announcement列全部承载在既有的metadataJSON 上只需向 announcement.schema.ts 中的announcementMetaSchema增加一个字段// added to announcementMetaSchema提案原文 placement: z.enum([site, generator, both]).default(site),placement控制公告渲染在哪里。现有公告默认site行为完全不变任何包含generator的公告都会以生成器模态框形态出现。仓库中的实际演进值得注意打开 announcement.schema.ts 可以看到这一提案在源码中已经落为更通用的类型枚举且默认值逻辑完全一致// Where an announcement is surfaced. site is the default catch-all (the global // banner area notifications); generator and training scope an announcement // to those pages. Untyped/legacy announcements are treated as site at read time. export const announcementTypes [site, generator, training] as const; export const announcementTypeSchema z.enum(announcementTypes); export type AnnouncementType z.infertypeof announcementTypeSchema;即placement在实现中被命名为metadata.type取值从二元的site/generator/both演进为site | generator | training并预留了training训练页作用域默认值site保证旧数据向后兼容——未设置类型的历史公告在读取时一律按site处理。同文件还展示了announcementMetaSchema的其余能力targetAudienceall/unauthenticated/authenticated默认all、dismissible默认true、colSpan、actions按钮动作数组、image裸存储键而非Image行 ID等。这些字段与placement正交生成器公告可以自由组合例如一条仅登录用户可见、带跳转按钮、不可关闭的生成器公告。确认机制复用 dismissal store不做后端追踪OK 点击复用既有的确认 storeannouncements.utils.ts 中的dismissAnnouncements/useAnnouncementsStore。store 本身由 src/store/dismissal-store.ts 的createDismissalStore工厂生成提供了四件套能力单一存储槽整个已确认集合放在一个槽位buckets按site/generator/training分区而不是每个 id 一个 keyadd 不重复dismiss(ids, bucket)幂等添加prune 自清理当公告不再 live 时prune(live, bucket)自动丢弃已失效的 idannouncements.utils.ts 在数据加载完成后执行单一写者state 与持久化同步写入二者不会分歧。localStorage 与 cookie 的演进说明提案写作时描述为existing localStorage store而当前仓库已将该 store 的持久化从 localStorage 升级为cookie 支撑见 announcements.utils.ts客户端在 store 初始化前执行migrateLegacyLocalStorageToCookie()把旧 localStorage 状态迁移到 cookie避免老用户已确认的公告重新出现。原因是服务端也需要读取已确认集合才能在 SSR 阶段以真实高度渲染轮播feed CLS 修复cookie 让服务端与客户端共享同一个解析器。对生成器公告而言dismissAnnouncements(ids, generator)会把确认写入generator桶而 dismissal-store.ts 同时保留了localStorageDismissalStorage适配器——生成器实验性警告等纯客户端场景仍在使用 localStorage。提案明确接受的取舍确认是按浏览器的用户切换设备或清除存储后公告可能重现。考虑到这类公告生命周期极短这一取舍可以接受——不需要新表不需要按用户的 DB 追踪。后端只有 schema 一处改动提案强调后端唯一的改动就是 schema 本身没有新模型、没有迁移、没有新的 mutation。Schema— 增加placement即实现中的typemetadata 字段通过site默认值保证向后兼容。Service— 零改动。announcement.service.ts 的getCurrentAnnouncements过滤逻辑保持原样placement/type完全由客户端从metadata读取服务端不感知。从服务层源码可以印证这个不感知的底气Redis 缓存getAnnouncementsCached走createKeyedTtlMemo进程内 30 秒 TTL Redis 键REDIS_KEYS.CACHES.ANNOUNCEMENTS[:domain]EX: CacheTTL.day按 domain 全局缓存upsert/delete 时redis.del全量失效见 announcement.service.ts。生成器公告作为同一批数据的子集天然共享这份缓存。时间窗口过滤activeAnnouncementWhere(now)判定已启用且当前处于 start/end 窗口内announcement.service.tsendsAt缺省视为2100-12-31永不结束。受众定向targetAudience在服务端按登录态过滤announcement.service.tstargetUserIds定向走AnnouncementUser关联表但这是既有的按用户定向能力与生成器作用域无关。路由层同样无需变更announcement.router.ts 的getAnnouncements是publicProcedure通过applyRequestDomainColor中间件按请求 host 打上 domain返回当前 domain 下所有类型的 live 公告——客户端拿到后再按type过滤。前端一个 hook 加一个 Gate 组件选取生成器公告零额外网络成本在useGetAnnouncements旁新增一个小 hook直接借用已经被 SSR 播种的查询见 announcements.utils.ts查询由 AppProvider 的/api/user/settingsSSR 播种initialData 5 分钟staleTime使每次启动不再发额外请求// useGetGeneratorAnnouncements()提案原文 const { data } useGetAnnouncements(); return data.filter( (a) a.metadata.placement generator || a.metadata.placement both );对应仓库实现中useGetAnnouncements(type)本身就以type参数在客户端过滤(x.metadata.type ?? site) type所以调用useGetAnnouncements(generator)即等价于提案中的过滤逻辑——数据源同一份不产生新网络请求。确认模态框GeneratorAnnouncementGate在生成器中挂载一个GeneratorAnnouncementGate组件流程为取生成器公告type包含generator者剔除已在确认 storeuseAnnouncementsStore中的若仍有剩余打开单个阻塞式 Mantine 模态框一次性列出所有未确认的生成器公告每条复用既有Announcement卡片渲染标题 markdown 正文底部只有一个OK按钮点击 OK → 对所有展示的公告调用dismissAnnouncements(ids, generator)store 接受数组随即关闭模态框。模态框不可通过 X 或点击遮罩关闭非 dismissible用户必须点击OK——OK 本身就是确认动作。两个复用点值得展开列表渲染每条公告复用 src/components/Announcements/Announcement.tsx 及其底层 src/components/Announcements/AnnouncementCard.tsx。AnnouncementCard是站点公告与创作者公告共用的唯一卡片避免二者视觉漂移负责布局封面裸 key 或Image行、标题、CustomMarkdown正文、动作按钮Announcement组件负责平台公告特有的部分——确认按钮与审核员控制项。生成器模态框内逐条复用这套卡片即可无需新布局。模态框模式参考生成器内既有对话框模式 src/components/generation_v2/CompatibilityConfirmModal.tsx——它展示了如何通过dialogStore.trigger({ id, component, props })打开居中Modal底部 sticky 操作区放确认按钮并在handleConfirm中回调后dialog.onClose()。GeneratorAnnouncementGate只需去掉 Cancel / Continue 双按钮改为单一 OK并去掉onClose逃生通道即可。创作审核员侧复用现有管理 UI审核员创作入口不做任何新表面直接复用现有的 mod 公告管理后台只多暴露一个字段Placement放置位置选择器Site站点/ Generator生成器/ Both两者在实现中该字段对应 announcement.schema.ts 的upsertAnnouncementSchema.metadata类型为announcementMetaSchema由moderatorProcedure的upsertAnnouncement/deleteAnnouncement/getAnnouncementsPaged完成 CRUDannouncement.router.ts。审核员保存后服务端会清空全部公告 Redis 缓存announcement.service.ts新公告在下一个客户端刷新时可见。作用域与触发行为提案对作用域做了明确收敛仅生成器全域定向公告在生成器中展示与用户当前选中的 ecosystem / model 无关——不做按生态系统的定向不在 ClickUp 诉求范围内。多条并存时合并展示同一时间多条生成器公告 live 时在同一个模态框中逐条列出一个 OK 全部确认而不是一条条轮流弹。触发行为由挂载位置天然保证无需额外判断模态框适用于任何打开生成面板的用户。因为GeneratorAnnouncementGate挂载在生成表单/面板内部它只在面板存在时渲染从而触发——而面板在/generate页面上总是存在的。仅仅在别处浏览、面板未打开的用户不会被中断。不需要额外的面板是否打开检查挂载位置已经处理了这一点。挂载点在 src/components/generation_v2/GenerationLayout.tsxGenerationLayout包裹生成器主体与页脚在该布局内挂载 Gate 即可保证生成面板渲染 → Gate 渲染 → 弹窗出现面板不渲染 → 完全不打扰。落地要点小结零迁移无新表、无迁移、无新后端 mutation唯一 schema 改动是announcementMetaSchema上的作用域枚举type默认site。零额外网络复用 SSR 播种的getAnnouncements查询客户端过滤generator类型。零后端确认OK 点击走 dismissal store当前为 cookie 支撑、按类型分桶、自动 prune接受换设备/清存储会重现的短生命周期取舍。触发即挂载Gate 挂在生成器布局内面板是否存在决定弹窗是否出现。多公告合并同弹窗列表 单 OK避免连环弹窗打断生成流程。对需要快速上线生成器内强提示模型下线、工作流变更、临时规则的团队而言这套方案的核心价值在于完全建立在既有公告栈之上只新增一个枚举字段、一个 hook、一个 Gate 组件即可获得带确认语义的生成器内公告能力。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考