DeepChat 启动体验(Splash Experience)设计与实现:从 Logo 加载动画到数据库解锁的透明窗口方案
【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat
导读
启动画面是桌面应用留给用户的第一印象,也是承载"数据库解锁"这类关键安全交互的必经入口。本文以 DeepChat 仓库中的 Splash Experience 规格文档 为核心,深入剖析其"Logo 动画加载 + 圆形系统解锁 + 矩形手动解锁"的三态启动体验设计,并结合 splashWindow.ts、loading.vue 等真实源码,讲解透明 BrowserWindow 的创建参数、渲染进程四态切换、IPC 解锁契约以及 reduced-motion 无障碍降级等落地细节。读完本文,你将掌握一套"启动画面与安全解锁 UI 一体化"的 Electron 实现范式。
一、用户需求:为什么启动画面需要一次重新设计
规格文档首先明确了问题边界:启动时应当呈现一个可识别的 DeepChat 加载画面,同时必须在"自动系统解锁"与"手动数据库解锁"之间保持清晰的视觉区分(对应 spec.md 的 User Need 一节)。
深层矛盾在于:传统的纯文本加载进度文案既没有品牌感,又无法承载"是否需要用户输入密码"这一语义。DeepChat 的方案是把启动画面升级为一个同时具备加载与解锁两种职责的状态机界面——它不只是一个 Logo,还是一个安全交互入口。
二、设计目标:四态界面与圆形/矩形视觉语言
规格文档列出的 Goals 可以归纳为四件事:
- 用基于 Logo 的动画替代文本加载进度(animated logo-based splash);
- 加载态与系统凭据解锁态使用圆形 DOM 构图(circular DOM compositions);
- 手动密码解锁保持矩形、功能完整(square and functional);
- 不改变既有数据库解锁的 IPC 行为(preserve existing IPC behavior)。
落到代码里,渲染进程 loading.vue 用一个moderef 维护四个互斥状态(见第 176 行):
const mode = ref<'loading' | 'system-unlock' | 'unlock' | 'recovery'>('loading')| mode | 视觉容器 | 交互内容 | 触发场景 |
|---|---|---|---|
loading | 圆形loader-stage--orb | 无(纯动画) | 应用初始化、启动加载 |
system-unlock | 圆形unlock-stage--orb | 无(等待系统凭据) | 从系统凭据库读取保存的密码 |
unlock | 矩形unlock-stage--manual | 密码输入框 + Unlock / Quit | 需要用户手动输入 SQLite 密码 |
recovery | 矩形unlock-stage--manual | 密码输入 / Start empty / Quit | 数据库损坏、无法读取等恢复场景 |
圆形与矩形的区分在模板中非常直观:加载态与系统解锁态挂载--orb变体类,由aurora-background渲染出一个 340px 的圆形边框容器;手动解锁与恢复态挂载--manual变体类,使用不透明表单面板(unlock-panel--manual去除背景与阴影,回归透明表单)。
三、验收标准逐条落地
3.1 加载态:Logo + 分层光效 + reduced-motion
验收标准第一条要求加载态"使用 DeepChat Logo、分层光效,并支持 reduced-motion"。代码中对应logo-loader容器内的一组元素:
<div class="logo-loader" aria-hidden="true"> <span class="logo-bloom"></span> <span class="logo-bloom logo-bloom--inner"></span> <span class="core-flare"></span> <span class="speed-line speed-line--one"></span> <span class="speed-line speed-line--two"></span> <div class="logo-mark logo-mark--dark" v-html="darkLogo" /> <div class="logo-mark logo-mark--light" v-html="lightLogo" /> </div>这些元素分别承担:外层光晕扩散(bloom-deploy)、内层闪光(inner-bloom-flash)、核心高光(core-flare)、左右两道速度线(speed-scan)以及 Logo 主体的机械式组装(mech-frame-arrive/mech-body-lock/mech-tail-fold)。
reduced-motion 支持在文件末尾的媒体查询中完整实现(loading.vue):
@media (prefers-reduced-motion: reduce) { .aurora-ribbon, .aurora-pool, .logo-bloom, .core-flare, .speed-line, .logo-mark, .logo-mark :deep(path), .unlock-logo, .unlock-logo :deep(path) { animation: none; } .speed-line { display: none; } }所有动画在系统开启"减弱动态效果"时被整体关闭,速度线直接隐藏,符合无障碍最佳实践。
3.2 圆形背景由 DOM/CSS 绘制,而非窗口形状
验收标准第二条非常关键:圆形背景必须由渲染进程 DOM/CSS 创建,而不是依赖 BrowserWindow 的 shape 配置。查看 splashWindow.ts 中窗口创建参数可以确认,窗口本身只是一个 420×340 的矩形透明窗口:
this.splashWindow = new BrowserWindow({ width: 420, height: 340, icon: iconFile, resizable: false, movable: false, frame: false, alwaysOnTop: true, center: true, show: false, // 先隐藏窗口,等待 ready-to-show 以避免白屏 autoHideMenuBar: true, skipTaskbar: true, transparent: true, backgroundColor: '#00000000', webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, '../preload/splash.mjs'), sandbox: false, devTools: is.dev } })圆形效果完全由 loading.vue 的 CSS 承担:
.loader-stage--orb .aurora-background, .unlock-stage--orb .aurora-background { top: 50%; left: 50%; width: 340px; height: 340px; border: 1px solid rgb(148 163 184 / 26%); border-radius: 50%; box-shadow: inset 0 1px rgb(255 255 255 / 10%); transform: translate(-50%, -50%); }这种"透明矩形窗口 + DOM 圆形构图"的取舍,让窗口无需依赖平台 shape API 即可呈现任意形状,同时保留了alwaysOnTop、center、skipTaskbar等窗口语义,跨平台表现一致。
3.3 手动解锁保持矩形且密码控件可用
第三条验收标准要求手动解锁使用矩形 DOM 背景并保留密码控件。unlock/recovery两个状态下的表单包含:密码输入框(unlock-input,type="password"+autocomplete="current-password"+autofocus)、主按钮(Unlock)、辅助按钮(Quit),恢复态另有Start empty(带二次确认文案Confirm start empty)。手动解锁态输入框自动聚焦,input事件实时控制提交按钮的disabled状态,未输入密码时无法提交。
3.4 真实解锁请求与进度事件驱动状态流转
第四条要求真实的解锁请求与进度事件仍能驱动状态迁移、提交与取消。这条由主进程 → preload → 渲染进程的完整链路保证,详见下文第四节。
四、主进程 SplashWindow:生命周期与显示策略
4.1 窗口创建与三段式显示门控
src/main/app/splashWindow.ts 中的SplashWindow类管理整个启动画面的生命周期。窗口创建后并不立即显示,而是等待三个条件同时满足(maybeShowSplash,第 381-393 行):
splashReadyToShow:BrowserWindow 触发ready-to-show;splashShowDelayElapsed:距离创建已过去 200ms(SPLASH_SHOW_DELAY_MS = 200,第 28 行);suppressSplashShow为 false(未被主窗口创建事件抑制)。
这套延迟机制用于避免启动画面一闪而过(白屏闪烁)或与主窗口出现竞态。forceShowSplash(第 395-424 行)则用于解锁请求场景:当数据库解锁/恢复请求到来时,跳过延迟立即显示(skipDelay: true),即使渲染进程尚未加载完成,也会在splashLoadPromise完成后补显。
4.2 渲染器加载的三级回退链
loadSplashRenderer(第 447-487 行)按优先级依次尝试三种加载来源:
- 开发模式下先尝试
ELECTRON_RENDERER_URL下的/splash/index.html与/splash/; - 其次加载打包后的本地文件
../renderer/splash/index.html; - 最后回退到
buildInlineFallbackSplashUrl()生成的内联data:text/html兜底页面(第 556-771 行)。
兜底页面内置了完整的手动解锁、恢复、系统解锁与加载四种 UI,并在密码提交逻辑上与正式页面保持一致——这意味着即便渲染资源加载失败,解锁流程依然可用。兜底页还通过.shell--manual-unlock类切换不透明背景,与正式实现的视觉语义相同。
4.3 关闭流程与平滑过渡
close()(第 225-257 行)默认会 resolve 未决的解锁/恢复请求(传null表示取消),并清理pendingUnlockProgress;如果窗口当前可见且未指定skipTransition,会先等待 500ms 再关闭,为主窗口出现留出平滑过渡时间。调试场景关闭则使用skipTransition: true立即关闭。
五、IPC 契约:解锁请求的完整闭环
规格文档强调"不得改变既有数据库解锁 IPC 行为",这些通道定义在 src/shared/contracts/databaseSecurity.ts:
export const DATABASE_UNLOCK_REQUEST_CHANNEL = 'database-security:unlock-request' export const DATABASE_UNLOCK_SUBMIT_CHANNEL = 'database-security:unlock-submit' export const DATABASE_UNLOCK_CANCEL_CHANNEL = 'database-security:unlock-cancel' export const DATABASE_UNLOCK_PROGRESS_CHANNEL = 'database-security:unlock-progress' export const DATABASE_RECOVERY_REQUEST_CHANNEL = 'database-security:recovery-request' export const DATABASE_RECOVERY_SUBMIT_CHANNEL = 'database-security:recovery-submit' export const DATABASE_RECOVERY_CANCEL_CHANNEL = 'database-security:recovery-cancel'对应的载荷类型同样由该文件定义:
export type DatabaseUnlockReason = | 'manual-required' | 'safe-storage-unavailable' | 'system-key-missing' | 'invalid' export type DatabaseUnlockRequestPayload = { requestId: string reason: DatabaseUnlockReason safeStorageAvailable: boolean } export type DatabaseUnlockProgressPayload = { active: boolean safeStorageAvailable: boolean } export type DatabaseRecoveryRequestPayload = { requestId: string kind: DatabaseStartupFailureKind // 'true-corruption' | 'unreadable' | 'orphaned-sidecar' preservedPath: string invalidPassword?: boolean quarantineFailed?: boolean }5.1 主进程侧:请求注册与 IPC 校验
主进程在requestDatabaseUnlock(splashWindow.ts)中为每次解锁生成带时间戳与随机数的requestId,将resolve回调挂入unlockRequest字段,随后forceShowSplash({ skipDelay: true })并广播状态。渲染进程提交密码后,监听器(第 270-336 行)会做三重校验:
- 发送者校验:
isSplashSender检查event.sender.id必须等于 splash 窗口的 webContents id,防止其他窗口伪造提交; - requestId 校验:载荷中的
requestId必须与当前挂起请求一致; - 类型校验:密码必须是 string 且非空。
满足条件后才调用resolve(password)或resolve(null)(取消),从而把 Promise 的结果交还给数据库启动流程。恢复请求的提交同样经过parseRecoverySubmit(第 793-811 行)对start-empty/password两种 action 做白名单解析。
5.2 preload 桥接:上下文隔离下的安全暴露
splash-preload.ts 通过contextBridge.exposeInMainWorld('deepchatSplash', splashApi)暴露最小 API,渲染进程window.deepchatSplash上只有六个事件订阅方法与四个提交/取消方法:
onUnlockRequest/onUnlockProgress/onRecoveryRequest/onDebugMode;submitUnlock/cancelUnlock/submitRecovery/cancelRecovery;getLanguageState(经 configGetLanguageRoute 获取语言状态,用于 splash 页 i18n)。
注意 preload 对提交载荷同样做了前置校验(如密码非 string 直接丢弃),未通过校验的消息不会发往主进程,形成第二道防线。窗口加载完成后还会通过webFrame.setVisualZoomLevelLimits(1, 1)与setZoomFactor(1)锁定缩放,保证 splash 布局尺寸精确。
5.3 渲染进程侧:事件驱动的状态机
渲染进程在onMounted中注册四个监听(loading.vue),由handleUnlockRequest、handleUnlockProgress、handleRecoveryRequest、handleDebugMode分别驱动状态迁移:
handleUnlockProgress({ active: true })→ 切到system-unlock,此时显示"Unlocking local database / reading the saved password from the system credential store";handleUnlockProgress({ active: false })且当前为system-unlock→ 回到loading;handleUnlockRequest→ 切到unlock,根据reason展示不同提示(system-key-missing提示凭据缺失需重新输入,safeStorageAvailable=false提示当前设备不支持系统解锁等);handleRecoveryRequest→ 切到recovery,按kind展示"数据库无法读取 / 残留 journal 文件 / 数据库损坏"等差异化文案,unreadable时聚焦密码框。
六、Logo 素材:受信任的本地 SVG 与路径级动画
约束条件要求"仅使用受信任的本地原始 SVG 素材"。两份 Logo 位于 src/renderer/src/assets/splash/logo-v3-dark.svg 与logo-v3-light.svg,通过 Vite 的?raw导入为字符串后经v-html内联:
import darkLogo from '@/assets/splash/logo-v3-dark.svg?raw' import lightLogo from '@/assets/splash/logo-v3-light.svg?raw'深色/浅色两套 Logo 分别对应prefers-color-scheme: dark/light(CSS 中.logo-mark--dark与.logo-mark--light互为显隐)。
更重要的是,SVG 内部把海豚 Logo 拆成了四个带 class 的独立路径:
.logo-wake(尾部水痕)、.logo-body(身体)、.logo-tail(尾巴)、.logo-eye(眼睛)
配合 CSS 的:deep(path)选择器,可以让每一条原始路径独立运动(模板注释明确写道:"Trusted local SVG sources are inlined so each original path can move independently")。例如尾巴折叠使用transform-box: view-box; transform-origin: 688px 515px定位旋转轴(第 673-676 行),组装完成后进入native-tail-idle与eye-blink的待机循环动画。
七、调试与测试:开发态场景预览
7.1 DEV-only 的调试路由
规格文档将"开发版 splash 预览控件"明确列为 Non-Goal,但仓库仍保留了开发态专用的调试入口。路由契约定义在 src/shared/contracts/routes/debug.routes.ts:
export const debugShowSplashScenarioRoute = defineRouteContract({ name: 'debug.showSplashScenario', input: z.object({ mode: z.enum(SPLASH_DEBUG_MODES) }), output: z.object({ shown: z.boolean() }) }) export const debugCloseSplashScenarioRoute = defineRouteContract({ name: 'debug.closeSplashScenario', input: z.object({}), output: z.object({ closed: z.boolean() }) })其中SPLASH_DEBUG_MODES定义于 src/shared/contracts/splash.ts:
export const SPLASH_DEBUG_MODES = ['loading', 'system-unlock', 'unlock', 'recovery'] as const export type SplashDebugMode = (typeof SPLASH_DEBUG_MODES)[number]路由处理位于 src/main/app/routes.ts:当import.meta.env.DEV且应用未打包(!app.isPackaged)时才真正调用splash.showDebugScenario(input.mode)并返回{ shown: true },否则直接返回{ shown: false }。调试模式下渲染进程会进入isDebugPreview = true,密码提交被禁用(password.disabled/submit.disabled),并在 hint 处显示"Development preview — password submission is disabled",从机制上杜绝调试误触提交。调试关闭走closeDebugScenario()(skipTransition: true立即关闭)。
7.2 自动化测试:显示契约的守护
启动画面的显示逻辑有专门的测试覆盖:test/main/app/splashWindow.display.test.ts。该测试使用 JSDOM + Mock BrowserWindow(mock 了show/focus/close/loadURL/loadFile/webContents.send等),验证包括:
- BrowserWindow 创建参数(透明、无边框、置顶、尺寸等)是否符合约束;
- 200ms 显示延迟与
ready-to-show门控; - 解锁/恢复请求到来时的
forceShowSplash强制显示路径; - IPC 监听器对非 splash 发送者、错误 requestId 的拒绝行为;
- 关闭流程对未决请求的 resolve 与过渡延迟。
这套测试把"保持窗口尺寸、形状、阴影与启动时序"这条约束从口头约定变成了可回归验证的契约。
八、约束清单回顾
规格文档的 Constraints 与实现一一对应:
| 约束 | 实现证据 |
|---|---|
| 透明窗口 + 透明画布;加载/系统解锁态使用透明文档根 | index.html 中html, body { background: transparent };splashWindow.ts 的transparent: true, backgroundColor: '#00000000' |
| 手动解锁与恢复保留不透明表单背景 | .unlock-panel--manual透明化,--orb圆形容器内嵌极光背景,兜底页用.shell--manual-unlock { background: #020817 } |
| 保留窗口尺寸、形状、阴影与启动时序 | 420×340、frame: false、alwaysOnTop、center、200ms 延迟门控、500ms 关闭过渡 |
| 仅使用受信任的本地原始 SVG | ?raw内联logo-v3-dark.svg/logo-v3-light.svg |
| 不泄露机密、不改解锁授权行为 | contextIsolation: true、最小 preload API、发送者 + requestId + 类型三重校验、IPC 通道与载荷契约原样保留 |
九、总结:一份可复用的启动画面架构
回到规格文档本身,Splash Experience 的 Non-Goals 划定了清晰的边界——不做开发预览控件、不改 splash 生命周期时序、不改数据库解锁架构。这意味着本次演进是一次纯视觉与信息架构层面的重构:窗口语义、IPC 契约、解锁授权逻辑全部保持不变,改动的只是渲染层如何表达状态。
从实现角度看,这套方案的价值在于三点:
- 视觉与安全职责合一:启动画面不只是品牌展示,还承载了系统解锁、手动解锁、数据库恢复三类安全交互,且通过圆形/矩形构图完成了语义传达;
- 分层清晰的进程协作:主进程(窗口生命周期 + IPC 校验)→ preload(最小桥接 API)→ 渲染进程(四态状态机 + CSS 动画),每一层职责单一、可独立测试;
- 健壮的降级策略:渲染资源加载失败时回退到内联 HTML,且兜底页面同样具备完整解锁能力。
对于需要在 Electron 应用中实现"品牌化启动画面 + 数据库安全解锁"的开发者,这套从规格到实现的完整链路值得直接借鉴。
相关文件索引
- 规格文档:docs/features/splash-experience/spec.md
- 主进程窗口管理:src/main/app/splashWindow.ts
- 渲染进程四态组件:src/renderer/splash/loading.vue
- 渲染入口与 HTML:src/renderer/splash/main.ts、src/renderer/splash/index.html
- preload 桥接:src/preload/splash-preload.ts
- IPC 契约:src/shared/contracts/databaseSecurity.ts
- 调试模式契约:src/shared/contracts/splash.ts、src/shared/contracts/routes/debug.routes.ts
- Logo 素材:src/renderer/src/assets/splash/logo-v3-dark.svg、
src/renderer/src/assets/splash/logo-v3-light.svg - 显示契约测试:test/main/app/splashWindow.display.test.ts
【免费下载链接】deepchat🐬DeepChat - A smart assistant that connects powerful AI to your personal world项目地址: https://gitcode.com/GitHub_Trending/dee/deepchat
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考