修复群导出时的错误昵称判定;修复引用样式的一些错误;修复打包问题

This commit is contained in:
cc
2026-03-22 11:25:59 +08:00
parent 58f22f4bb2
commit 641a3bf2ab
12 changed files with 415 additions and 251 deletions

View File

@@ -223,6 +223,12 @@ jobs:
- Linux (.tar.gz): ${LINUX_TAR_URL:-$RELEASE_PAGE} - Linux (.tar.gz): ${LINUX_TAR_URL:-$RELEASE_PAGE}
- linux (.AppImage): ${LINUX_APPIMAGE_URL:-$RELEASE_PAGE} - linux (.AppImage): ${LINUX_APPIMAGE_URL:-$RELEASE_PAGE}
## macOS 安装提示(未知来源)
- 若打开时提示“来自未知开发者”或“无法验证开发者”,请到「系统设置 -> 隐私与安全性」中允许打开该应用。
- 如果仍被系统拦截,请在终端执行以下命令去除隔离标记:
- `xattr -dr com.apple.quarantine "/Applications/WeFlow.app"`
- 执行后重新打开 WeFlow。
> 如果某个平台链接暂时未生成,可进入完整发布页查看全部资源:$RELEASE_PAGE > 如果某个平台链接暂时未生成,可进入完整发布页查看全部资源:$RELEASE_PAGE
EOF EOF

View File

@@ -146,33 +146,47 @@ const normalizeReleaseNotes = (rawReleaseNotes: unknown): string => {
if (!merged.trim()) return '' 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 的场景 // 兼容 electron-updater 直接返回 HTML 的场景
const removeDownloadSectionFromHtml = (input: string): string => { const removeDownloadSectionFromHtml = (input: string): string => {
return input.replace( return input
/<h[1-6][^>]*>\s*(?:下载|download)\s*<\/h[1-6]>\s*[\s\S]*?(?=<h[1-6]\b|$)/gi, .replace(
'' /<h[1-6][^>]*>\s*(?:下载|download)\s*<\/h[1-6]>\s*[\s\S]*?(?=<h[1-6]\b|$)/gi,
) ''
)
.replace(
/<h[1-6][^>]*>\s*(?:mac\s*os|mac)\s*安装提示(?:\s*[(]\s*未知来源\s*[)])?\s*<\/h[1-6]>\s*[\s\S]*?(?=<h[1-6]\b|$)/gi,
''
)
} }
// 兼容 Markdown 场景Action 最终 release note 模板) // 兼容 Markdown 场景Action 最终 release note 模板)
const removeDownloadSectionFromMarkdown = (input: string): string => { const removeDownloadSectionFromMarkdown = (input: string): string => {
const lines = input.split(/\r?\n/) const lines = input.split(/\r?\n/)
const output: string[] = [] const output: string[] = []
let skipDownloadSection = false let skipSection = false
for (const line of lines) { for (const line of lines) {
const headingMatch = line.match(/^\s*#{1,6}\s*(.+?)\s*$/) const headingMatch = line.match(/^\s*#{1,6}\s*(.+?)\s*$/)
if (headingMatch) { if (headingMatch) {
const heading = headingMatch[1].trim().toLowerCase() if (shouldStripReleaseSection(headingMatch[1])) {
if (heading === '下载' || heading === 'download') { skipSection = true
skipDownloadSection = true
continue continue
} }
if (skipDownloadSection) { if (skipSection) {
skipDownloadSection = false skipSection = false
} }
} }
if (!skipDownloadSection) { if (!skipSection) {
output.push(line) output.push(line)
} }
} }

View File

@@ -5096,14 +5096,35 @@ class ChatService {
} }
// 如果是群聊,尝试获取群昵称 // 如果是群聊,尝试获取群昵称
let groupNicknames: Record<string, string> = {} const groupNicknames = new Map<string, string>()
if (chatroomId.endsWith('@chatroom')) { if (chatroomId.endsWith('@chatroom')) {
const nickResult = await wcdbService.getGroupNicknames(chatroomId) const nickResult = await wcdbService.getGroupNicknames(chatroomId)
if (nickResult.success && nickResult.nicknames) { if (nickResult.success && nickResult.nicknames) {
groupNicknames = nickResult.nicknames const nicknameBuckets = new Map<string, Set<string>>()
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用于识别"自己" // 获取当前用户 wxid用于识别"自己"
const myWxid = this.configService.get('myWxid') const myWxid = this.configService.get('myWxid')
const cleanedMyWxid = myWxid ? this.cleanAccountDirName(myWxid) : '' const cleanedMyWxid = myWxid ? this.cleanAccountDirName(myWxid) : ''
@@ -5113,7 +5134,7 @@ class ChatService {
// 特判如果是当前用户自己contact 表通常不包含自己) // 特判如果是当前用户自己contact 表通常不包含自己)
if (myWxid && (username === myWxid || username === cleanedMyWxid)) { if (myWxid && (username === myWxid || username === cleanedMyWxid)) {
// 先查群昵称中是否有自己 // 先查群昵称中是否有自己
const myGroupNick = groupNicknames[username] const myGroupNick = lookupGroupNickname(username) || lookupGroupNickname(myWxid)
if (myGroupNick) return myGroupNick if (myGroupNick) return myGroupNick
// 尝试从缓存获取自己的昵称 // 尝试从缓存获取自己的昵称
const cached = this.avatarCache.get(username) || this.avatarCache.get(myWxid) 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 if (groupNick) return groupNick
// 再查联系人信息 // 再查联系人信息

View File

@@ -1404,33 +1404,60 @@ class ExportService {
} }
/** /**
* 获取群成员群昵称。优先使用 DLL必要时回退到 `contact.chat_room.ext_buffer` 解析 * 获取群成员群昵称。后端结果为唯一业务真值,前端仅做冲突净化防串号
*/ */
async getGroupNicknamesForRoom(chatroomId: string, candidates: string[] = []): Promise<Map<string, string>> { async getGroupNicknamesForRoom(chatroomId: string, candidates: string[] = []): Promise<Map<string, string>> {
const nicknameMap = new Map<string, string>()
try { try {
const dllResult = await wcdbService.getGroupNicknames(chatroomId) const dllResult = await wcdbService.getGroupNicknames(chatroomId)
if (dllResult.success && dllResult.nicknames) { if (!dllResult.success || !dllResult.nicknames) {
this.mergeGroupNicknameEntries(nicknameMap, Object.entries(dllResult.nicknames)) return new Map<string, string>()
} }
return this.buildTrustedGroupNicknameMap(Object.entries(dllResult.nicknames), candidates)
} catch (e) { } catch (e) {
console.error('getGroupNicknamesForRoom dll error:', e) console.error('getGroupNicknamesForRoom dll error:', e)
return new Map<string, string>()
}
}
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<string, string> {
const candidateSet = new Set(
this.buildGroupNicknameIdCandidates(candidates)
.map((id) => this.normalizeGroupNicknameIdentity(id))
.filter(Boolean)
)
const buckets = new Map<string, Set<string>>()
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 trusted = new Map<string, string>()
const result = await wcdbService.getChatRoomExtBuffer(chatroomId) for (const [identity, nicknameSet] of buckets.entries()) {
if (!result.success || !result.extBuffer) { if (nicknameSet.size !== 1) continue
return nicknameMap trusted.set(identity, Array.from(nicknameSet)[0])
}
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
} }
return trusted
} }
private mergeGroupNicknameEntries( private mergeGroupNicknameEntries(
@@ -1680,8 +1707,6 @@ class ExportService {
const raw = String(rawValue || '').trim() const raw = String(rawValue || '').trim()
if (!raw) continue if (!raw) continue
set.add(raw) set.add(raw)
const cleaned = this.cleanAccountDirName(raw)
if (cleaned && cleaned !== raw) set.add(cleaned)
} }
return Array.from(set) return Array.from(set)
} }
@@ -1690,29 +1715,20 @@ class ExportService {
const idCandidates = this.buildGroupNicknameIdCandidates(candidates) const idCandidates = this.buildGroupNicknameIdCandidates(candidates)
if (idCandidates.length === 0) return '' if (idCandidates.length === 0) return ''
let resolved = ''
for (const id of idCandidates) { for (const id of idCandidates) {
const exact = this.normalizeGroupNickname(groupNicknamesMap.get(id) || '') const normalizedId = this.normalizeGroupNicknameIdentity(id)
if (exact) return exact if (!normalizedId) continue
const lower = this.normalizeGroupNickname(groupNicknamesMap.get(id.toLowerCase()) || '') const candidateNickname = this.normalizeGroupNickname(groupNicknamesMap.get(normalizedId) || '')
if (lower) return lower if (!candidateNickname) continue
} if (!resolved) {
resolved = candidateNickname
for (const id of idCandidates) { continue
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 ''
} }
if (matched === 1 && found) return found if (resolved !== candidateNickname) return ''
} }
return '' return resolved
} }
/** /**

View File

@@ -257,34 +257,60 @@ class GroupAnalyticsService {
} }
/** /**
* 从 DLL 获取群成员群昵称 * 从后端获取群成员群昵称,并在前端进行唯一性净化防串号。
*/ */
private async getGroupNicknamesForRoom(chatroomId: string, candidates: string[] = []): Promise<Map<string, string>> { private async getGroupNicknamesForRoom(chatroomId: string, candidates: string[] = []): Promise<Map<string, string>> {
const nicknameMap = new Map<string, string>()
try { try {
const dllResult = await wcdbService.getGroupNicknames(chatroomId) const dllResult = await wcdbService.getGroupNicknames(chatroomId)
if (dllResult.success && dllResult.nicknames) { if (!dllResult.success || !dllResult.nicknames) {
this.mergeGroupNicknameEntries(nicknameMap, Object.entries(dllResult.nicknames)) return new Map<string, string>()
} }
return this.buildTrustedGroupNicknameMap(Object.entries(dllResult.nicknames), candidates)
} catch (e) { } catch (e) {
console.error('getGroupNicknamesForRoom dll error:', e) console.error('getGroupNicknamesForRoom dll error:', e)
return new Map<string, string>()
} }
}
try { private normalizeGroupNicknameIdentity(value: string): string {
const result = await wcdbService.getChatRoomExtBuffer(chatroomId) const raw = String(value || '').trim()
if (!result.success || !result.extBuffer) { if (!raw) return ''
return nicknameMap return raw.toLowerCase()
}
private buildTrustedGroupNicknameMap(
entries: Iterable<[string, string]>,
candidates: string[] = []
): Map<string, string> {
const candidateSet = new Set(
this.buildGroupNicknameIdCandidates(candidates)
.map((id) => this.normalizeGroupNicknameIdentity(id))
.filter(Boolean)
)
const buckets = new Map<string, Set<string>>()
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<string, string>()
for (const [identity, nicknameSet] of buckets.entries()) {
if (nicknameSet.size !== 1) continue
trusted.set(identity, Array.from(nicknameSet)[0])
}
return trusted
} }
private mergeGroupNicknameEntries( private mergeGroupNicknameEntries(
@@ -475,6 +501,16 @@ class GroupAnalyticsService {
return Array.from(set) return Array.from(set)
} }
private buildGroupNicknameIdCandidates(values: Array<string | undefined | null>): string[] {
const set = new Set<string>()
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 { private toNonNegativeInteger(value: unknown): number {
const parsed = Number(value) const parsed = Number(value)
if (!Number.isFinite(parsed)) return 0 if (!Number.isFinite(parsed)) return 0
@@ -663,30 +699,23 @@ class GroupAnalyticsService {
} }
private resolveGroupNicknameByCandidates(groupNicknames: Map<string, string>, candidates: string[]): string { private resolveGroupNicknameByCandidates(groupNicknames: Map<string, string>, candidates: string[]): string {
const idCandidates = this.buildIdCandidates(candidates) const idCandidates = this.buildGroupNicknameIdCandidates(candidates)
if (idCandidates.length === 0) return '' if (idCandidates.length === 0) return ''
let resolved = ''
for (const id of idCandidates) { for (const id of idCandidates) {
const exact = this.normalizeGroupNickname(groupNicknames.get(id) || '') const normalizedId = this.normalizeGroupNicknameIdentity(id)
if (exact) return exact if (!normalizedId) continue
} const candidateNickname = this.normalizeGroupNickname(groupNicknames.get(normalizedId) || '')
if (!candidateNickname) continue
for (const id of idCandidates) { if (!resolved) {
const lower = id.toLowerCase() resolved = candidateNickname
let found = '' continue
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 ''
} }
if (matched === 1 && found) return found if (resolved !== candidateNickname) return ''
} }
return '' return resolved
} }
private sanitizeWorksheetName(name: string): string { private sanitizeWorksheetName(name: string): string {

View File

@@ -1017,13 +1017,31 @@ class HttpService {
} }
private lookupGroupNickname(groupNicknamesMap: Map<string, string>, sender: string): string { private lookupGroupNickname(groupNicknamesMap: Map<string, string>, sender: string): string {
if (!sender) return '' const key = String(sender || '').trim().toLowerCase()
const cleaned = this.normalizeAccountId(sender) if (!key) return ''
return groupNicknamesMap.get(sender) return groupNicknamesMap.get(key) || ''
|| groupNicknamesMap.get(sender.toLowerCase()) }
|| groupNicknamesMap.get(cleaned)
|| groupNicknamesMap.get(cleaned.toLowerCase()) private buildTrustedGroupNicknameMap(nicknames: Record<string, string>): Map<string, string> {
|| '' const buckets = new Map<string, Set<string>>()
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<string, string>()
for (const [memberId, nicknameSet] of buckets.entries()) {
if (nicknameSet.size !== 1) continue
trusted.set(memberId, Array.from(nicknameSet)[0])
}
return trusted
} }
private resolveChatLabSenderInfo( private resolveChatLabSenderInfo(
@@ -1094,21 +1112,7 @@ class HttpService {
try { try {
const result = await wcdbService.getGroupNicknames(talkerId) const result = await wcdbService.getGroupNicknames(talkerId)
if (result.success && result.nicknames) { if (result.success && result.nicknames) {
groupNicknamesMap = new Map() groupNicknamesMap = this.buildTrustedGroupNicknameMap(result.nicknames)
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)
}
}
} }
} catch (e) { } catch (e) {
console.error('[HttpService] Failed to get group nicknames:', e) console.error('[HttpService] Failed to get group nicknames:', e)
@@ -1389,4 +1393,3 @@ class HttpService {
} }
export const httpService = new HttpService() export const httpService = new HttpService()

View File

@@ -304,11 +304,8 @@ class MessagePushService {
} }
const groupNicknames = await this.getGroupNicknames(chatroomId) const groupNicknames = await this.getGroupNicknames(chatroomId)
const normalizedSender = this.normalizeAccountId(senderUsername) const senderKey = senderUsername.toLowerCase()
const nickname = groupNicknames[senderUsername] const nickname = groupNicknames[senderKey]
|| groupNicknames[senderUsername.toLowerCase()]
|| groupNicknames[normalizedSender]
|| groupNicknames[normalizedSender.toLowerCase()]
if (nickname) { if (nickname) {
return nickname return nickname
@@ -328,22 +325,33 @@ class MessagePushService {
} }
const result = await wcdbService.getGroupNicknames(cacheKey) 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() }) this.groupNicknameCache.set(cacheKey, { nicknames, updatedAt: Date.now() })
return nicknames return nicknames
} }
private normalizeAccountId(value: string): string { private sanitizeGroupNicknames(nicknames: Record<string, string>): Record<string, string> {
const trimmed = String(value || '').trim() const buckets = new Map<string, Set<string>>()
if (!trimmed) return trimmed for (const [memberIdRaw, nicknameRaw] of Object.entries(nicknames || {})) {
const memberId = String(memberIdRaw || '').trim().toLowerCase()
if (trimmed.toLowerCase().startsWith('wxid_')) { const nickname = String(nicknameRaw || '').trim()
const match = trimmed.match(/^(wxid_[^_]+)/i) if (!memberId || !nickname) continue
return match ? match[1] : trimmed 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})$/) const trusted: Record<string, string> = {}
return suffixMatch ? suffixMatch[1] : trimmed 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 { private isRecentMessage(messageKey: string): boolean {

Binary file not shown.

View File

@@ -625,7 +625,7 @@
.bubble-content { .bubble-content {
background: var(--primary-gradient); background: var(--primary-gradient);
color: #fff; color: var(--on-primary);
border-radius: 18px 18px 4px 18px; border-radius: 18px 18px 4px 18px;
padding: 10px 14px; padding: 10px 14px;
font-size: 14px; font-size: 14px;
@@ -1962,7 +1962,7 @@
.bubble-content { .bubble-content {
background: var(--primary); background: var(--primary);
color: white; color: var(--on-primary);
border-radius: 18px 18px 4px 18px; border-radius: 18px 18px 4px 18px;
} }
} }
@@ -2453,15 +2453,15 @@
// 自己发送的消息中的引用样式 // 自己发送的消息中的引用样式
.message-bubble.sent .quoted-message { .message-bubble.sent .quoted-message {
background: rgba(255, 255, 255, 0.15); background: color-mix(in srgb, var(--on-primary) 12%, var(--primary));
border-left-color: rgba(255, 255, 255, 0.5); border-left-color: color-mix(in srgb, var(--on-primary) 36%, var(--primary));
.quoted-sender { .quoted-sender {
color: rgba(255, 255, 255, 0.9); color: color-mix(in srgb, var(--on-primary) 92%, var(--primary));
} }
.quoted-text { .quoted-text {
color: rgba(255, 255, 255, 0.8); color: color-mix(in srgb, var(--on-primary) 80%, var(--primary));
} }
} }

View File

@@ -1145,9 +1145,13 @@
} }
} }
.quote-layout-group {
margin-top: 14px;
}
.quote-layout-picker { .quote-layout-picker {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 12px; gap: 12px;
margin-top: 10px; margin-top: 10px;
} }
@@ -1155,32 +1159,146 @@
.quote-layout-card { .quote-layout-card {
position: relative; position: relative;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 16px; border-radius: 14px;
padding: 14px; padding: 14px 14px 12px;
background: var(--bg-primary); background: color-mix(in srgb, var(--primary) 6%, var(--bg-primary));
color: inherit; color: inherit;
cursor: pointer; cursor: pointer;
text-align: left; 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 { &:hover {
border-color: color-mix(in srgb, var(--primary) 35%, var(--border-color)); border-color: color-mix(in srgb, var(--primary) 32%, var(--border-color));
transform: translateY(-1px);
} }
&.active { &.active {
border-color: var(--primary); border-color: color-mix(in srgb, var(--primary) 68%, var(--border-color));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 14%, transparent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 12%, transparent);
background: color-mix(in srgb, var(--bg-primary) 92%, var(--primary) 8%); 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; display: flex;
align-items: flex-start; 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 { .quote-layout-card-title-group {
@@ -1190,89 +1308,22 @@
} }
.quote-layout-card-title { .quote-layout-card-title {
font-size: 14px; font-size: 13px;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
} }
.quote-layout-card-desc { .quote-layout-card-desc {
font-size: 12px; font-size: 11px;
color: var(--text-tertiary); color: var(--text-tertiary);
} }
.quote-layout-card-check { @media (max-width: 760px) {
width: 22px; .quote-layout-picker {
height: 22px; grid-template-columns: 1fr;
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;
} }
} }
.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 { .theme-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));

View File

@@ -1061,9 +1061,9 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
))} ))}
</div> </div>
<div className="form-group"> <div className="form-group quote-layout-group">
<label></label> <label></label>
<span className="form-hint"></span> <span className="form-hint"></span>
<div className="quote-layout-picker" role="radiogroup" aria-label="引用样式选择"> <div className="quote-layout-picker" role="radiogroup" aria-label="引用样式选择">
{[ {[
{ {
@@ -1080,15 +1080,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
} }
].map(option => { ].map(option => {
const selected = quoteLayout === option.value const selected = quoteLayout === option.value
const quotePreview = ( const isQuoteBottom = option.value === 'quote-bottom'
<div className="quote-layout-preview-quote">
<span className="quote-layout-preview-sender"></span>
<span className="quote-layout-preview-text"></span>
</div>
)
const messagePreview = (
<div className="quote-layout-preview-message"></div>
)
return ( return (
<button <button
@@ -1104,27 +1096,37 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
role="radio" role="radio"
aria-checked={selected} aria-checked={selected}
> >
<div className="quote-layout-card-header"> <span className={`quote-layout-card-check ${selected ? 'active' : ''}`} aria-hidden="true" />
<div className="quote-layout-preview-shell">
<div className="quote-layout-preview-chat">
<div className="message-bubble sent">
<div className={`bubble-content ${isQuoteBottom ? 'quote-layout-bottom' : 'quote-layout-top'}`}>
{isQuoteBottom ? (
<>
<div className="message-text">!</div>
<div className="quoted-message">
<span className="quoted-sender"></span>
<span className="quoted-text">...</span>
</div>
</>
) : (
<>
<div className="quoted-message">
<span className="quoted-sender"></span>
<span className="quoted-text">...</span>
</div>
<div className="message-text">!</div>
</>
)}
</div>
</div>
</div>
</div>
<div className="quote-layout-card-footer">
<div className="quote-layout-card-title-group"> <div className="quote-layout-card-title-group">
<span className="quote-layout-card-title">{option.label}</span> <span className="quote-layout-card-title">{option.label}</span>
<span className="quote-layout-card-desc">{option.description}</span> <span className="quote-layout-card-desc">{option.description}</span>
</div> </div>
<span className={`quote-layout-card-check ${selected ? 'active' : ''}`}>
<Check size={14} />
</span>
</div>
<div className={`quote-layout-preview ${option.value}`}>
{option.value === 'quote-bottom' ? (
<>
{messagePreview}
{quotePreview}
</>
) : (
<>
{quotePreview}
{messagePreview}
</>
)}
</div> </div>
</button> </button>
) )

View File

@@ -13,6 +13,7 @@ import './WelcomePage.scss'
const isMac = navigator.userAgent.toLowerCase().includes('mac') const isMac = navigator.userAgent.toLowerCase().includes('mac')
const isLinux = navigator.userAgent.toLowerCase().includes('linux') const isLinux = navigator.userAgent.toLowerCase().includes('linux')
const isWindows = !isMac && !isLinux
const dbDirName = isMac ? '2.0b4.0.9 目录' : 'xwechat_files 目录' const dbDirName = isMac ? '2.0b4.0.9 目录' : 'xwechat_files 目录'
const dbPathPlaceholder = isMac const dbPathPlaceholder = isMac
@@ -46,6 +47,19 @@ const formatDbKeyFailureMessage = (error?: string, logs?: string[]): string => {
return `${base};最近状态:${tailLogs.join(' | ')}` 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) { function WelcomePage({ standalone = false }: WelcomePageProps) {
const navigate = useNavigate() const navigate = useNavigate()
const { isDbConnected, setDbConnected, setLoading } = useAppStore() const { isDbConnected, setDbConnected, setLoading } = useAppStore()
@@ -139,8 +153,9 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
useEffect(() => { useEffect(() => {
const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => { const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => {
setDbKeyStatus(payload.message) const normalizedMessage = normalizeDbKeyStatusMessage(payload.message)
if (payload.message.includes('现在可以登录') || payload.message.includes('Hook安装成功')) { setDbKeyStatus(normalizedMessage)
if (isDbKeyReadyMessage(normalizedMessage)) {
window.electronAPI.notification?.show({ window.electronAPI.notification?.show({
title: 'WeFlow 准备就绪', title: 'WeFlow 准备就绪',
content: '现在可以登录微信了', content: '现在可以登录微信了',
@@ -761,8 +776,7 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
)} )}
</div> </div>
{dbKeyStatus && <div className={`status-message ${dbKeyStatus.includes('现在可以登录') || dbKeyStatus.includes('Hook安装成功') ? 'is-success' : ''}`}>{dbKeyStatus}</div>} {dbKeyStatus && <div className={`status-message ${isDbKeyReadyMessage(dbKeyStatus) ? 'is-success' : ''}`}>{dbKeyStatus}</div>}
<div className="field-hint"></div>
</div> </div>
)} )}