diff --git a/electron/main.ts b/electron/main.ts index 2e635b6..ab9128d 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -82,6 +82,8 @@ let configService: ConfigService | null = null // 协议窗口实例 let agreementWindow: BrowserWindow | null = null let onboardingWindow: BrowserWindow | null = null +// Splash 启动窗口 +let splashWindow: BrowserWindow | null = null const keyService = new KeyService() let mainWindowReady = false @@ -122,9 +124,10 @@ function createWindow(options: { autoShow?: boolean } = {}) { }) // 窗口准备好后显示 + // Splash 模式下不在这里 show,由启动流程统一控制 win.once('ready-to-show', () => { mainWindowReady = true - if (autoShow || shouldShowMain) { + if (autoShow && !splashWindow) { win.show() } }) @@ -250,6 +253,73 @@ function createAgreementWindow() { return agreementWindow } +/** + * 创建 Splash 启动窗口 + * 使用纯 HTML 页面,不依赖 React,确保极速显示 + */ +function createSplashWindow(): BrowserWindow { + const isDev = !!process.env.VITE_DEV_SERVER_URL + const iconPath = isDev + ? join(__dirname, '../public/icon.ico') + : join(process.resourcesPath, 'icon.ico') + + splashWindow = new BrowserWindow({ + width: 760, + height: 460, + resizable: false, + frame: false, + transparent: true, + backgroundColor: '#00000000', + hasShadow: false, + center: true, + skipTaskbar: false, + icon: iconPath, + webPreferences: { + contextIsolation: true, + nodeIntegration: false + // 不需要 preload —— 通过 executeJavaScript 单向推送进度 + }, + show: false + }) + + if (isDev) { + splashWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}splash.html`) + } else { + splashWindow.loadFile(join(__dirname, '../dist/splash.html')) + } + + splashWindow.once('ready-to-show', () => { + splashWindow?.show() + }) + + splashWindow.on('closed', () => { + splashWindow = null + }) + + return splashWindow +} + +/** + * 向 Splash 窗口发送进度更新 + */ +function updateSplashProgress(percent: number, text: string, indeterminate = false) { + if (splashWindow && !splashWindow.isDestroyed()) { + splashWindow.webContents + .executeJavaScript(`updateProgress(${percent}, ${JSON.stringify(text)}, ${indeterminate})`) + .catch(() => {}) + } +} + +/** + * 关闭 Splash 窗口 + */ +function closeSplash() { + if (splashWindow && !splashWindow.isDestroyed()) { + splashWindow.close() + splashWindow = null + } +} + /** * 创建首次引导窗口 */ @@ -1508,26 +1578,70 @@ function checkForUpdatesOnStartup() { }, 3000) } -app.whenReady().then(() => { +app.whenReady().then(async () => { + // 立即创建 Splash 窗口,确保用户尽快看到反馈 + createSplashWindow() + + // 等待 Splash 页面加载完成后再推送进度 + if (splashWindow) { + await new Promise((resolve) => { + if (splashWindow!.webContents.isLoading()) { + splashWindow!.webContents.once('did-finish-load', () => resolve()) + } else { + resolve() + } + }) + splashWindow.webContents + .executeJavaScript(`setVersion(${JSON.stringify(app.getVersion())})`) + .catch(() => {}) + } + + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) + + // 初始化配置服务 + updateSplashProgress(5, '正在加载配置...') configService = new ConfigService() + + // 将用户主题配置推送给 Splash 窗口 + if (splashWindow && !splashWindow.isDestroyed()) { + const themeId = configService.get('themeId') || 'cloud-dancer' + const themeMode = configService.get('theme') || 'system' + splashWindow.webContents + .executeJavaScript(`applyTheme(${JSON.stringify(themeId)}, ${JSON.stringify(themeMode)})`) + .catch(() => {}) + } + await delay(200) + + // 设置资源路径 + updateSplashProgress(10, '正在初始化...') const candidateResources = app.isPackaged ? join(process.resourcesPath, 'resources') : join(app.getAppPath(), 'resources') const fallbackResources = join(process.cwd(), 'resources') const resourcesPath = existsSync(candidateResources) ? candidateResources : fallbackResources const userDataPath = app.getPath('userData') + await delay(200) + + // 初始化数据库服务 + updateSplashProgress(18, '正在初始化...') wcdbService.setPaths(resourcesPath, userDataPath) wcdbService.setLogEnabled(configService.get('logEnabled') === true) + await delay(200) + + // 注册 IPC 处理器 + updateSplashProgress(25, '正在初始化...') registerIpcHandlers() + await delay(200) + + // 检查配置状态 const onboardingDone = configService.get('onboardingDone') shouldShowMain = onboardingDone === true - mainWindow = createWindow({ autoShow: shouldShowMain }) - if (!onboardingDone) { - createOnboardingWindow() - } + // 创建主窗口(不显示,由启动流程统一控制) + updateSplashProgress(30, '正在加载界面...') + mainWindow = createWindow({ autoShow: false }) - // 解决朋友圈图片无法加载问题(添加 Referer) + // 配置网络服务 session.defaultSession.webRequest.onBeforeSendHeaders( { urls: ['*://*.qpic.cn/*', '*://*.wx.qq.com/*'] @@ -1538,7 +1652,31 @@ app.whenReady().then(() => { } ) - // 启动时检测更新 + // 等待主窗口加载完成(真正耗时阶段,进度条末端呼吸光点) + updateSplashProgress(30, '正在加载界面...', true) + await new Promise((resolve) => { + if (mainWindowReady) { + resolve() + } else { + mainWindow!.once('ready-to-show', () => { + mainWindowReady = true + resolve() + }) + } + }) + + // 加载完成,收尾 + updateSplashProgress(100, '启动完成') + await new Promise((resolve) => setTimeout(resolve, 250)) + closeSplash() + + if (!onboardingDone) { + createOnboardingWindow() + } else { + mainWindow?.show() + } + + // 启动时检测更新(不阻塞启动) checkForUpdatesOnStartup() app.on('activate', () => { diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index eda6e7a..be86d54 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -991,12 +991,34 @@ class ChatService { } console.warn(`[ChatService] 表情包数据库未命中: md5=${msg.emojiMd5}, db=${dbPath}`) + // 数据库未命中时,尝试从本地 emoji 缓存目录查找(转发的表情包只有 md5,无 CDN URL) + this.findEmojiInLocalCache(msg) } catch (e) { console.error(`[ChatService] 恢复表情包失败: md5=${msg.emojiMd5}`, e) } } + /** + * 从本地 WeFlow emoji 缓存目录按 md5 查找文件 + */ + private findEmojiInLocalCache(msg: Message): void { + if (!msg.emojiMd5) return + const cacheDir = this.getEmojiCacheDir() + if (!existsSync(cacheDir)) return + + const extensions = ['.gif', '.png', '.webp', '.jpg', '.jpeg'] + for (const ext of extensions) { + const filePath = join(cacheDir, `${msg.emojiMd5}${ext}`) + if (existsSync(filePath)) { + msg.emojiLocalPath = filePath + // 同步写入内存缓存,避免重复查找 + emojiCache.set(msg.emojiMd5, filePath) + return + } + } + } + /** * 查找 emoticon.db 路径 */ @@ -1338,6 +1360,9 @@ class ChatService { chatRecordList = type49Info.chatRecordList transferPayerUsername = type49Info.transferPayerUsername transferReceiverUsername = type49Info.transferReceiverUsername + // 引用消息(appmsg type=57)的 quotedContent/quotedSender + if (type49Info.quotedContent !== undefined) quotedContent = type49Info.quotedContent + if (type49Info.quotedSender !== undefined) quotedSender = type49Info.quotedSender } else if (localType === 244813135921 || (content && content.includes('57'))) { const quoteInfo = this.parseQuoteMessage(content) quotedContent = quoteInfo.content @@ -1381,6 +1406,8 @@ class ChatService { chatRecordList = chatRecordList || type49Info.chatRecordList transferPayerUsername = transferPayerUsername || type49Info.transferPayerUsername transferReceiverUsername = transferReceiverUsername || type49Info.transferReceiverUsername + if (!quotedContent && type49Info.quotedContent !== undefined) quotedContent = type49Info.quotedContent + if (!quotedSender && type49Info.quotedSender !== undefined) quotedSender = type49Info.quotedSender } messages.push({ @@ -1549,7 +1576,17 @@ class ChatService { private parseType49(content: string): string { const title = this.extractXmlValue(content, 'title') - const type = this.extractXmlValue(content, 'type') + // 从 appmsg 直接子节点提取 type,避免匹配到 refermsg 内部的 + let type = '' + const appmsgMatch = /([\s\S]*?)<\/appmsg>/i.exec(content) + if (appmsgMatch) { + const inner = appmsgMatch[1] + .replace(//gi, '') + .replace(//gi, '') + const typeMatch = /([\s\S]*?)<\/type>/i.exec(inner) + if (typeMatch) type = typeMatch[1].trim() + } + if (!type) type = this.extractXmlValue(content, 'type') const normalized = content.toLowerCase() const locationLabel = this.extractXmlAttribute(content, 'location', 'label') || @@ -1964,6 +2001,8 @@ class ChatService { */ private parseType49Message(content: string): { xmlType?: string + quotedContent?: string + quotedSender?: string linkTitle?: string linkUrl?: string linkThumb?: string @@ -2008,8 +2047,20 @@ class ChatService { try { if (!content) return {} - // 提取 appmsg 中的 type - const xmlType = this.extractXmlValue(content, 'type') + // 提取 appmsg 直接子节点的 type,避免匹配到 refermsg 内部的 + // 先尝试从 ... 块内提取,再用正则跳过嵌套标签 + let xmlType = '' + const appmsgMatch = /([\s\S]*?)<\/appmsg>/i.exec(content) + if (appmsgMatch) { + // 在 appmsg 内容中,找第一个 但跳过在子元素内部的(如 refermsg > type) + // 策略:去掉所有嵌套块(refermsg、patMsg 等),再提取 type + const appmsgInner = appmsgMatch[1] + .replace(//gi, '') + .replace(//gi, '') + const typeMatch = /([\s\S]*?)<\/type>/i.exec(appmsgInner) + if (typeMatch) xmlType = typeMatch[1].trim() + } + if (!xmlType) xmlType = this.extractXmlValue(content, 'type') if (!xmlType) return {} const result: any = { xmlType } @@ -2126,6 +2177,12 @@ class ChatService { result.appMsgKind = 'transfer' } else if (xmlType === '87') { result.appMsgKind = 'announcement' + } else if (xmlType === '57') { + // 引用回复消息,解析 refermsg + result.appMsgKind = 'quote' + const quoteInfo = this.parseQuoteMessage(content) + result.quotedContent = quoteInfo.content + result.quotedSender = quoteInfo.sender } else if ((xmlType === '5' || xmlType === '49') && (sourceUsername?.startsWith('gh_') || appName?.includes('公众号') || sourceName)) { result.appMsgKind = 'official-link' } else if (url) { diff --git a/public/splash.html b/public/splash.html new file mode 100644 index 0000000..d71c241 --- /dev/null +++ b/public/splash.html @@ -0,0 +1,249 @@ + + + + + + WeFlow + + + +
+
+ +
+
WeFlow
+
微信聊天记录管理工具
+
+
+ +
+ +
+
+
+
+
+
正在启动...
+
+
+
+
+ + + + diff --git a/src/App.scss b/src/App.scss index 2613fa3..3c137bd 100644 --- a/src/App.scss +++ b/src/App.scss @@ -4,6 +4,48 @@ flex-direction: column; background: var(--bg-primary); animation: appFadeIn 0.35s ease-out; + position: relative; + overflow: hidden; +} + +// 繁花如梦:底色层(::before)+ 光晕层(::after)分离,避免 blur 吃掉边缘 +[data-theme="blossom-dream"] .app-container { + background: transparent; +} + +// ::before 纯底色,不模糊 +[data-theme="blossom-dream"] .app-container::before { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + z-index: -2; + background: var(--bg-primary); +} + +// ::after 光晕层,模糊叠加在底色上 +[data-theme="blossom-dream"] .app-container::after { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + z-index: -1; + background: + radial-gradient(ellipse 55% 45% at 15% 20%, var(--blossom-pink) 0%, transparent 70%), + radial-gradient(ellipse 50% 40% at 85% 75%, var(--blossom-peach) 0%, transparent 65%), + radial-gradient(ellipse 45% 50% at 80% 10%, var(--blossom-blue) 0%, transparent 60%); + filter: blur(80px); + opacity: 0.75; +} + +// 深色模式光晕更克制 +[data-theme="blossom-dream"][data-mode="dark"] .app-container::after { + background: + radial-gradient(ellipse 55% 45% at 15% 20%, var(--blossom-pink) 0%, transparent 70%), + radial-gradient(ellipse 50% 40% at 85% 75%, var(--blossom-purple) 0%, transparent 65%), + radial-gradient(ellipse 45% 50% at 80% 10%, var(--blossom-blue) 0%, transparent 60%); + filter: blur(100px); + opacity: 0.2; } .window-drag-region { diff --git a/src/components/NotificationToast.scss b/src/components/NotificationToast.scss index b48013c..d442af7 100644 --- a/src/components/NotificationToast.scss +++ b/src/components/NotificationToast.scss @@ -7,10 +7,12 @@ -webkit-backdrop-filter: blur(20px); border: 1px solid var(--border-light); - // 浅色模式下使用不透明背景,避免透明窗口中通知过于透明 + // 浅色模式下使用完全不透明背景,并禁用毛玻璃效果 [data-mode="light"] &, :not([data-mode]) & { background: rgba(255, 255, 255, 1); + backdrop-filter: none; + -webkit-backdrop-filter: none; } border-radius: 12px; @@ -46,10 +48,16 @@ backdrop-filter: none !important; -webkit-backdrop-filter: none !important; - // 确保背景不透明 - background: var(--bg-secondary, #2c2c2c); + // 确保背景完全不透明(通知是独立窗口,透明背景会穿透) + background: var(--bg-secondary-solid, var(--bg-secondary, #2c2c2c)); color: var(--text-primary, #ffffff); + // 浅色模式强制完全不透明白色背景 + [data-mode="light"] &, + :not([data-mode]) & { + background: #ffffff !important; + } + box-shadow: none !important; // NO SHADOW border: 1px solid var(--border-light, rgba(255, 255, 255, 0.1)); diff --git a/src/components/Sidebar.scss b/src/components/Sidebar.scss index 6899c93..d2a1b7f 100644 --- a/src/components/Sidebar.scss +++ b/src/components/Sidebar.scss @@ -103,4 +103,31 @@ background: var(--bg-tertiary); color: var(--text-primary); } +} + +// 繁花如梦主题:侧边栏毛玻璃 + 激活项用主品牌色 +[data-theme="blossom-dream"] .sidebar { + background: rgba(255, 255, 255, 0.6); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-right: 1px solid rgba(255, 255, 255, 0.4); +} + +[data-theme="blossom-dream"][data-mode="dark"] .sidebar { + background: rgba(34, 30, 36, 0.75); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-right: 1px solid rgba(255, 255, 255, 0.06); +} + +// 激活项:主品牌色纵向微渐变 +[data-theme="blossom-dream"] .nav-item.active { + background: linear-gradient(180deg, #D4849A 0%, #C4748A 100%); +} + +// 深色激活项:用藕粉色,背景深灰底 + 粉色文字/图标(高阶玩法) +[data-theme="blossom-dream"][data-mode="dark"] .nav-item.active { + background: rgba(209, 158, 187, 0.15); + color: #D19EBB; + border: 1px solid rgba(209, 158, 187, 0.2); } \ No newline at end of file diff --git a/src/components/TitleBar.scss b/src/components/TitleBar.scss index f17c998..9c18972 100644 --- a/src/components/TitleBar.scss +++ b/src/components/TitleBar.scss @@ -10,6 +10,12 @@ gap: 8px; } +// 繁花如梦:标题栏毛玻璃 +[data-theme="blossom-dream"] .title-bar { + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); +} + .title-logo { width: 20px; height: 20px; diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index 8a6a9c5..d8c81b9 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -2243,6 +2243,18 @@ .quoted-text { color: var(--text-secondary); white-space: pre-wrap; + + .quoted-type-label { + font-style: italic; + opacity: 0.8; + } + + .quoted-emoji-image { + width: 40px; + height: 40px; + vertical-align: middle; + object-fit: contain; + } } } @@ -2897,7 +2909,6 @@ display: flex; align-items: center; gap: 6px; - padding: 8px 12px; color: var(--text-secondary); font-size: 13px; diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index e5389eb..b5ec3df 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -2780,6 +2780,31 @@ const voiceTranscriptCache = new Map() const senderAvatarCache = new Map() const senderAvatarLoading = new Map>() +// 引用消息中的动画表情组件 +function QuotedEmoji({ cdnUrl, md5 }: { cdnUrl: string; md5?: string }) { + const cacheKey = md5 || cdnUrl + const [localPath, setLocalPath] = useState(() => emojiDataUrlCache.get(cacheKey)) + const [loading, setLoading] = useState(false) + const [error, setError] = useState(false) + + useEffect(() => { + if (localPath || loading || error) return + setLoading(true) + window.electronAPI.chat.downloadEmoji(cdnUrl, md5).then((result: { success: boolean; localPath?: string }) => { + if (result.success && result.localPath) { + emojiDataUrlCache.set(cacheKey, result.localPath) + setLocalPath(result.localPath) + } else { + setError(true) + } + }).catch(() => setError(true)).finally(() => setLoading(false)) + }, [cdnUrl, md5, cacheKey, localPath, loading, error]) + + if (error || (!loading && !localPath)) return [动画表情] + if (loading) return [动画表情] + return 动画表情 +} + // 消息气泡组件 function MessageBubble({ message, @@ -2901,7 +2926,7 @@ function MessageBubble({ // 从缓存获取表情包 data URL const cacheKey = message.emojiMd5 || message.emojiCdnUrl || '' const [emojiLocalPath, setEmojiLocalPath] = useState( - () => emojiDataUrlCache.get(cacheKey) + () => emojiDataUrlCache.get(cacheKey) || message.emojiLocalPath ) const imageCacheKey = message.imageMd5 || message.imageDatName || `local:${message.localId}` const [imageLocalPath, setImageLocalPath] = useState( @@ -3036,10 +3061,15 @@ function MessageBubble({ // 自动下载表情包 useEffect(() => { if (emojiLocalPath) return + // 后端已从本地缓存找到文件(转发表情包无 CDN URL 的情况) + if (isEmoji && message.emojiLocalPath && !emojiLocalPath) { + setEmojiLocalPath(message.emojiLocalPath) + return + } if (isEmoji && message.emojiCdnUrl && !emojiLoading && !emojiError) { downloadEmoji() } - }, [isEmoji, message.emojiCdnUrl, emojiLocalPath, emojiLoading, emojiError]) + }, [isEmoji, message.emojiCdnUrl, message.emojiLocalPath, emojiLocalPath, emojiLoading, emojiError]) const requestImageDecrypt = useCallback(async (forceUpdate = false, silent = false) => { if (!isImage) return @@ -3971,11 +4001,13 @@ function MessageBubble({ // 通话消息 if (isCall) { return ( -
- - - - {message.parsedContent || '[通话]'} +
+
+ + + + {message.parsedContent || '[通话]'} +
) } @@ -4043,11 +4075,39 @@ function MessageBubble({ const replyText = q('title') || cleanMessageContent(message.parsedContent) || '' const referContent = q('refermsg > content') || '' const referSender = q('refermsg > displayname') || '' + const referType = q('refermsg > type') || '' + + // 根据被引用消息类型渲染对应内容 + const renderReferContent = () => { + // 动画表情:解析嵌套 XML 提取 cdnurl 渲染 + if (referType === '47') { + try { + const innerDoc = new DOMParser().parseFromString(referContent, 'text/xml') + const cdnUrl = innerDoc.querySelector('emoji')?.getAttribute('cdnurl') || '' + const md5 = innerDoc.querySelector('emoji')?.getAttribute('md5') || '' + if (cdnUrl) return + } catch { /* 解析失败降级 */ } + return [动画表情] + } + + // 各类型名称映射 + const typeLabels: Record = { + '3': '图片', '34': '语音', '43': '视频', + '49': '链接', '50': '通话', '10000': '系统消息', '10002': '撤回消息', + } + if (referType && typeLabels[referType]) { + return [{typeLabels[referType]}] + } + + // 普通文本或未知类型 + return <>{renderTextWithEmoji(cleanMessageContent(referContent))} + } + return (
{referSender && {referSender}} - {renderTextWithEmoji(cleanMessageContent(referContent))} + {renderReferContent()}
{renderTextWithEmoji(cleanMessageContent(replyText))}
@@ -4143,6 +4203,22 @@ function MessageBubble({
) + if (kind === 'quote') { + // 引用回复消息(appMsgKind='quote',xmlType=57) + const replyText = message.linkTitle || q('title') || cleanMessageContent(message.parsedContent) || '' + const referContent = message.quotedContent || q('refermsg > content') || '' + const referSender = message.quotedSender || q('refermsg > displayname') || '' + return ( +
+
+ {referSender && {referSender}} + {renderTextWithEmoji(cleanMessageContent(referContent))} +
+
{renderTextWithEmoji(cleanMessageContent(replyText))}
+
+ ) + } + if (kind === 'red-packet') { // 专属红包卡片 const greeting = (() => { @@ -4347,6 +4423,44 @@ function MessageBubble({ console.error('解析 AppMsg 失败:', e) } + // 引用回复消息 (type=57),防止被误判为链接 + if (appMsgType === '57') { + const replyText = parsedDoc?.querySelector('title')?.textContent?.trim() || cleanMessageContent(message.parsedContent) || '' + const referContent = parsedDoc?.querySelector('refermsg > content')?.textContent?.trim() || '' + const referSender = parsedDoc?.querySelector('refermsg > displayname')?.textContent?.trim() || '' + const referType = parsedDoc?.querySelector('refermsg > type')?.textContent?.trim() || '' + + const renderReferContent2 = () => { + if (referType === '47') { + try { + const innerDoc = new DOMParser().parseFromString(referContent, 'text/xml') + const cdnUrl = innerDoc.querySelector('emoji')?.getAttribute('cdnurl') || '' + const md5 = innerDoc.querySelector('emoji')?.getAttribute('md5') || '' + if (cdnUrl) return + } catch { /* 解析失败降级 */ } + return [动画表情] + } + const typeLabels: Record = { + '3': '图片', '34': '语音', '43': '视频', + '49': '链接', '50': '通话', '10000': '系统消息', '10002': '撤回消息', + } + if (referType && typeLabels[referType]) { + return [{typeLabels[referType]}] + } + return <>{renderTextWithEmoji(cleanMessageContent(referContent))} + } + + return ( +
+
+ {referSender && {referSender}} + {renderReferContent2()} +
+
{renderTextWithEmoji(cleanMessageContent(replyText))}
+
+ ) + } + // 群公告消息 (type=87) if (appMsgType === '87') { const announcementText = textAnnouncement || desc || '群公告' @@ -4579,7 +4693,7 @@ function MessageBubble({ if (isEmoji) { // ... (keep existing emoji logic) // 没有 cdnUrl 或加载失败,显示占位符 - if (!message.emojiCdnUrl || emojiError) { + if ((!message.emojiCdnUrl && !message.emojiLocalPath) || emojiError) { return (
diff --git a/src/pages/HomePage.scss b/src/pages/HomePage.scss index 6b12cb1..cd4cb78 100644 --- a/src/pages/HomePage.scss +++ b/src/pages/HomePage.scss @@ -29,7 +29,7 @@ .blob-1 { width: 400px; height: 400px; - background: rgba(139, 115, 85, 0.25); + background: rgba(var(--primary-rgb), 0.25); top: -100px; left: -50px; animation-duration: 25s; @@ -38,7 +38,7 @@ .blob-2 { width: 350px; height: 350px; - background: rgba(139, 115, 85, 0.15); + background: rgba(var(--primary-rgb), 0.15); bottom: -50px; right: -50px; animation-duration: 30s; @@ -74,7 +74,7 @@ margin: 0 0 16px; color: var(--text-primary); letter-spacing: -2px; - background: linear-gradient(135deg, var(--text-primary) 0%, rgba(139, 115, 85, 0.8) 100%); + background: linear-gradient(135deg, var(--primary) 0%, rgba(var(--primary-rgb), 0.6) 100%); background-clip: text; -webkit-background-clip: text; -webkit-text-fill-color: transparent; diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 48b2f2e..4361e33 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -939,8 +939,16 @@ function SettingsPage() {
{themes.map((theme) => (
setTheme(theme.id)}> -
-
+
+
{theme.name} diff --git a/src/stores/themeStore.ts b/src/stores/themeStore.ts index df2bd9d..523d3e4 100644 --- a/src/stores/themeStore.ts +++ b/src/stores/themeStore.ts @@ -1,7 +1,7 @@ import { create } from 'zustand' import { persist } from 'zustand/middleware' -export type ThemeId = 'cloud-dancer' | 'corundum-blue' | 'kiwi-green' | 'spicy-red' | 'teal-water' +export type ThemeId = 'cloud-dancer' | 'corundum-blue' | 'kiwi-green' | 'spicy-red' | 'teal-water' | 'blossom-dream' export type ThemeMode = 'light' | 'dark' | 'system' export interface ThemeInfo { @@ -10,6 +10,8 @@ export interface ThemeInfo { description: string primaryColor: string bgColor: string + // 可选副色,用于多彩主题的渐变预览 + accentColor?: string } export const themes: ThemeInfo[] = [ @@ -20,6 +22,14 @@ export const themes: ThemeInfo[] = [ primaryColor: '#8B7355', bgColor: '#F0EEE9' }, + { + id: 'blossom-dream', + name: '繁花如梦', + description: '晨曦花境 · 夜阑幽梦', + primaryColor: '#D4849A', + bgColor: '#FCF9FB', + accentColor: '#FFBE98' + }, { id: 'corundum-blue', name: '刚玉蓝', diff --git a/src/styles/main.scss b/src/styles/main.scss index 9f81ffd..1939130 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -153,6 +153,43 @@ --sent-card-bg: var(--primary); } +// 繁花如梦 - 浅色(晨曦花境) +[data-theme="blossom-dream"][data-mode="light"], +[data-theme="blossom-dream"]:not([data-mode]) { + // 三色定义(供伪元素光晕使用,饱和度提高以便在底色上可见) + --blossom-pink: #F0A0B8; + --blossom-peach: #FFB07A; + --blossom-blue: #90B8E0; + + // 主品牌色:Pantone 粉晶 Rose Quartz + --primary: #D4849A; + --primary-rgb: 212, 132, 154; + --primary-hover: #C4748A; + --primary-light: rgba(212, 132, 154, 0.12); + + // 背景三层:主背景最深(相对),面板次之,卡片最白 + --bg-primary: #F5EDF2; + --bg-secondary: rgba(255, 255, 255, 0.82); + --bg-tertiary: rgba(212, 132, 154, 0.06); + --bg-hover: rgba(212, 132, 154, 0.09); + + // 文字:提高对比度,主色接近纯黑只带微弱紫调 + --text-primary: #1E1A22; + --text-secondary: #6B5F70; + --text-tertiary: #9A8A9E; + // 边框:粉色半透明,有存在感但不强硬 + --border-color: rgba(212, 132, 154, 0.18); + + --bg-gradient: linear-gradient(150deg, #F5EDF2 0%, #F0EAF6 50%, #EAF0F8 100%); + --primary-gradient: linear-gradient(135deg, #D4849A 0%, #E8A8B8 100%); + + // 卡片:高不透明度白,与背景形成明显层次 + --card-bg: rgba(255, 255, 255, 0.88); + --card-inner-bg: rgba(255, 255, 255, 0.95); + + --sent-card-bg: var(--primary); +} + // ==================== 深色主题 ==================== // 云上舞白 - 深色 @@ -163,6 +200,7 @@ --primary-light: rgba(201, 168, 108, 0.15); --bg-primary: #1a1816; --bg-secondary: rgba(40, 36, 32, 0.9); + --bg-secondary-solid: #282420; --bg-tertiary: rgba(255, 255, 255, 0.05); --bg-hover: rgba(255, 255, 255, 0.08); --text-primary: #F0EEE9; @@ -184,6 +222,7 @@ --primary-light: rgba(106, 154, 170, 0.15); --bg-primary: #141a1c; --bg-secondary: rgba(30, 40, 44, 0.9); + --bg-secondary-solid: #1e282c; --bg-tertiary: rgba(255, 255, 255, 0.05); --bg-hover: rgba(255, 255, 255, 0.08); --text-primary: #E8EEF0; @@ -205,6 +244,7 @@ --primary-light: rgba(154, 186, 124, 0.15); --bg-primary: #161a14; --bg-secondary: rgba(34, 42, 30, 0.9); + --bg-secondary-solid: #222a1e; --bg-tertiary: rgba(255, 255, 255, 0.05); --bg-hover: rgba(255, 255, 255, 0.08); --text-primary: #E8F0E4; @@ -226,6 +266,7 @@ --primary-light: rgba(192, 96, 104, 0.15); --bg-primary: #1a1416; --bg-secondary: rgba(42, 32, 34, 0.9); + --bg-secondary-solid: #2a2022; --bg-tertiary: rgba(255, 255, 255, 0.05); --bg-hover: rgba(255, 255, 255, 0.08); --text-primary: #F0E8E8; @@ -247,6 +288,7 @@ --primary-light: rgba(122, 186, 170, 0.15); --bg-primary: #121a1a; --bg-secondary: rgba(28, 42, 42, 0.9); + --bg-secondary-solid: #1c2a2a; --bg-tertiary: rgba(255, 255, 255, 0.05); --bg-hover: rgba(255, 255, 255, 0.08); --text-primary: #E4F0F0; @@ -260,6 +302,43 @@ --sent-card-bg: var(--primary); } +// 繁花如梦 - 深色(夜阑幽梦) +[data-theme="blossom-dream"][data-mode="dark"] { + // 光晕色(供伪元素使用,降低饱和度避免刺眼) + --blossom-pink: #C670C3; + --blossom-purple: #5F4B8B; + --blossom-blue: #3A2A50; + + // 主品牌色:藕粉/烟紫粉,降饱和度不刺眼 + --primary: #D19EBB; + --primary-rgb: 209, 158, 187; + --primary-hover: #DDB0C8; + --primary-light: rgba(209, 158, 187, 0.15); + + // 背景三层:极深黑灰底(去掉紫薯色),面板略浅,卡片再浅一级 + --bg-primary: #151316; + --bg-secondary: rgba(34, 30, 36, 0.92); + --bg-secondary-solid: #221E24; + --bg-tertiary: rgba(255, 255, 255, 0.04); + --bg-hover: rgba(209, 158, 187, 0.1); + + // 文字 + --text-primary: #F0EAF4; + --text-secondary: #A898AE; + --text-tertiary: #6A5870; + // 边框:极细白色内发光,剥离层级 + --border-color: rgba(255, 255, 255, 0.07); + + --bg-gradient: linear-gradient(150deg, #151316 0%, #1A1620 50%, #131018 100%); + --primary-gradient: linear-gradient(135deg, #D19EBB 0%, #A878A8 100%); + + // 卡片:比面板更亮一档,用深灰而非紫色 + --card-bg: rgba(42, 38, 46, 0.92); + --card-inner-bg: rgba(52, 48, 56, 0.96); + + --sent-card-bg: var(--primary); +} + // 重置样式 * { margin: 0;