diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0e8ab1c..629d7a2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -82,6 +82,16 @@ jobs:
runs-on: windows-latest
environment: 软件发布
needs: prepare-meta
+ env:
+ GH_TOKEN: ${{ secrets.GH_TOKEN }}
+ AI_API_KEY: ${{ secrets.AI_API_KEY }}
+ AI_API_URL: ${{ vars.AI_API_URL }}
+ AI_MODEL: ${{ vars.AI_MODEL }}
+ FORCE_UPDATE_MIN_VERSION: ${{ vars.FORCE_UPDATE_MIN_VERSION }}
+ FORCE_UPDATE_BLOCKED_VERSIONS: ${{ vars.FORCE_UPDATE_BLOCKED_VERSIONS }}
+ FORCE_UPDATE_TITLE: ${{ vars.FORCE_UPDATE_TITLE }}
+ FORCE_UPDATE_MESSAGE: ${{ vars.FORCE_UPDATE_MESSAGE }}
+ FORCE_UPDATE_RELEASE_NOTES: ${{ vars.FORCE_UPDATE_RELEASE_NOTES }}
steps:
- name: Checkout
uses: actions/checkout@v5
@@ -98,6 +108,15 @@ jobs:
- name: Rebuild native modules
run: npx electron-rebuild
+ - name: Download release metadata
+ uses: actions/download-artifact@v4
+ with:
+ name: release-meta
+ path: release
+
+ - name: Generate embedded release body
+ run: npm run build:release-body
+
- name: Build app
run: npm run build:ci
diff --git a/.gitignore b/.gitignore
index ccaf71e..21af4c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,7 @@ dist-electron
# Build output
out
release
+.tmp/release-announcement.json
# Database
*.db
diff --git a/README.md b/README.md
index 2f12d3d..93d234b 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
**一款现代化的微信聊天记录查看与分析工具**
[](LICENSE)
-[](package.json)
+[](package.json)
[]()
[]()
[]()
diff --git a/electron/main.ts b/electron/main.ts
index 51bd1ea..e9be6ae 100644
--- a/electron/main.ts
+++ b/electron/main.ts
@@ -94,6 +94,64 @@ let welcomeWindow: BrowserWindow | null = null
let chatHistoryWindow: BrowserWindow | null = null
const allowDevTools = !!process.env.VITE_DEV_SERVER_URL
+type ReleaseAnnouncementPayload = {
+ version: string
+ releaseBody?: string
+ releaseNotes?: string
+ generatedAt?: string
+}
+
+function getReleaseAnnouncementPath(): string {
+ const isDev = !!process.env.VITE_DEV_SERVER_URL
+ return isDev
+ ? join(__dirname, '../.tmp/release-announcement.json')
+ : join(process.resourcesPath, 'release-announcement.json')
+}
+
+function syncPackagedReleaseAnnouncement() {
+ if (!configService) return
+
+ const announcementPath = getReleaseAnnouncementPath()
+ if (!existsSync(announcementPath)) {
+ return
+ }
+
+ try {
+ const raw = readFileSync(announcementPath, 'utf8')
+ const payload = JSON.parse(raw) as ReleaseAnnouncementPayload
+ if (!payload || typeof payload !== 'object') return
+
+ const version = String(payload.version || '').trim()
+ if (!version || version !== app.getVersion()) return
+
+ const releaseBody = String(payload.releaseBody || '').trim()
+ const releaseNotes = String(payload.releaseNotes || '').trim()
+
+ const storedVersion = configService.get('releaseAnnouncementVersion')
+ const storedBody = configService.get('releaseAnnouncementBody')
+ const storedNotes = configService.get('releaseAnnouncementNotes')
+
+ if (
+ storedVersion === version &&
+ storedBody === releaseBody &&
+ storedNotes === releaseNotes
+ ) {
+ return
+ }
+
+ configService.set('releaseAnnouncementVersion', version)
+ configService.set('releaseAnnouncementBody', releaseBody)
+ configService.set('releaseAnnouncementNotes', releaseNotes)
+ logService?.info('ReleaseAnnouncement', '已同步本地版本公告', {
+ version,
+ hasBody: Boolean(releaseBody),
+ hasNotes: Boolean(releaseNotes)
+ })
+ } catch (error) {
+ logService?.warn('ReleaseAnnouncement', '同步本地版本公告失败', { error: String(error) })
+ }
+}
+
/**
* 获取当前主题的 URL 查询参数
* 用于子窗口加载时传递主题,防止闪烁
@@ -203,6 +261,7 @@ function createWindow() {
dbService = new DatabaseService()
logService = new LogService(configService)
+ syncPackagedReleaseAnnouncement()
mcpProxyService.setLogger(logService)
autoUpdater.logger = {
info(message: string) {
diff --git a/electron/services/config.ts b/electron/services/config.ts
index ad2f72f..726d852 100644
--- a/electron/services/config.ts
+++ b/electron/services/config.ts
@@ -30,6 +30,10 @@ interface ConfigSchema {
themeMode: string
appIcon: string
language: string
+ releaseAnnouncementVersion: string
+ releaseAnnouncementBody: string
+ releaseAnnouncementNotes: string
+ releaseAnnouncementSeenVersion: string
// 协议相关
agreementVersion: number
@@ -99,6 +103,10 @@ const defaults: ConfigSchema = {
themeMode: 'light',
appIcon: 'default',
language: 'zh-CN',
+ releaseAnnouncementVersion: '',
+ releaseAnnouncementBody: '',
+ releaseAnnouncementNotes: '',
+ releaseAnnouncementSeenVersion: '',
sttLanguages: ['zh'],
sttModelType: 'int8',
sttMode: 'cpu', // 默认使用 CPU 模式
diff --git a/package-lock.json b/package-lock.json
index 6c28a1e..3ed1ef9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "ciphertalk",
- "version": "2.3.3",
+ "version": "2.3.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ciphertalk",
- "version": "2.3.3",
+ "version": "2.3.5",
"hasInstallScript": true,
"license": "CC-BY-NC-SA-4.0",
"dependencies": {
diff --git a/package.json b/package.json
index 305d0b9..3ebd0e2 100644
--- a/package.json
+++ b/package.json
@@ -1,19 +1,20 @@
{
"name": "ciphertalk",
- "version": "2.3.4",
+ "version": "2.3.5",
"description": "密语 - 微信聊天记录查看工具",
"author": "ILoveBingLu",
"license": "CC-BY-NC-SA-4.0",
"main": "dist-electron/main.js",
"scripts": {
"dev": "vite",
- "prebuild": "node scripts/update-readme-version.js",
+ "prebuild": "node scripts/update-readme-version.js && node scripts/prepare-release-announcement.js",
"build": "tsc && vite build && electron-builder && node scripts/add-size-to-yml.js",
- "build:ci": "tsc && vite build && electron-builder --publish never && node scripts/add-size-to-yml.js",
+ "build:ci": "node scripts/prepare-release-announcement.js && tsc && vite build && electron-builder --publish never && node scripts/add-size-to-yml.js",
"build:mcp": "tsc && vite build",
"build:force-update-manifest": "node scripts/generate-force-update-manifest.js",
"build:release-context": "node scripts/generate-release-context.js",
"build:release-body": "node scripts/generate-release-body.js",
+ "build:release-announcement": "node scripts/prepare-release-announcement.js",
"notify:telegram": "node scripts/send-telegram-release.js",
"mcp": "node scripts/mcp-runner.js",
"mcp:probe": "node scripts/mcp-probe.js",
@@ -95,7 +96,7 @@
"repo": "CipherTalk"
},
"win": {
- "icon": "public/xinnian.ico",
+ "icon": "public/icon.ico",
"target": "nsis",
"requestedExecutionLevel": "asInvoker"
},
@@ -112,9 +113,9 @@
"language": "2052",
"displayLanguageSelector": false,
"include": "installer.nsh",
- "installerIcon": "public/xinnian.ico",
- "uninstallerIcon": "public/xinnian.ico",
- "installerHeaderIcon": "public/xinnian.ico",
+ "installerIcon": "public/icon.ico",
+ "uninstallerIcon": "public/icon.ico",
+ "installerHeaderIcon": "public/icon.ico",
"perMachine": false,
"allowElevation": true,
"installerSidebar": null,
@@ -143,6 +144,10 @@
{
"from": "public/xinnian.ico",
"to": "xinnian.ico"
+ },
+ {
+ "from": ".tmp/release-announcement.json",
+ "to": "release-announcement.json"
}
],
"extraFiles": [
diff --git a/scripts/add-size-to-yml.js b/scripts/add-size-to-yml.js
index 5946ca4..bb8d5cc 100644
--- a/scripts/add-size-to-yml.js
+++ b/scripts/add-size-to-yml.js
@@ -10,7 +10,7 @@ if (!fs.existsSync(ymlPath)) {
}
// 读取 yml 内容
-let content = fs.readFileSync(ymlPath, 'utf-8')
+const content = fs.readFileSync(ymlPath, 'utf-8')
const lines = content.split('\n')
// 从 yml 中提取文件名
@@ -32,10 +32,11 @@ if (!fs.existsSync(exePath)) {
const stats = fs.statSync(exePath)
const size = stats.size
-// 找到 files 块内第一个 sha512 行,在其后插入 size
+// electron-builder 新版本已经会生成 files[0].size,这里只在缺失时补齐,避免写出重复键
const newLines = []
let inFiles = false
let sizeAdded = false
+let fileItemIndent = ''
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
@@ -43,11 +44,36 @@ for (let i = 0; i < lines.length; i++) {
if (line.startsWith('files:')) {
inFiles = true
+ fileItemIndent = ''
+ continue
}
-
+
+ if (!inFiles) {
+ continue
+ }
+
+ const trimmed = line.trim()
+ const indent = line.match(/^\s*/)?.[0] || ''
+
+ if (trimmed.startsWith('- ')) {
+ fileItemIndent = `${indent} `
+ continue
+ }
+
+ if (trimmed.startsWith('size:')) {
+ console.log('latest.yml 已包含 size,跳过写入')
+ process.exit(0)
+ }
+
+ // 离开 files 块
+ if (trimmed && !line.startsWith(' ') && !line.startsWith('\t')) {
+ inFiles = false
+ continue
+ }
+
// 在 files 块内的第一个 sha512 后添加 size
- if (inFiles && !sizeAdded && line.trim().startsWith('sha512:')) {
- newLines.push(` size: ${size}`)
+ if (!sizeAdded && trimmed.startsWith('sha512:')) {
+ newLines.push(`${fileItemIndent || ' '}size: ${size}`)
sizeAdded = true
inFiles = false
}
diff --git a/scripts/prepare-release-announcement.js b/scripts/prepare-release-announcement.js
new file mode 100644
index 0000000..503c724
--- /dev/null
+++ b/scripts/prepare-release-announcement.js
@@ -0,0 +1,81 @@
+const fs = require('fs')
+const path = require('path')
+
+const rootDir = path.resolve(__dirname, '..')
+const releaseDir = path.join(rootDir, 'release')
+const tempDir = path.join(rootDir, '.tmp')
+const bodyPath = path.join(releaseDir, 'release-body.md')
+const forceUpdatePath = path.join(releaseDir, 'force-update.json')
+const outputPath = path.join(tempDir, 'release-announcement.json')
+const packageJsonPath = path.join(rootDir, 'package.json')
+
+function readJsonIfExists(filePath) {
+ if (!fs.existsSync(filePath)) return null
+ try {
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'))
+ } catch (error) {
+ console.warn(`[ReleaseAnnouncement] 读取 JSON 失败: ${filePath}`, String(error))
+ return null
+ }
+}
+
+function readTextIfExists(filePath) {
+ if (!fs.existsSync(filePath)) return ''
+ try {
+ return fs.readFileSync(filePath, 'utf8').trim()
+ } catch (error) {
+ console.warn(`[ReleaseAnnouncement] 读取文本失败: ${filePath}`, String(error))
+ return ''
+ }
+}
+
+function buildFallbackBody(version, releaseNotes) {
+ const normalizedNotes = String(releaseNotes || '').trim()
+ const overview = normalizedNotes
+ ? normalizedNotes
+ .split(/\r?\n/)
+ .map((line) => line.trim())
+ .filter(Boolean)
+ .map((line) => (line.startsWith('-') || line.startsWith('*') ? line : `- ${line}`))
+ .join('\n')
+ : '- 本次版本已完成发布,详细内容将在后续发布说明中补充。'
+
+ return [
+ `## CipherTalk v${version}`,
+ '',
+ '### 概览',
+ overview,
+ '',
+ '### 感谢贡献者',
+ '- 感谢每一位使用与反馈的用户',
+ '',
+ '### 相关提交与 PR',
+ '- 详见本次发布记录',
+ ''
+ ].join('\n')
+}
+
+function main() {
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
+ const version = String(pkg.version || '').trim()
+ if (!version) {
+ throw new Error('package.json 中未找到 version')
+ }
+
+ const releaseBody = readTextIfExists(bodyPath)
+ const forceUpdate = readJsonIfExists(forceUpdatePath) || {}
+ const releaseNotes = String(forceUpdate.releaseNotes || '').trim()
+
+ const payload = {
+ version,
+ releaseBody: releaseBody || buildFallbackBody(version, releaseNotes),
+ releaseNotes: releaseNotes || releaseBody || '',
+ generatedAt: new Date().toISOString()
+ }
+
+ fs.mkdirSync(tempDir, { recursive: true })
+ fs.writeFileSync(outputPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8')
+ console.log(`[ReleaseAnnouncement] 已生成 ${outputPath}`)
+}
+
+main()
diff --git a/src/components/WhatsNewModal.tsx b/src/components/WhatsNewModal.tsx
index 17268da..abfe9c8 100644
--- a/src/components/WhatsNewModal.tsx
+++ b/src/components/WhatsNewModal.tsx
@@ -1,87 +1,134 @@
-//更新说明!!!
-import { Package, Image, Mic, Filter, Send, Aperture } from 'lucide-react'
+import { ReactNode } from 'react'
+import { Aperture, Package, Send, Sparkles, Wand2 } from 'lucide-react'
import './WhatsNewModal.scss'
interface WhatsNewModalProps {
- onClose: () => void
- version: string
+ onClose: () => void
+ version: string
+ releaseBody?: string
+ releaseNotes?: string
}
-function WhatsNewModal({ onClose, version }: WhatsNewModalProps) {
- const updates = [
- {
- icon: ,
- title: '优化',
- desc: '优化html导出。'
- },
- {
- icon: ,
- title: '优化',
- desc: '优化最小化至托盘功能。'
- }
- // {
- // icon: ,
- // title: '聊天内图片',
- // desc: '支持查看谷歌标准实况图片(iOS端与大疆等实况图片,发送后实况暂不支持)。'
- // }
- // {
- // icon: ,
- // title: '语音导出',
- // desc: '支持将语音消息解码为 WAV 格式导出,含转写文字。'
- // },
- // {
- // icon: ,
- // title: '新增',
- // desc: '新增API端点等功能。'
- // },
- // {
- // icon: ,
- // title: '朋友圈',
- // desc: '优化样式!'
- // }
- ]
+type UpdateItem = {
+ icon: ReactNode
+ title: string
+ desc: string
+}
- const handleTelegram = () => {
- window.electronAPI?.shell?.openExternal?.('https://t.me/+p7YzmRMBm-gzNzJl')
+function inferTitle(text: string): string {
+ if (/[修复|稳定|兼容|解决]/.test(text)) return '修复'
+ if (/[优化|提升|改进|性能]/.test(text)) return '优化'
+ if (/[新增|支持|加入|开放]/.test(text)) return '新增'
+ return '更新'
+}
+
+function inferIcon(text: string): ReactNode {
+ if (/[界面|动画|视觉|样式|体验]/.test(text)) return
+ if (/[新增|支持|加入|开放]/.test(text)) return
+ if (/[优化|提升|改进|性能]/.test(text)) return
+ return
+}
+
+function parseAnnouncementText(content?: string): UpdateItem[] {
+ if (!content?.trim()) return []
+
+ const lines = content
+ .split(/\r?\n/)
+ .map(line => line.trim())
+ .filter(line => line && !/^#+\s*/.test(line) && !/^\d+\.\s*$/.test(line))
+ .map(line => line.replace(/^[-*•]\s*/, ''))
+ .filter(Boolean)
+ .slice(0, 5)
+
+ return lines.map((line) => ({
+ icon: inferIcon(line),
+ title: inferTitle(line),
+ desc: line
+ }))
+}
+
+function buildFallbackUpdates(version: string): UpdateItem[] {
+ return [
+ {
+ icon: ,
+ title: '版本上线',
+ desc: `已切换到 ${version},界面与功能会自动按当前版本展示最新内容。`
+ },
+ {
+ icon: ,
+ title: '体验优化',
+ desc: '我们会持续打磨性能、细节和稳定性,无需再为这条欢迎信息手动改文案。'
+ },
+ {
+ icon: ,
+ title: '自动适配',
+ desc: '如果发布说明存在,这里会优先自动展示本次更新要点。'
}
+ ]
+}
- return (
-
-
-
-
新版本 {version}
-
欢迎体验全新的密语
-
我们为您带来了一些令人兴奋的改进
-
+function buildHeadline(version: string, updates: UpdateItem[]) {
+ if (updates.length > 0) {
+ return {
+ title: `密语 ${version} 已就绪`,
+ subtitle: '以下是这次版本自动整理出的更新重点'
+ }
+ }
-
-
- {updates.map((item, index) => (
-
-
- {item.icon}
-
-
-
{item.title}
-
{item.desc}
-
-
- ))}
-
-
+ return {
+ title: `欢迎使用密语 ${version}`,
+ subtitle: '当前版本已安装完成,以下内容会根据版本自动展示'
+ }
+}
-
-
-
-
-
+function WhatsNewModal({ onClose, version, releaseBody, releaseNotes }: WhatsNewModalProps) {
+ const notesUpdates = parseAnnouncementText(releaseNotes)
+ const bodyUpdates = parseAnnouncementText(releaseBody)
+ const parsedUpdates = notesUpdates.length > 0 ? notesUpdates : bodyUpdates
+ const items = parsedUpdates.length > 0 ? parsedUpdates : buildFallbackUpdates(version)
+ const headline = buildHeadline(version, parsedUpdates)
+
+ const handleTelegram = () => {
+ window.electronAPI?.shell?.openExternal?.('https://t.me/+p7YzmRMBm-gzNzJl')
+ }
+
+ return (
+
+
+
+
新版本 {version}
+
{headline.title}
+
{headline.subtitle}
- )
+
+
+
+ {items.map((item, index) => (
+
+
+ {item.icon}
+
+
+
{item.title}
+
{item.desc}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+ )
}
export default WhatsNewModal
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
index 9423fce..5028760 100644
--- a/src/pages/HomePage.tsx
+++ b/src/pages/HomePage.tsx
@@ -27,6 +27,8 @@ function HomePage() {
// 新版本弹窗状态
const [showWhatsNew, setShowWhatsNew] = useState(false)
const [currentVersion, setCurrentVersion] = useState('')
+ const [releaseBody, setReleaseBody] = useState('')
+ const [releaseNotes, setReleaseNotes] = useState('')
useEffect(() => {
checkNewVersion()
@@ -61,20 +63,32 @@ function HomePage() {
const checkNewVersion = async () => {
try {
- // 获取当前应用版本
const version = await window.electronAPI.app.getVersion()
setCurrentVersion(version)
- // 获取上次查看的版本
- const lastSeenVersion = localStorage.getItem('lastSeenVersion')
+ const [
+ announcementVersion,
+ announcementBody,
+ announcementNotes,
+ seenVersion
+ ] = await Promise.all([
+ window.electronAPI.config.get('releaseAnnouncementVersion'),
+ window.electronAPI.config.get('releaseAnnouncementBody'),
+ window.electronAPI.config.get('releaseAnnouncementNotes'),
+ window.electronAPI.config.get('releaseAnnouncementSeenVersion')
+ ])
- // 简单的版本比较逻辑:如果版本不同且未记录,或者是新安装,则显示
- // 为了防止每次开发时版本号不变也弹,这里只在版本确实不同时弹
- // 注意:这里假设版本号格式为 x.y.z
+ const normalizedAnnouncementVersion = String(announcementVersion || '').trim()
+ const normalizedBody = String(announcementBody || '').trim()
+ const normalizedNotes = String(announcementNotes || '').trim()
+ const normalizedSeenVersion = String(seenVersion || '').trim()
- if (version !== lastSeenVersion) {
- // 如果是全新安装(没有 lastSeenVersion),也显示
- // 实际上这通常用于引导新用户
+ if (normalizedAnnouncementVersion === version) {
+ setReleaseBody(normalizedBody)
+ setReleaseNotes(normalizedNotes)
+ }
+
+ if (normalizedAnnouncementVersion === version && normalizedSeenVersion !== version) {
setShowWhatsNew(true)
}
} catch (e) {
@@ -85,7 +99,7 @@ function HomePage() {
const handleCloseWhatsNew = () => {
setShowWhatsNew(false)
if (currentVersion) {
- localStorage.setItem('lastSeenVersion', currentVersion)
+ window.electronAPI.config.set('releaseAnnouncementSeenVersion', currentVersion)
}
}
@@ -119,6 +133,8 @@ function HomePage() {
{showWhatsNew && (
)}
diff --git a/src/pages/SplashPage.scss b/src/pages/SplashPage.scss
index 462b451..e0f983c 100644
--- a/src/pages/SplashPage.scss
+++ b/src/pages/SplashPage.scss
@@ -1,108 +1,279 @@
-// 确保启动屏窗口背景透明(让 Electron 的 transparent: true 生效)
html,
body {
background: transparent !important;
- overflow: hidden !important; // 隐藏滚动条
+ overflow: hidden !important;
}
.splash-page {
- width: calc(100% - 24px); // 减去外边距
- height: calc(100vh - 24px); // 减去外边距
- margin: 12px; // 添加外边距,让圆角更明显
- display: flex;
- align-items: center;
- justify-content: center;
- background: linear-gradient(135deg, #F0EEE9 0%, #E8E6E1 100%);
- color: #3d3d3d;
- overflow: hidden;
position: relative;
- border-radius: 24px; // 大圆角
- box-shadow: 0 8px 40px rgba(0, 0, 0, 0.15); // 添加阴影效果
-
- // 入场动画
- animation: fadeIn 0.4s ease-out;
+ width: calc(100% - 24px);
+ height: calc(100vh - 24px);
+ margin: 12px;
+ overflow: hidden;
+ border-radius: 28px;
+ background:
+ radial-gradient(circle at top left, rgba(255, 226, 184, 0.72), transparent 38%),
+ radial-gradient(circle at 85% 18%, rgba(255, 153, 111, 0.24), transparent 26%),
+ linear-gradient(145deg, #fbf5ea 0%, #f3eadc 38%, #ece2d4 100%);
+ box-shadow:
+ 0 28px 70px rgba(71, 43, 20, 0.18),
+ inset 0 1px 0 rgba(255, 255, 255, 0.55);
+ animation: splash-enter 0.55s cubic-bezier(0.22, 1, 0.36, 1);
&::before {
content: '';
position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: radial-gradient(circle at 50% 50%, rgba(139, 115, 85, 0.05) 0%, transparent 70%);
+ inset: 0;
+ background:
+ linear-gradient(125deg, rgba(255, 255, 255, 0.28), transparent 30%, transparent 70%, rgba(122, 71, 35, 0.08)),
+ repeating-linear-gradient(
+ 135deg,
+ rgba(122, 71, 35, 0.03) 0,
+ rgba(122, 71, 35, 0.03) 1px,
+ transparent 1px,
+ transparent 18px
+ );
pointer-events: none;
}
- // 退出动画类
&.fade-out {
- animation: fadeOut 0.3s ease-in forwards;
+ animation: splash-exit 0.28s ease-in forwards;
+ }
+}
+
+.splash-content {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: 44px 42px 34px;
+}
+
+.splash-orb {
+ position: absolute;
+ border-radius: 999px;
+ filter: blur(8px);
+ opacity: 0.8;
+ pointer-events: none;
+}
+
+.splash-orb-left {
+ top: 48px;
+ left: 56px;
+ width: 180px;
+ height: 180px;
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.92) 0%, rgba(255, 232, 197, 0.16) 72%, transparent 100%);
+ animation: float-orb 6s ease-in-out infinite;
+}
+
+.splash-orb-right {
+ right: 38px;
+ bottom: 54px;
+ width: 240px;
+ height: 240px;
+ background: radial-gradient(circle, rgba(222, 140, 80, 0.24) 0%, rgba(222, 140, 80, 0.08) 52%, transparent 100%);
+ animation: float-orb 7.5s ease-in-out infinite reverse;
+}
+
+.splash-brand {
+ display: flex;
+ align-items: center;
+ gap: 22px;
+ position: relative;
+ z-index: 1;
+}
+
+.splash-logo-shell {
+ position: relative;
+ width: 110px;
+ height: 110px;
+ display: grid;
+ place-items: center;
+ flex: 0 0 auto;
+ border-radius: 32px;
+ background: linear-gradient(160deg, rgba(255, 255, 255, 0.74), rgba(247, 233, 210, 0.64));
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.92),
+ 0 18px 40px rgba(116, 72, 36, 0.14);
+ backdrop-filter: blur(10px);
+}
+
+.splash-logo-glow {
+ position: absolute;
+ inset: 16px;
+ border-radius: 22px;
+ background: radial-gradient(circle, rgba(255, 196, 129, 0.42) 0%, transparent 72%);
+ animation: logo-breathe 3.2s ease-in-out infinite;
+}
+
+.splash-logo-image,
+.splash-logo-fallback {
+ position: relative;
+ z-index: 1;
+}
+
+.splash-logo-image {
+ width: 74px;
+ height: 74px;
+ object-fit: contain;
+ filter: drop-shadow(0 10px 20px rgba(122, 71, 35, 0.14));
+ animation: logo-drift 3.8s ease-in-out infinite;
+}
+
+.splash-logo-fallback {
+ width: 74px;
+ height: 74px;
+ display: grid;
+ place-items: center;
+ font-size: 28px;
+ font-weight: 700;
+ color: #81512d;
+ letter-spacing: 4px;
+}
+
+.splash-copy {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ color: #49311f;
+
+ h1 {
+ margin: 0;
+ font-size: 36px;
+ line-height: 1;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ }
+
+ p {
+ margin: 0;
+ font-size: 14px;
+ color: rgba(73, 49, 31, 0.68);
+ letter-spacing: 0.04em;
+ }
+}
+
+.splash-eyebrow {
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.28em;
+ text-transform: uppercase;
+ color: rgba(129, 81, 45, 0.72);
+}
+
+.splash-status {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ position: relative;
+ z-index: 1;
+ padding-top: 18px;
+}
+
+.splash-status::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: min(220px, 34vw);
+ height: 1px;
+ background: linear-gradient(90deg, rgba(120, 77, 44, 0.28), rgba(120, 77, 44, 0));
+}
+
+.splash-status-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ min-height: 20px;
+}
+
+.splash-status-dot {
+ width: 9px;
+ height: 9px;
+ flex: 0 0 auto;
+ border-radius: 999px;
+ background: linear-gradient(135deg, #ff9d54, #d96b2d);
+ box-shadow: 0 0 0 6px rgba(217, 107, 45, 0.12);
+ animation: status-pulse 1.8s ease-in-out infinite;
+}
+
+.splash-status-text {
+ font-size: 14px;
+ font-weight: 600;
+ color: rgba(73, 49, 31, 0.82);
+ animation: status-fade 220ms ease-out;
+}
+
+.splash-progress-track {
+ position: relative;
+ height: 4px;
+ overflow: hidden;
+ border-radius: 999px;
+ background: rgba(108, 69, 39, 0.12);
+ max-width: 260px;
+}
+
+.splash-progress-bar {
+ position: absolute;
+ inset: 0;
+ width: 42%;
+ border-radius: inherit;
+ background: linear-gradient(90deg, rgba(255, 161, 98, 0) 0%, #ffb36b 22%, #d96b2d 78%, rgba(217, 107, 45, 0) 100%);
+ animation: progress-slide 1.8s cubic-bezier(0.4, 0, 0.2, 1) infinite;
+}
+
+@media (max-width: 560px) {
+ .splash-page {
+ width: calc(100% - 16px);
+ height: calc(100vh - 16px);
+ margin: 8px;
+ border-radius: 22px;
}
.splash-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 32px;
- z-index: 1;
+ padding: 28px 24px 24px;
}
- .splash-logo {
- display: flex;
- align-items: center;
- justify-content: center;
-
- .logo-icon {
- font-size: 36px;
- font-weight: 700;
- letter-spacing: 2px;
- color: #8B7355;
- background: linear-gradient(135deg, #8B7355 0%, #A68B5B 100%);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- // 每2秒翻转一次
- animation: flip 2s ease-in-out infinite;
- }
-
- // logo 图片每2秒翻转一次
- img {
- width: 100px;
- height: 100px;
- object-fit: contain;
- animation: flip 2s ease-in-out infinite;
- }
+ .splash-brand {
+ gap: 16px;
+ align-items: flex-start;
}
- .splash-text {
- display: flex;
- align-items: center;
- gap: 12px;
- font-size: 15px;
- font-weight: 500;
- color: #666666;
+ .splash-logo-shell {
+ width: 92px;
+ height: 92px;
+ border-radius: 24px;
+ }
- .spin {
- animation: spin 1s linear infinite;
- color: #8B7355;
- }
+ .splash-logo-image,
+ .splash-logo-fallback {
+ width: 62px;
+ height: 62px;
+ }
+
+ .splash-copy h1 {
+ font-size: 28px;
+ }
+
+ .splash-progress-track {
+ max-width: 180px;
}
}
-// 入场淡入动画
-@keyframes fadeIn {
+@keyframes splash-enter {
from {
opacity: 0;
- transform: scale(0.95);
+ transform: scale(0.97) translateY(10px);
}
to {
opacity: 1;
- transform: scale(1);
+ transform: scale(1) translateY(0);
}
}
-// 退出淡出动画
-@keyframes fadeOut {
+@keyframes splash-exit {
from {
opacity: 1;
transform: scale(1);
@@ -110,34 +281,76 @@ body {
to {
opacity: 0;
- transform: scale(0.95);
+ transform: scale(0.985) translateY(8px);
}
}
-// 翻转动画(每2秒翻转360度,中间有停顿)
-@keyframes flip {
-
+@keyframes float-orb {
0%,
- 25% {
- transform: rotateY(0deg);
+ 100% {
+ transform: translate3d(0, 0, 0);
}
50% {
- transform: rotateY(180deg);
- }
-
- 75%,
- 100% {
- transform: rotateY(360deg);
+ transform: translate3d(10px, -12px, 0);
}
}
-@keyframes spin {
+@keyframes logo-breathe {
+ 0%,
+ 100% {
+ transform: scale(0.94);
+ opacity: 0.6;
+ }
+
+ 50% {
+ transform: scale(1.04);
+ opacity: 1;
+ }
+}
+
+@keyframes logo-drift {
+ 0%,
+ 100% {
+ transform: translateY(0);
+ }
+
+ 50% {
+ transform: translateY(-4px);
+ }
+}
+
+@keyframes status-pulse {
+ 0%,
+ 100% {
+ transform: scale(1);
+ box-shadow: 0 0 0 6px rgba(217, 107, 45, 0.12);
+ }
+
+ 50% {
+ transform: scale(1.08);
+ box-shadow: 0 0 0 9px rgba(217, 107, 45, 0.08);
+ }
+}
+
+@keyframes status-fade {
from {
- transform: rotate(0deg);
+ opacity: 0;
+ transform: translateY(4px);
}
to {
- transform: rotate(360deg);
+ opacity: 1;
+ transform: translateY(0);
}
-}
\ No newline at end of file
+}
+
+@keyframes progress-slide {
+ 0% {
+ transform: translateX(-135%);
+ }
+
+ 100% {
+ transform: translateX(330%);
+ }
+}
diff --git a/src/pages/SplashPage.tsx b/src/pages/SplashPage.tsx
index a3321e9..d104581 100644
--- a/src/pages/SplashPage.tsx
+++ b/src/pages/SplashPage.tsx
@@ -1,15 +1,17 @@
import { useEffect, useState } from 'react'
-import { Loader2 } from 'lucide-react'
import './SplashPage.scss'
-import { useThemeStore } from '../stores/themeStore'
+const loadingMessages = [
+ '正在校验本地环境',
+ '正在连接数据库',
+ '正在整理聊天索引'
+]
function SplashPage() {
const [fadeOut, setFadeOut] = useState(false)
- const appIcon = useThemeStore(state => state.appIcon)
+ const [messageIndex, setMessageIndex] = useState(0)
useEffect(() => {
- // 等待入场动画完成后再通知主进程(入场动画 0.4s + 额外停留 0.6s = 1s)
const readyTimer = setTimeout(() => {
try {
// @ts-ignore - splashReady 方法在运行时可用
@@ -19,37 +21,61 @@ function SplashPage() {
}
}, 1000)
- // 监听淡出事件
+ const messageTimer = setInterval(() => {
+ setMessageIndex((prev) => (prev + 1) % loadingMessages.length)
+ }, 1600)
+
const cleanup = window.electronAPI?.window?.onSplashFadeOut?.(() => {
setFadeOut(true)
})
return () => {
clearTimeout(readyTimer)
+ clearInterval(messageTimer)
cleanup?.()
}
}, [])
return (
+
+
+
-
- {/* 尝试加载logo图片,如果不存在则显示文字 */}
-

{
- // 如果图片加载失败,隐藏img,显示文字
- e.currentTarget.style.display = 'none'
- const textEl = e.currentTarget.nextElementSibling as HTMLElement
- if (textEl) textEl.style.display = 'block'
- }}
- />
-
密语
+
+
+
+

{
+ e.currentTarget.style.display = 'none'
+ const textEl = e.currentTarget.nextElementSibling as HTMLElement | null
+ if (textEl) textEl.style.display = 'grid'
+ }}
+ />
+
密语
+
+
+
+
CipherTalk
+
密语
+
本地聊天记录分析工作台
+
-
-
-
正在连接数据库...
+
+
+
+
+
+ {loadingMessages[messageIndex]}
+
+
+
+
@@ -57,4 +83,3 @@ function SplashPage() {
}
export default SplashPage
-