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](https://img.shields.io/badge/license-CC--BY--NC--SA--4.0-blue.svg)](LICENSE) -[![Version](https://img.shields.io/badge/version-2.3.4-green.svg)](package.json) +[![Version](https://img.shields.io/badge/version-2.3.5-green.svg)](package.json) [![Platform](https://img.shields.io/badge/platform-Windows-0078D6.svg?logo=windows)]() [![Electron](https://img.shields.io/badge/Electron-39-47848F.svg?logo=electron)]() [![React](https://img.shields.io/badge/React-19-61DAFB.svg?logo=react)]() 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 -