feat: 重构「开发者愿景」弹窗内容与交互

将弹窗从「版本更新列表」改为固定的「开发者愿景」内容,展示三个核心主题:
记忆与亲情、证据与事实、归档与掌控。

- WhatsNewModal.tsx:移除动态解析发布说明的逻辑(parseAnnouncementText、
  buildFallbackUpdates 等),改为静态 VISION_SECTIONS 数据驱动渲染;
  移除 releaseBody / releaseNotes props,简化组件接口;
  Footer 新增 CipherTalk 品牌文案区域和「开始使用」箭头图标
This commit is contained in:
ILoveBingLu
2026-04-21 21:34:48 +08:00
parent f2b4129595
commit 46a284cba9
+107 -95
View File
@@ -1,93 +1,65 @@
import { ReactNode } from 'react'
import { Aperture, Package, Send, Sparkles, Wand2 } from 'lucide-react'
import { ArrowRight, Database, Heart, Quote, Scale, Send } from 'lucide-react'
import './WhatsNewModal.scss'
interface WhatsNewModalProps {
onClose: () => void
version: string
releaseBody?: string
releaseNotes?: string
}
type UpdateItem = {
type VisionSection = {
key: 'memory' | 'evidence' | 'ownership'
index: string
icon: ReactNode
kicker: string
title: string
desc: string
paragraphs: string[]
quote?: string
quoteSource?: string
note?: string
}
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: '如果发布说明存在,这里会优先自动展示本次更新要点。'
}
]
}
function buildHeadline(version: string, updates: UpdateItem[]) {
if (updates.length > 0) {
return {
title: `密语 ${version} 已就绪`,
subtitle: '以下是这次版本自动整理出的更新重点'
}
const VISION_SECTIONS: VisionSection[] = [
{
key: 'memory',
index: '01',
icon: <Heart size={20} />,
kicker: '记忆与亲情',
title: '为思念留下可以触摸的温度',
paragraphs: [
'当亲人离世后,曾经的点点滴滴往往都留在逝者的手机里,手机也成了继续思念的唯一入口。我希望这款软件能把这些记录整理为真正属于家人的数字资产。',
'一段反复叮嘱的文字,一条“儿子(闺女),爸(妈)想你了,啥时候回家呀,回来给你做你爱吃的!”的语音,一次平凡却再也无法重来的问候。'
],
quote: '死亡不是生命的终点,遗忘才是。',
quoteSource: '《寻梦环游记》',
note: '愿技术能替你留住一点声音、一点温度,也留住一点未曾说完的爱。'
},
{
key: 'evidence',
index: '02',
icon: <Scale size={20} />,
kicker: '证据与事实',
title: '为不公保留足够有力的证据',
paragraphs: [
'当您遭遇不公、不平、不正,甚至被聊天中的恶意、羞辱、威胁反复消耗时,您不该只能忍受。我希望这款软件能帮您从海量记录中快速找出关键证据。',
'把零散对话整理成清晰、完整、可追溯的事实链,让每一句伤害都有据可查,让每一次压迫都有证可举。'
],
note: '人可以善良,但不该没有反击的凭据。'
},
{
key: 'ownership',
index: '03',
icon: <Database size={20} />,
kicker: '归档与掌控',
title: '让聊天记录真正回到用户手中',
paragraphs: [
'我也希望这款软件能帮助更多普通人重新掌握自己的数字人生。聊天记录不该只是被困在某台设备里的碎片,它也可以是记忆的档案、关系的注脚、成长的年轮。',
'无论是回望过去、整理生活、备份重要信息,还是在关键时刻还原事实、保护自己,这些数据都应该真正属于用户,而不是在设备更换、账号异常或时间流逝中悄然消失。'
]
}
]
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)
function WhatsNewModal({ onClose, version }: WhatsNewModalProps) {
const handleTelegram = () => {
window.electronAPI?.shell?.openExternal?.('https://t.me/+p7YzmRMBm-gzNzJl')
}
@@ -96,35 +68,75 @@ function WhatsNewModal({ onClose, version, releaseBody, releaseNotes }: WhatsNew
<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>
<span className="version-tag"> · v{version}</span>
<h2></h2>
<p className="vision-lead"></p>
<p className="vision-intro">
</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 className="vision-opening">
<div className="opening-mark" aria-hidden="true" />
<p>
</p>
</div>
<div className="vision-list">
{VISION_SECTIONS.map((section) => (
<article className={`vision-card ${section.key}`} key={section.key}>
<div className="vision-card-top">
<span className="vision-index">{section.index}</span>
<div className="vision-icon" aria-hidden="true">
{section.icon}
</div>
<div className="vision-meta">
<span className="vision-kicker">{section.kicker}</span>
<h3>{section.title}</h3>
</div>
</div>
<div className="item-info">
<h3>{item.title}</h3>
<p>{item.desc}</p>
<div className="vision-body">
{section.paragraphs.map((paragraph) => (
<p key={paragraph}>{paragraph}</p>
))}
{section.quote && (
<blockquote className="vision-quote">
<div className="quote-icon" aria-hidden="true">
<Quote size={18} />
</div>
<p>{section.quote}</p>
{section.quoteSource && <cite>{section.quoteSource}</cite>}
</blockquote>
)}
{section.note && <p className="vision-note">{section.note}</p>}
</div>
</div>
</article>
))}
</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 className="footer-copy">
<span className="footer-label">CipherTalk</span>
<p></p>
</div>
<div className="footer-actions">
<button className="telegram-btn" onClick={handleTelegram}>
<Send size={16} />
Telegram
</button>
<button className="start-btn" onClick={onClose}>
使
<ArrowRight size={16} />
</button>
</div>
</div>
</div>
</div>