diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 821e9e3..d35b621 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -223,6 +223,12 @@ jobs: - Linux (.tar.gz): ${LINUX_TAR_URL:-$RELEASE_PAGE} - linux (.AppImage): ${LINUX_APPIMAGE_URL:-$RELEASE_PAGE} + ## macOS 安装提示(未知来源) + - 若打开时提示“来自未知开发者”或“无法验证开发者”,请到「系统设置 -> 隐私与安全性」中允许打开该应用。 + - 如果仍被系统拦截,请在终端执行以下命令去除隔离标记: + - `xattr -dr com.apple.quarantine "/Applications/WeFlow.app"` + - 执行后重新打开 WeFlow。 + > 如果某个平台链接暂时未生成,可进入完整发布页查看全部资源:$RELEASE_PAGE EOF diff --git a/electron/main.ts b/electron/main.ts index 5b2ea5b..860e356 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -146,33 +146,47 @@ const normalizeReleaseNotes = (rawReleaseNotes: unknown): string => { if (!merged.trim()) return '' + const shouldStripReleaseSection = (headingRaw: string): boolean => { + const heading = headingRaw.trim().toLowerCase() + if (heading === '下载' || heading === 'download') return true + + const compactHeading = heading.replace(/\s+/g, '') + if (compactHeading.startsWith('macos安装提示')) return true + if (compactHeading.startsWith('mac安装提示')) return true + return false + } + // 兼容 electron-updater 直接返回 HTML 的场景 const removeDownloadSectionFromHtml = (input: string): string => { - return input.replace( - /]*>\s*(?:下载|download)\s*<\/h[1-6]>\s*[\s\S]*?(?=]*>\s*(?:下载|download)\s*<\/h[1-6]>\s*[\s\S]*?(?=]*>\s*(?:mac\s*os|mac)\s*安装提示(?:\s*[((]\s*未知来源\s*[))])?\s*<\/h[1-6]>\s*[\s\S]*?(?= { const lines = input.split(/\r?\n/) const output: string[] = [] - let skipDownloadSection = false + let skipSection = false for (const line of lines) { const headingMatch = line.match(/^\s*#{1,6}\s*(.+?)\s*$/) if (headingMatch) { - const heading = headingMatch[1].trim().toLowerCase() - if (heading === '下载' || heading === 'download') { - skipDownloadSection = true + if (shouldStripReleaseSection(headingMatch[1])) { + skipSection = true continue } - if (skipDownloadSection) { - skipDownloadSection = false + if (skipSection) { + skipSection = false } } - if (!skipDownloadSection) { + if (!skipSection) { output.push(line) } } diff --git a/electron/services/chatService.ts b/electron/services/chatService.ts index 4a71e88..71f3dc0 100644 --- a/electron/services/chatService.ts +++ b/electron/services/chatService.ts @@ -5096,14 +5096,35 @@ class ChatService { } // 如果是群聊,尝试获取群昵称 - let groupNicknames: Record = {} + const groupNicknames = new Map() if (chatroomId.endsWith('@chatroom')) { const nickResult = await wcdbService.getGroupNicknames(chatroomId) if (nickResult.success && nickResult.nicknames) { - groupNicknames = nickResult.nicknames + const nicknameBuckets = new Map>() + for (const [memberIdRaw, nicknameRaw] of Object.entries(nickResult.nicknames)) { + const memberId = String(memberIdRaw || '').trim().toLowerCase() + const nickname = String(nicknameRaw || '').trim() + if (!memberId || !nickname) continue + const slot = nicknameBuckets.get(memberId) + if (slot) { + slot.add(nickname) + } else { + nicknameBuckets.set(memberId, new Set([nickname])) + } + } + for (const [memberId, nicknameSet] of nicknameBuckets.entries()) { + if (nicknameSet.size !== 1) continue + groupNicknames.set(memberId, Array.from(nicknameSet)[0]) + } } } + const lookupGroupNickname = (username?: string | null): string => { + const key = String(username || '').trim().toLowerCase() + if (!key) return '' + return groupNicknames.get(key) || '' + } + // 获取当前用户 wxid,用于识别"自己" const myWxid = this.configService.get('myWxid') const cleanedMyWxid = myWxid ? this.cleanAccountDirName(myWxid) : '' @@ -5113,7 +5134,7 @@ class ChatService { // 特判:如果是当前用户自己(contact 表通常不包含自己) if (myWxid && (username === myWxid || username === cleanedMyWxid)) { // 先查群昵称中是否有自己 - const myGroupNick = groupNicknames[username] + const myGroupNick = lookupGroupNickname(username) || lookupGroupNickname(myWxid) if (myGroupNick) return myGroupNick // 尝试从缓存获取自己的昵称 const cached = this.avatarCache.get(username) || this.avatarCache.get(myWxid) @@ -5122,7 +5143,7 @@ class ChatService { } // 先查群昵称 - const groupNick = groupNicknames[username] + const groupNick = lookupGroupNickname(username) if (groupNick) return groupNick // 再查联系人信息 diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index e0f43f3..34815cb 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -1404,33 +1404,60 @@ class ExportService { } /** - * 获取群成员群昵称。优先使用 DLL,必要时回退到 `contact.chat_room.ext_buffer` 解析。 + * 获取群成员群昵称。后端结果为唯一业务真值,前端仅做冲突净化防串号。 */ async getGroupNicknamesForRoom(chatroomId: string, candidates: string[] = []): Promise> { - const nicknameMap = new Map() - try { const dllResult = await wcdbService.getGroupNicknames(chatroomId) - if (dllResult.success && dllResult.nicknames) { - this.mergeGroupNicknameEntries(nicknameMap, Object.entries(dllResult.nicknames)) + if (!dllResult.success || !dllResult.nicknames) { + return new Map() } + return this.buildTrustedGroupNicknameMap(Object.entries(dllResult.nicknames), candidates) } catch (e) { console.error('getGroupNicknamesForRoom dll error:', e) + return new Map() + } + } + + private normalizeGroupNicknameIdentity(value: string): string { + const raw = String(value || '').trim() + if (!raw) return '' + return raw.toLowerCase() + } + + private buildTrustedGroupNicknameMap( + entries: Iterable<[string, string]>, + candidates: string[] = [] + ): Map { + const candidateSet = new Set( + this.buildGroupNicknameIdCandidates(candidates) + .map((id) => this.normalizeGroupNicknameIdentity(id)) + .filter(Boolean) + ) + + const buckets = new Map>() + for (const [memberIdRaw, nicknameRaw] of entries) { + const identity = this.normalizeGroupNicknameIdentity(memberIdRaw || '') + if (!identity) continue + if (candidateSet.size > 0 && !candidateSet.has(identity)) continue + + const nickname = this.normalizeGroupNickname(nicknameRaw || '') + if (!nickname) continue + + const slot = buckets.get(identity) + if (slot) { + slot.add(nickname) + } else { + buckets.set(identity, new Set([nickname])) + } } - try { - const result = await wcdbService.getChatRoomExtBuffer(chatroomId) - if (!result.success || !result.extBuffer) { - return nicknameMap - } - const extBuffer = this.decodeExtBuffer(result.extBuffer) - if (!extBuffer) return nicknameMap - this.mergeGroupNicknameEntries(nicknameMap, this.parseGroupNicknamesFromExtBuffer(extBuffer, candidates).entries()) - return nicknameMap - } catch (e) { - console.error('getGroupNicknamesForRoom error:', e) - return nicknameMap + const trusted = new Map() + for (const [identity, nicknameSet] of buckets.entries()) { + if (nicknameSet.size !== 1) continue + trusted.set(identity, Array.from(nicknameSet)[0]) } + return trusted } private mergeGroupNicknameEntries( @@ -1680,8 +1707,6 @@ class ExportService { const raw = String(rawValue || '').trim() if (!raw) continue set.add(raw) - const cleaned = this.cleanAccountDirName(raw) - if (cleaned && cleaned !== raw) set.add(cleaned) } return Array.from(set) } @@ -1690,29 +1715,20 @@ class ExportService { const idCandidates = this.buildGroupNicknameIdCandidates(candidates) if (idCandidates.length === 0) return '' + let resolved = '' for (const id of idCandidates) { - const exact = this.normalizeGroupNickname(groupNicknamesMap.get(id) || '') - if (exact) return exact - const lower = this.normalizeGroupNickname(groupNicknamesMap.get(id.toLowerCase()) || '') - if (lower) return lower - } - - for (const id of idCandidates) { - const lower = id.toLowerCase() - let found = '' - let matched = 0 - for (const [key, value] of groupNicknamesMap.entries()) { - if (String(key || '').toLowerCase() !== lower) continue - const normalized = this.normalizeGroupNickname(value || '') - if (!normalized) continue - found = normalized - matched += 1 - if (matched > 1) return '' + const normalizedId = this.normalizeGroupNicknameIdentity(id) + if (!normalizedId) continue + const candidateNickname = this.normalizeGroupNickname(groupNicknamesMap.get(normalizedId) || '') + if (!candidateNickname) continue + if (!resolved) { + resolved = candidateNickname + continue } - if (matched === 1 && found) return found + if (resolved !== candidateNickname) return '' } - return '' + return resolved } /** diff --git a/electron/services/groupAnalyticsService.ts b/electron/services/groupAnalyticsService.ts index 8d66ce9..175c307 100644 --- a/electron/services/groupAnalyticsService.ts +++ b/electron/services/groupAnalyticsService.ts @@ -257,34 +257,60 @@ class GroupAnalyticsService { } /** - * 从 DLL 获取群成员的群昵称 + * 从后端获取群成员群昵称,并在前端进行唯一性净化防串号。 */ private async getGroupNicknamesForRoom(chatroomId: string, candidates: string[] = []): Promise> { - const nicknameMap = new Map() - try { const dllResult = await wcdbService.getGroupNicknames(chatroomId) - if (dllResult.success && dllResult.nicknames) { - this.mergeGroupNicknameEntries(nicknameMap, Object.entries(dllResult.nicknames)) + if (!dllResult.success || !dllResult.nicknames) { + return new Map() } + return this.buildTrustedGroupNicknameMap(Object.entries(dllResult.nicknames), candidates) } catch (e) { console.error('getGroupNicknamesForRoom dll error:', e) + return new Map() } + } - try { - const result = await wcdbService.getChatRoomExtBuffer(chatroomId) - if (!result.success || !result.extBuffer) { - return nicknameMap + private normalizeGroupNicknameIdentity(value: string): string { + const raw = String(value || '').trim() + if (!raw) return '' + return raw.toLowerCase() + } + + private buildTrustedGroupNicknameMap( + entries: Iterable<[string, string]>, + candidates: string[] = [] + ): Map { + const candidateSet = new Set( + this.buildGroupNicknameIdCandidates(candidates) + .map((id) => this.normalizeGroupNicknameIdentity(id)) + .filter(Boolean) + ) + + const buckets = new Map>() + for (const [memberIdRaw, nicknameRaw] of entries) { + const identity = this.normalizeGroupNicknameIdentity(memberIdRaw || '') + if (!identity) continue + if (candidateSet.size > 0 && !candidateSet.has(identity)) continue + + const nickname = this.normalizeGroupNickname(nicknameRaw || '') + if (!nickname) continue + + const slot = buckets.get(identity) + if (slot) { + slot.add(nickname) + } else { + buckets.set(identity, new Set([nickname])) } - - const extBuffer = this.decodeExtBuffer(result.extBuffer) - if (!extBuffer) return nicknameMap - this.mergeGroupNicknameEntries(nicknameMap, this.parseGroupNicknamesFromExtBuffer(extBuffer, candidates).entries()) - return nicknameMap - } catch (e) { - console.error('getGroupNicknamesForRoom error:', e) - return nicknameMap } + + const trusted = new Map() + for (const [identity, nicknameSet] of buckets.entries()) { + if (nicknameSet.size !== 1) continue + trusted.set(identity, Array.from(nicknameSet)[0]) + } + return trusted } private mergeGroupNicknameEntries( @@ -475,6 +501,16 @@ class GroupAnalyticsService { return Array.from(set) } + private buildGroupNicknameIdCandidates(values: Array): string[] { + const set = new Set() + for (const rawValue of values) { + const raw = String(rawValue || '').trim() + if (!raw) continue + set.add(raw) + } + return Array.from(set) + } + private toNonNegativeInteger(value: unknown): number { const parsed = Number(value) if (!Number.isFinite(parsed)) return 0 @@ -663,30 +699,23 @@ class GroupAnalyticsService { } private resolveGroupNicknameByCandidates(groupNicknames: Map, candidates: string[]): string { - const idCandidates = this.buildIdCandidates(candidates) + const idCandidates = this.buildGroupNicknameIdCandidates(candidates) if (idCandidates.length === 0) return '' + let resolved = '' for (const id of idCandidates) { - const exact = this.normalizeGroupNickname(groupNicknames.get(id) || '') - if (exact) return exact - } - - for (const id of idCandidates) { - const lower = id.toLowerCase() - let found = '' - let matched = 0 - for (const [key, value] of groupNicknames.entries()) { - if (String(key || '').toLowerCase() !== lower) continue - const normalized = this.normalizeGroupNickname(value || '') - if (!normalized) continue - found = normalized - matched += 1 - if (matched > 1) return '' + const normalizedId = this.normalizeGroupNicknameIdentity(id) + if (!normalizedId) continue + const candidateNickname = this.normalizeGroupNickname(groupNicknames.get(normalizedId) || '') + if (!candidateNickname) continue + if (!resolved) { + resolved = candidateNickname + continue } - if (matched === 1 && found) return found + if (resolved !== candidateNickname) return '' } - return '' + return resolved } private sanitizeWorksheetName(name: string): string { diff --git a/electron/services/httpService.ts b/electron/services/httpService.ts index 163106c..b59a014 100644 --- a/electron/services/httpService.ts +++ b/electron/services/httpService.ts @@ -1017,13 +1017,31 @@ class HttpService { } private lookupGroupNickname(groupNicknamesMap: Map, sender: string): string { - if (!sender) return '' - const cleaned = this.normalizeAccountId(sender) - return groupNicknamesMap.get(sender) - || groupNicknamesMap.get(sender.toLowerCase()) - || groupNicknamesMap.get(cleaned) - || groupNicknamesMap.get(cleaned.toLowerCase()) - || '' + const key = String(sender || '').trim().toLowerCase() + if (!key) return '' + return groupNicknamesMap.get(key) || '' + } + + private buildTrustedGroupNicknameMap(nicknames: Record): Map { + const buckets = new Map>() + for (const [memberIdRaw, nicknameRaw] of Object.entries(nicknames || {})) { + const memberId = String(memberIdRaw || '').trim().toLowerCase() + const nickname = String(nicknameRaw || '').trim() + if (!memberId || !nickname) continue + const slot = buckets.get(memberId) + if (slot) { + slot.add(nickname) + } else { + buckets.set(memberId, new Set([nickname])) + } + } + + const trusted = new Map() + for (const [memberId, nicknameSet] of buckets.entries()) { + if (nicknameSet.size !== 1) continue + trusted.set(memberId, Array.from(nicknameSet)[0]) + } + return trusted } private resolveChatLabSenderInfo( @@ -1094,21 +1112,7 @@ class HttpService { try { const result = await wcdbService.getGroupNicknames(talkerId) if (result.success && result.nicknames) { - groupNicknamesMap = new Map() - for (const [memberIdRaw, nicknameRaw] of Object.entries(result.nicknames)) { - const memberId = String(memberIdRaw || '').trim() - const nickname = String(nicknameRaw || '').trim() - if (!memberId || !nickname) continue - - groupNicknamesMap.set(memberId, nickname) - groupNicknamesMap.set(memberId.toLowerCase(), nickname) - - const cleaned = this.normalizeAccountId(memberId) - if (cleaned) { - groupNicknamesMap.set(cleaned, nickname) - groupNicknamesMap.set(cleaned.toLowerCase(), nickname) - } - } + groupNicknamesMap = this.buildTrustedGroupNicknameMap(result.nicknames) } } catch (e) { console.error('[HttpService] Failed to get group nicknames:', e) @@ -1389,4 +1393,3 @@ class HttpService { } export const httpService = new HttpService() - diff --git a/electron/services/messagePushService.ts b/electron/services/messagePushService.ts index 07b219b..95c180c 100644 --- a/electron/services/messagePushService.ts +++ b/electron/services/messagePushService.ts @@ -304,11 +304,8 @@ class MessagePushService { } const groupNicknames = await this.getGroupNicknames(chatroomId) - const normalizedSender = this.normalizeAccountId(senderUsername) - const nickname = groupNicknames[senderUsername] - || groupNicknames[senderUsername.toLowerCase()] - || groupNicknames[normalizedSender] - || groupNicknames[normalizedSender.toLowerCase()] + const senderKey = senderUsername.toLowerCase() + const nickname = groupNicknames[senderKey] if (nickname) { return nickname @@ -328,22 +325,33 @@ class MessagePushService { } const result = await wcdbService.getGroupNicknames(cacheKey) - const nicknames = result.success && result.nicknames ? result.nicknames : {} + const nicknames = result.success && result.nicknames + ? this.sanitizeGroupNicknames(result.nicknames) + : {} this.groupNicknameCache.set(cacheKey, { nicknames, updatedAt: Date.now() }) return nicknames } - private normalizeAccountId(value: string): string { - const trimmed = String(value || '').trim() - if (!trimmed) return trimmed - - if (trimmed.toLowerCase().startsWith('wxid_')) { - const match = trimmed.match(/^(wxid_[^_]+)/i) - return match ? match[1] : trimmed + private sanitizeGroupNicknames(nicknames: Record): Record { + const buckets = new Map>() + for (const [memberIdRaw, nicknameRaw] of Object.entries(nicknames || {})) { + const memberId = String(memberIdRaw || '').trim().toLowerCase() + const nickname = String(nicknameRaw || '').trim() + if (!memberId || !nickname) continue + const slot = buckets.get(memberId) + if (slot) { + slot.add(nickname) + } else { + buckets.set(memberId, new Set([nickname])) + } } - const suffixMatch = trimmed.match(/^(.+)_([a-zA-Z0-9]{4})$/) - return suffixMatch ? suffixMatch[1] : trimmed + const trusted: Record = {} + for (const [memberId, nicknameSet] of buckets.entries()) { + if (nicknameSet.size !== 1) continue + trusted[memberId] = Array.from(nicknameSet)[0] + } + return trusted } private isRecentMessage(messageKey: string): boolean { diff --git a/resources/wcdb_api.dll b/resources/wcdb_api.dll index 100bbc2..b864b4e 100644 Binary files a/resources/wcdb_api.dll and b/resources/wcdb_api.dll differ diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index 89049bf..69781d3 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -625,7 +625,7 @@ .bubble-content { background: var(--primary-gradient); - color: #fff; + color: var(--on-primary); border-radius: 18px 18px 4px 18px; padding: 10px 14px; font-size: 14px; @@ -1962,7 +1962,7 @@ .bubble-content { background: var(--primary); - color: white; + color: var(--on-primary); border-radius: 18px 18px 4px 18px; } } @@ -2453,15 +2453,15 @@ // 自己发送的消息中的引用样式 .message-bubble.sent .quoted-message { - background: rgba(255, 255, 255, 0.15); - border-left-color: rgba(255, 255, 255, 0.5); + background: color-mix(in srgb, var(--on-primary) 12%, var(--primary)); + border-left-color: color-mix(in srgb, var(--on-primary) 36%, var(--primary)); .quoted-sender { - color: rgba(255, 255, 255, 0.9); + color: color-mix(in srgb, var(--on-primary) 92%, var(--primary)); } .quoted-text { - color: rgba(255, 255, 255, 0.8); + color: color-mix(in srgb, var(--on-primary) 80%, var(--primary)); } } diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index 2de9358..5862037 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -1145,9 +1145,13 @@ } } +.quote-layout-group { + margin-top: 14px; +} + .quote-layout-picker { display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 12px; margin-top: 10px; } @@ -1155,32 +1159,146 @@ .quote-layout-card { position: relative; border: 1px solid var(--border-color); - border-radius: 16px; - padding: 14px; - background: var(--bg-primary); + border-radius: 14px; + padding: 14px 14px 12px; + background: color-mix(in srgb, var(--primary) 6%, var(--bg-primary)); color: inherit; cursor: pointer; text-align: left; - transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease, background 0.2s ease; + transition: border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease; &:hover { - border-color: color-mix(in srgb, var(--primary) 35%, var(--border-color)); - transform: translateY(-1px); + border-color: color-mix(in srgb, var(--primary) 32%, var(--border-color)); } &.active { - border-color: var(--primary); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 14%, transparent); - background: color-mix(in srgb, var(--bg-primary) 92%, var(--primary) 8%); + border-color: color-mix(in srgb, var(--primary) 68%, var(--border-color)); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 12%, transparent); + background: color-mix(in srgb, var(--primary) 10%, var(--bg-primary)); } } -.quote-layout-card-header { +.quote-layout-card-check { + position: absolute; + top: 12px; + left: 12px; + width: 18px; + height: 18px; + border-radius: 50%; + border: 1.5px solid color-mix(in srgb, var(--primary) 46%, var(--border-color)); + background: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: all 0.2s ease; + + &::after { + content: ''; + width: 7px; + height: 7px; + border-radius: 50%; + background: color-mix(in srgb, var(--primary) 78%, #6f8653); + transform: scale(0); + transition: transform 0.2s ease; + } + + &.active { + border-color: color-mix(in srgb, var(--primary) 78%, var(--border-color)); + } + + &.active::after { + transform: scale(1); + } +} + +.quote-layout-preview-shell { + padding-left: 22px; + min-height: 72px; +} + +.quote-layout-preview-chat { display: flex; align-items: flex-start; - justify-content: space-between; - gap: 12px; - margin-bottom: 12px; +} + +.quote-layout-preview-chat .message-bubble { + display: flex; + gap: 10px; + max-width: 70%; + align-items: flex-start; +} + +.quote-layout-preview-chat .message-bubble.sent { + flex-direction: row-reverse; +} + +.quote-layout-preview-chat .message-bubble.sent .bubble-content { + background: var(--primary); + color: var(--on-primary); + border-radius: 18px 18px 4px 18px; +} + +.quote-layout-preview-chat .bubble-content { + padding: 10px 14px; + font-size: 14px; + line-height: 1.6; + word-break: break-word; + white-space: pre-wrap; +} + +.quote-layout-preview-chat .message-text { + font-size: 14px; + line-height: 1.6; +} + +.quote-layout-preview-chat .quoted-message { + background: rgba(0, 0, 0, 0.04); + border-left: 2px solid var(--primary); + padding: 6px 10px; + border-radius: 4px; + font-size: 13px; +} + +.quote-layout-preview-chat .quoted-sender { + color: var(--primary); + font-weight: 500; + margin-right: 4px; +} + +.quote-layout-preview-chat .quoted-sender::after { + content: ':'; +} + +.quote-layout-preview-chat .quoted-text { + color: var(--text-secondary); + white-space: pre-wrap; +} + +.quote-layout-preview-chat .message-bubble.sent .quoted-message { + background: color-mix(in srgb, var(--on-primary) 12%, var(--primary)); + border-left-color: color-mix(in srgb, var(--on-primary) 36%, var(--primary)); +} + +.quote-layout-preview-chat .message-bubble.sent .quoted-sender { + color: color-mix(in srgb, var(--on-primary) 92%, var(--primary)); +} + +.quote-layout-preview-chat .message-bubble.sent .quoted-text { + color: color-mix(in srgb, var(--on-primary) 80%, var(--primary)); +} + +.quote-layout-preview-chat .bubble-content.quote-layout-top .quoted-message { + margin-bottom: 8px; +} + +.quote-layout-preview-chat .bubble-content.quote-layout-bottom .quoted-message { + margin-top: 8px; +} + +.quote-layout-card-footer { + margin-top: 8px; + padding-left: 22px; } .quote-layout-card-title-group { @@ -1190,89 +1308,22 @@ } .quote-layout-card-title { - font-size: 14px; + font-size: 13px; font-weight: 600; color: var(--text-primary); } .quote-layout-card-desc { - font-size: 12px; + font-size: 11px; color: var(--text-tertiary); } -.quote-layout-card-check { - width: 22px; - height: 22px; - border-radius: 50%; - border: 1px solid var(--border-color); - color: transparent; - display: inline-flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - transition: all 0.2s ease; - - &.active { - border-color: var(--primary); - background: var(--primary); - color: #fff; +@media (max-width: 760px) { + .quote-layout-picker { + grid-template-columns: 1fr; } } -.quote-layout-preview { - display: flex; - flex-direction: column; - gap: 8px; - padding: 12px; - border-radius: 12px; - background: color-mix(in srgb, var(--bg-secondary) 92%, var(--bg-primary)); - border: 1px solid color-mix(in srgb, var(--border-color) 70%, transparent); - min-height: 112px; - - &.quote-bottom { - .quote-layout-preview-message { - order: 1; - } - - .quote-layout-preview-quote { - order: 2; - } - } -} - -.quote-layout-preview-quote { - padding: 8px 10px; - border-left: 2px solid var(--primary); - border-radius: 8px; - background: rgba(0, 0, 0, 0.04); - display: flex; - flex-direction: column; - gap: 3px; -} - -.quote-layout-preview-sender { - font-size: 12px; - font-weight: 600; - color: var(--primary); -} - -.quote-layout-preview-text { - font-size: 12px; - color: var(--text-secondary); - line-height: 1.4; -} - -.quote-layout-preview-message { - align-self: flex-start; - max-width: 88%; - padding: 9px 12px; - border-radius: 12px; - background: color-mix(in srgb, var(--primary) 14%, var(--bg-primary)); - color: var(--text-primary); - font-size: 13px; - line-height: 1.45; -} - .theme-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 36fc4ed..b2caa5f 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -1061,9 +1061,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { ))} -
- - 选择聊天中引用消息与正文的上下顺序,右侧预览会同步展示布局差异。 +
+ + 选择聊天中引用消息与正文的上下顺序,下方预览会同步展示布局差异。
{[ { @@ -1080,15 +1080,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { } ].map(option => { const selected = quoteLayout === option.value - const quotePreview = ( -
- 张三 - 这是一条被引用的消息 -
- ) - const messagePreview = ( -
这是当前发送的回复内容
- ) + const isQuoteBottom = option.value === 'quote-bottom' return ( ) diff --git a/src/pages/WelcomePage.tsx b/src/pages/WelcomePage.tsx index ff1fd0d..8f529cc 100644 --- a/src/pages/WelcomePage.tsx +++ b/src/pages/WelcomePage.tsx @@ -13,6 +13,7 @@ import './WelcomePage.scss' const isMac = navigator.userAgent.toLowerCase().includes('mac') const isLinux = navigator.userAgent.toLowerCase().includes('linux') +const isWindows = !isMac && !isLinux const dbDirName = isMac ? '2.0b4.0.9 目录' : 'xwechat_files 目录' const dbPathPlaceholder = isMac @@ -46,6 +47,19 @@ const formatDbKeyFailureMessage = (error?: string, logs?: string[]): string => { return `${base};最近状态:${tailLogs.join(' | ')}` } +const normalizeDbKeyStatusMessage = (message: string): string => { + if (isWindows && message.includes('Hook安装成功')) { + return '已准备就绪,现在登录微信或退出登录后重新登录微信' + } + return message +} + +const isDbKeyReadyMessage = (message: string): boolean => ( + message.includes('现在可以登录') + || message.includes('Hook安装成功') + || message.includes('已准备就绪,现在登录微信或退出登录后重新登录微信') +) + function WelcomePage({ standalone = false }: WelcomePageProps) { const navigate = useNavigate() const { isDbConnected, setDbConnected, setLoading } = useAppStore() @@ -139,8 +153,9 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { useEffect(() => { const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => { - setDbKeyStatus(payload.message) - if (payload.message.includes('现在可以登录') || payload.message.includes('Hook安装成功')) { + const normalizedMessage = normalizeDbKeyStatusMessage(payload.message) + setDbKeyStatus(normalizedMessage) + if (isDbKeyReadyMessage(normalizedMessage)) { window.electronAPI.notification?.show({ title: 'WeFlow 准备就绪', content: '现在可以登录微信了', @@ -761,8 +776,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) { )}
- {dbKeyStatus &&
{dbKeyStatus}
} -
点击自动获取后微信将重启,请留意弹窗提示
+ {dbKeyStatus &&
{dbKeyStatus}
}
)}