mirror of
https://mirror.skon.top/github.com/ILoveBingLu/CipherTalk
synced 2026-04-23 10:23:17 +08:00
chore: release 2.3.5
This commit is contained in:
19
.github/workflows/release.yml
vendored
19
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,6 +28,7 @@ dist-electron
|
||||
# Build output
|
||||
out
|
||||
release
|
||||
.tmp/release-announcement.json
|
||||
|
||||
# Database
|
||||
*.db
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
**一款现代化的微信聊天记录查看与分析工具**
|
||||
|
||||
[](LICENSE)
|
||||
[](package.json)
|
||||
[](package.json)
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 模式
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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": {
|
||||
|
||||
19
package.json
19
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": [
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
81
scripts/prepare-release-announcement.js
Normal file
81
scripts/prepare-release-announcement.js
Normal file
@@ -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()
|
||||
@@ -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: <Package size={20} />,
|
||||
title: '优化',
|
||||
desc: '优化html导出。'
|
||||
},
|
||||
{
|
||||
icon: <Package size={20} />,
|
||||
title: '优化',
|
||||
desc: '优化最小化至托盘功能。'
|
||||
}
|
||||
// {
|
||||
// icon: <Image size={20} />,
|
||||
// title: '聊天内图片',
|
||||
// desc: '支持查看谷歌标准实况图片(iOS端与大疆等实况图片,发送后实况暂不支持)。'
|
||||
// }
|
||||
// {
|
||||
// icon: <Mic size={20} />,
|
||||
// title: '语音导出',
|
||||
// desc: '支持将语音消息解码为 WAV 格式导出,含转写文字。'
|
||||
// },
|
||||
// {
|
||||
// icon: <Filter size={20} />,
|
||||
// title: '新增',
|
||||
// desc: '新增API端点等功能。'
|
||||
// },
|
||||
// {
|
||||
// icon: <Aperture size={20} />,
|
||||
// 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 <Aperture size={20} />
|
||||
if (/[新增|支持|加入|开放]/.test(text)) return <Sparkles size={20} />
|
||||
if (/[优化|提升|改进|性能]/.test(text)) return <Wand2 size={20} />
|
||||
return <Package size={20} />
|
||||
}
|
||||
|
||||
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: <Sparkles size={20} />,
|
||||
title: '版本上线',
|
||||
desc: `已切换到 ${version},界面与功能会自动按当前版本展示最新内容。`
|
||||
},
|
||||
{
|
||||
icon: <Wand2 size={20} />,
|
||||
title: '体验优化',
|
||||
desc: '我们会持续打磨性能、细节和稳定性,无需再为这条欢迎信息手动改文案。'
|
||||
},
|
||||
{
|
||||
icon: <Package size={20} />,
|
||||
title: '自动适配',
|
||||
desc: '如果发布说明存在,这里会优先自动展示本次更新要点。'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="whats-new-overlay">
|
||||
<div className="whats-new-modal">
|
||||
<div className="modal-header">
|
||||
<span className="version-tag">新版本 {version}</span>
|
||||
<h2>欢迎体验全新的密语</h2>
|
||||
<p>我们为您带来了一些令人兴奋的改进</p>
|
||||
</div>
|
||||
function buildHeadline(version: string, updates: UpdateItem[]) {
|
||||
if (updates.length > 0) {
|
||||
return {
|
||||
title: `密语 ${version} 已就绪`,
|
||||
subtitle: '以下是这次版本自动整理出的更新重点'
|
||||
}
|
||||
}
|
||||
|
||||
<div className="modal-content">
|
||||
<div className="update-list">
|
||||
{updates.map((item, index) => (
|
||||
<div className="update-item" key={index}>
|
||||
<div className="item-icon">
|
||||
{item.icon}
|
||||
</div>
|
||||
<div className="item-info">
|
||||
<h3>{item.title}</h3>
|
||||
<p>{item.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
return {
|
||||
title: `欢迎使用密语 ${version}`,
|
||||
subtitle: '当前版本已安装完成,以下内容会根据版本自动展示'
|
||||
}
|
||||
}
|
||||
|
||||
<div className="modal-footer">
|
||||
<button className="telegram-btn" onClick={handleTelegram}>
|
||||
<Send size={16} />
|
||||
加入 Telegram 频道
|
||||
</button>
|
||||
<button className="start-btn" onClick={onClose}>
|
||||
开启新旅程
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
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 (
|
||||
<div className="whats-new-overlay">
|
||||
<div className="whats-new-modal">
|
||||
<div className="modal-header">
|
||||
<span className="version-tag">新版本 {version}</span>
|
||||
<h2>{headline.title}</h2>
|
||||
<p>{headline.subtitle}</p>
|
||||
</div>
|
||||
)
|
||||
|
||||
<div className="modal-content">
|
||||
<div className="update-list">
|
||||
{items.map((item, index) => (
|
||||
<div className="update-item" key={`${item.title}-${index}`}>
|
||||
<div className="item-icon">
|
||||
{item.icon}
|
||||
</div>
|
||||
<div className="item-info">
|
||||
<h3>{item.title}</h3>
|
||||
<p>{item.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
<button className="telegram-btn" onClick={handleTelegram}>
|
||||
<Send size={16} />
|
||||
加入 Telegram 频道
|
||||
</button>
|
||||
<button className="start-btn" onClick={onClose}>
|
||||
开始使用
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WhatsNewModal
|
||||
|
||||
@@ -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 && (
|
||||
<WhatsNewModal
|
||||
version={currentVersion}
|
||||
releaseBody={releaseBody}
|
||||
releaseNotes={releaseNotes}
|
||||
onClose={handleCloseWhatsNew}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes progress-slide {
|
||||
0% {
|
||||
transform: translateX(-135%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(330%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div className={`splash-page ${fadeOut ? 'fade-out' : ''}`}>
|
||||
<div className="splash-orb splash-orb-left" />
|
||||
<div className="splash-orb splash-orb-right" />
|
||||
|
||||
<div className="splash-content">
|
||||
<div className="splash-logo">
|
||||
{/* 尝试加载logo图片,如果不存在则显示文字 */}
|
||||
<img
|
||||
src={appIcon === 'xinnian' ? "./xinnian.png" : "./logo.png"}
|
||||
alt="密语"
|
||||
onError={(e) => {
|
||||
// 如果图片加载失败,隐藏img,显示文字
|
||||
e.currentTarget.style.display = 'none'
|
||||
const textEl = e.currentTarget.nextElementSibling as HTMLElement
|
||||
if (textEl) textEl.style.display = 'block'
|
||||
}}
|
||||
/>
|
||||
<div className="logo-icon" style={{ display: 'none' }}>密语</div>
|
||||
<div className="splash-brand">
|
||||
<div className="splash-logo-shell">
|
||||
<div className="splash-logo-glow" />
|
||||
<img
|
||||
className="splash-logo-image"
|
||||
src="./logo.png"
|
||||
alt="密语"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none'
|
||||
const textEl = e.currentTarget.nextElementSibling as HTMLElement | null
|
||||
if (textEl) textEl.style.display = 'grid'
|
||||
}}
|
||||
/>
|
||||
<div className="splash-logo-fallback" style={{ display: 'none' }}>密语</div>
|
||||
</div>
|
||||
|
||||
<div className="splash-copy">
|
||||
<span className="splash-eyebrow">CipherTalk</span>
|
||||
<h1>密语</h1>
|
||||
<p>本地聊天记录分析工作台</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="splash-text">
|
||||
<Loader2 size={20} className="spin" />
|
||||
<span>正在连接数据库...</span>
|
||||
|
||||
<div className="splash-status">
|
||||
<div className="splash-status-row">
|
||||
<span className="splash-status-dot" />
|
||||
<span key={loadingMessages[messageIndex]} className="splash-status-text">
|
||||
{loadingMessages[messageIndex]}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="splash-progress-track" aria-hidden="true">
|
||||
<div className="splash-progress-bar" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,4 +83,3 @@ function SplashPage() {
|
||||
}
|
||||
|
||||
export default SplashPage
|
||||
|
||||
|
||||
Reference in New Issue
Block a user