mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
Merge pull request #456 from H3CoF6/feat/global_config
feat: 解析global_config, 优化账号选择体验,顺便修个搜索的bug
This commit is contained in:
@@ -1,13 +1,90 @@
|
|||||||
import { join, basename } from 'path'
|
import { join, basename } from 'path'
|
||||||
import { existsSync, readdirSync, statSync } from 'fs'
|
import { existsSync, readdirSync, statSync, readFileSync } from 'fs'
|
||||||
import { homedir } from 'os'
|
import { homedir } from 'os'
|
||||||
|
import { createDecipheriv } from 'crypto'
|
||||||
|
|
||||||
export interface WxidInfo {
|
export interface WxidInfo {
|
||||||
wxid: string
|
wxid: string
|
||||||
modifiedTime: number
|
modifiedTime: number
|
||||||
|
nickname?: string
|
||||||
|
avatarUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DbPathService {
|
export class DbPathService {
|
||||||
|
private readVarint(buf: Buffer, offset: number): { value: number, length: number } {
|
||||||
|
let value = 0;
|
||||||
|
let length = 0;
|
||||||
|
let shift = 0;
|
||||||
|
while (offset < buf.length && shift < 32) {
|
||||||
|
const b = buf[offset++];
|
||||||
|
value |= (b & 0x7f) << shift;
|
||||||
|
length++;
|
||||||
|
if ((b & 0x80) === 0) break;
|
||||||
|
shift += 7;
|
||||||
|
}
|
||||||
|
return { value, length };
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractMmkvString(buf: Buffer, keyName: string): string {
|
||||||
|
const keyBuf = Buffer.from(keyName, 'utf8');
|
||||||
|
const idx = buf.indexOf(keyBuf);
|
||||||
|
if (idx === -1) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
let offset = idx + keyBuf.length;
|
||||||
|
const v1 = this.readVarint(buf, offset);
|
||||||
|
offset += v1.length;
|
||||||
|
const v2 = this.readVarint(buf, offset);
|
||||||
|
offset += v2.length;
|
||||||
|
|
||||||
|
// 合理性检查
|
||||||
|
if (v2.value > 0 && v2.value <= 10000 && offset + v2.value <= buf.length) {
|
||||||
|
return buf.toString('utf8', offset, offset + v2.value);
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseGlobalConfig(rootPath: string): { wxid: string, nickname: string, avatarUrl: string } | null {
|
||||||
|
try {
|
||||||
|
const configPath = join(rootPath, 'all_users', 'config', 'global_config');
|
||||||
|
if (!existsSync(configPath)) return null;
|
||||||
|
|
||||||
|
const fullData = readFileSync(configPath);
|
||||||
|
if (fullData.length <= 4) return null;
|
||||||
|
const encryptedData = fullData.subarray(4);
|
||||||
|
|
||||||
|
const key = Buffer.alloc(16, 0);
|
||||||
|
Buffer.from('xwechat_crypt_key').copy(key); // 直接硬编码,iv更是不重要
|
||||||
|
const iv = Buffer.alloc(16, 0);
|
||||||
|
|
||||||
|
const decipher = createDecipheriv('aes-128-cfb', key, iv);
|
||||||
|
decipher.setAutoPadding(false);
|
||||||
|
const decrypted = Buffer.concat([decipher.update(encryptedData), decipher.final()]);
|
||||||
|
|
||||||
|
const wxid = this.extractMmkvString(decrypted, 'mmkv_key_user_name');
|
||||||
|
const nickname = this.extractMmkvString(decrypted, 'mmkv_key_nick_name');
|
||||||
|
let avatarUrl = this.extractMmkvString(decrypted, 'mmkv_key_head_img_url');
|
||||||
|
|
||||||
|
if (!avatarUrl && decrypted.includes('http')) {
|
||||||
|
const httpIdx = decrypted.indexOf('http');
|
||||||
|
const nullIdx = decrypted.indexOf(0x00, httpIdx);
|
||||||
|
if (nullIdx !== -1) {
|
||||||
|
avatarUrl = decrypted.toString('utf8', httpIdx, nullIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wxid || nickname) {
|
||||||
|
return { wxid, nickname, avatarUrl };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析 global_config 失败:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自动检测微信数据库根目录
|
* 自动检测微信数据库根目录
|
||||||
*/
|
*/
|
||||||
@@ -135,21 +212,16 @@ export class DbPathService {
|
|||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const entryPath = join(rootPath, entry)
|
const entryPath = join(rootPath, entry)
|
||||||
let stat: ReturnType<typeof statSync>
|
let stat: ReturnType<typeof statSync>
|
||||||
try {
|
try { stat = statSync(entryPath) } catch { continue }
|
||||||
stat = statSync(entryPath)
|
|
||||||
} catch {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!stat.isDirectory()) continue
|
if (!stat.isDirectory()) continue
|
||||||
const lower = entry.toLowerCase()
|
const lower = entry.toLowerCase()
|
||||||
if (lower === 'all_users') continue
|
if (lower === 'all_users') continue
|
||||||
if (!entry.includes('_')) continue
|
if (!entry.includes('_')) continue
|
||||||
|
|
||||||
wxids.push({ wxid: entry, modifiedTime: stat.mtimeMs })
|
wxids.push({ wxid: entry, modifiedTime: stat.mtimeMs })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (wxids.length === 0) {
|
if (wxids.length === 0) {
|
||||||
const rootName = basename(rootPath)
|
const rootName = basename(rootPath)
|
||||||
if (rootName.includes('_') && rootName.toLowerCase() !== 'all_users') {
|
if (rootName.includes('_') && rootName.toLowerCase() !== 'all_users') {
|
||||||
@@ -159,11 +231,24 @@ export class DbPathService {
|
|||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|
||||||
return wxids.sort((a, b) => {
|
const sorted = wxids.sort((a, b) => {
|
||||||
if (b.modifiedTime !== a.modifiedTime) return b.modifiedTime - a.modifiedTime
|
if (b.modifiedTime !== a.modifiedTime) return b.modifiedTime - a.modifiedTime
|
||||||
return a.wxid.localeCompare(b.wxid)
|
return a.wxid.localeCompare(b.wxid)
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const globalInfo = this.parseGlobalConfig(rootPath);
|
||||||
|
if (globalInfo) {
|
||||||
|
for (const w of sorted) {
|
||||||
|
if (w.wxid.startsWith(globalInfo.wxid) || sorted.length === 1) {
|
||||||
|
w.nickname = globalInfo.nickname;
|
||||||
|
w.avatarUrl = globalInfo.avatarUrl;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 扫描 wxid 列表
|
* 扫描 wxid 列表
|
||||||
@@ -187,10 +272,21 @@ export class DbPathService {
|
|||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
|
|
||||||
return wxids.sort((a, b) => {
|
const sorted = wxids.sort((a, b) => {
|
||||||
if (b.modifiedTime !== a.modifiedTime) return b.modifiedTime - a.modifiedTime
|
if (b.modifiedTime !== a.modifiedTime) return b.modifiedTime - a.modifiedTime
|
||||||
return a.wxid.localeCompare(b.wxid)
|
return a.wxid.localeCompare(b.wxid)
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const globalInfo = this.parseGlobalConfig(rootPath);
|
||||||
|
if (globalInfo) {
|
||||||
|
for (const w of sorted) {
|
||||||
|
if (w.wxid.startsWith(globalInfo.wxid) || sorted.length === 1) {
|
||||||
|
w.nickname = globalInfo.nickname;
|
||||||
|
w.avatarUrl = globalInfo.avatarUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useChatStore } from '../stores/chatStore'
|
|||||||
import { useAnalyticsStore } from '../stores/analyticsStore'
|
import { useAnalyticsStore } from '../stores/analyticsStore'
|
||||||
import * as configService from '../services/config'
|
import * as configService from '../services/config'
|
||||||
import { onExportSessionStatus, requestExportSessionStatus } from '../services/exportBridge'
|
import { onExportSessionStatus, requestExportSessionStatus } from '../services/exportBridge'
|
||||||
|
import { UserRound } from 'lucide-react'
|
||||||
|
|
||||||
import './Sidebar.scss'
|
import './Sidebar.scss'
|
||||||
|
|
||||||
@@ -35,6 +36,7 @@ interface AccountProfilesCache {
|
|||||||
interface WxidOption {
|
interface WxidOption {
|
||||||
wxid: string
|
wxid: string
|
||||||
modifiedTime: number
|
modifiedTime: number
|
||||||
|
nickname?: string
|
||||||
displayName?: string
|
displayName?: string
|
||||||
avatarUrl?: string
|
avatarUrl?: string
|
||||||
}
|
}
|
||||||
@@ -280,26 +282,28 @@ function Sidebar({ collapsed }: SidebarProps) {
|
|||||||
const accountsCache = readAccountProfilesCache()
|
const accountsCache = readAccountProfilesCache()
|
||||||
console.log('[切换账号] 账号缓存:', accountsCache)
|
console.log('[切换账号] 账号缓存:', accountsCache)
|
||||||
|
|
||||||
const enrichedWxids = wxids.map(option => {
|
const enrichedWxids = wxids.map((option: WxidOption) => {
|
||||||
const normalizedWxid = normalizeAccountId(option.wxid)
|
const normalizedWxid = normalizeAccountId(option.wxid)
|
||||||
const cached = accountsCache[option.wxid] || accountsCache[normalizedWxid]
|
const cached = accountsCache[option.wxid] || accountsCache[normalizedWxid]
|
||||||
|
|
||||||
|
let displayName = option.nickname || option.wxid
|
||||||
|
let avatarUrl = option.avatarUrl
|
||||||
|
|
||||||
if (option.wxid === userProfile.wxid || normalizedWxid === userProfile.wxid) {
|
if (option.wxid === userProfile.wxid || normalizedWxid === userProfile.wxid) {
|
||||||
|
displayName = userProfile.displayName || displayName
|
||||||
|
avatarUrl = userProfile.avatarUrl || avatarUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (cached) {
|
||||||
|
displayName = cached.displayName || displayName
|
||||||
|
avatarUrl = cached.avatarUrl || avatarUrl
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...option,
|
...option,
|
||||||
displayName: userProfile.displayName,
|
displayName,
|
||||||
avatarUrl: userProfile.avatarUrl
|
avatarUrl
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (cached) {
|
|
||||||
console.log('[切换账号] 使用缓存:', option.wxid, cached)
|
|
||||||
return {
|
|
||||||
...option,
|
|
||||||
displayName: cached.displayName,
|
|
||||||
avatarUrl: cached.avatarUrl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { ...option, displayName: option.wxid }
|
|
||||||
})
|
})
|
||||||
|
|
||||||
setWxidOptions(enrichedWxids)
|
setWxidOptions(enrichedWxids)
|
||||||
@@ -553,11 +557,17 @@ function Sidebar({ collapsed }: SidebarProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div className="wxid-avatar">
|
<div className="wxid-avatar">
|
||||||
{option.avatarUrl ? <img src={option.avatarUrl} alt="" /> : <span>{getAvatarLetter(option.displayName || option.wxid)}</span>}
|
{option.avatarUrl ? (
|
||||||
|
<img src={option.avatarUrl} alt="" />
|
||||||
|
) : (
|
||||||
|
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--bg-tertiary)', borderRadius: '6px', color: 'var(--text-tertiary)' }}>
|
||||||
|
<UserRound size={16} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="wxid-info">
|
<div className="wxid-info">
|
||||||
<div className="wxid-name">{option.displayName || option.wxid}</div>
|
<div className="wxid-name">{option.displayName}</div>
|
||||||
<div className="wxid-id">{option.wxid}</div>
|
{option.displayName !== option.wxid && <div className="wxid-id">{option.wxid}</div>}
|
||||||
</div>
|
</div>
|
||||||
{userProfile.wxid === option.wxid && <span className="current-badge">当前</span>}
|
{userProfile.wxid === option.wxid && <span className="current-badge">当前</span>}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3338,24 +3338,32 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const lower = searchKeyword.toLowerCase()
|
const lower = searchKeyword.toLowerCase()
|
||||||
setFilteredSessions(visible.filter(s => {
|
setFilteredSessions(visible
|
||||||
|
.filter(s => {
|
||||||
|
const matchedByName = s.displayName?.toLowerCase().includes(lower)
|
||||||
|
const matchedByUsername = s.username.toLowerCase().includes(lower)
|
||||||
|
const matchedByAlias = s.alias?.toLowerCase().includes(lower)
|
||||||
|
return matchedByName || matchedByUsername || matchedByAlias
|
||||||
|
})
|
||||||
|
.map(s => {
|
||||||
const matchedByName = s.displayName?.toLowerCase().includes(lower)
|
const matchedByName = s.displayName?.toLowerCase().includes(lower)
|
||||||
const matchedByUsername = s.username.toLowerCase().includes(lower)
|
const matchedByUsername = s.username.toLowerCase().includes(lower)
|
||||||
const matchedByAlias = s.alias?.toLowerCase().includes(lower)
|
const matchedByAlias = s.alias?.toLowerCase().includes(lower)
|
||||||
|
|
||||||
|
let matchedField: 'wxid' | 'alias' | 'name' | undefined = undefined
|
||||||
|
|
||||||
if (matchedByUsername && !matchedByName && !matchedByAlias) {
|
if (matchedByUsername && !matchedByName && !matchedByAlias) {
|
||||||
s.matchedField = 'wxid'
|
matchedField = 'wxid'
|
||||||
} else if (matchedByAlias && !matchedByName && !matchedByUsername) {
|
} else if (matchedByAlias && !matchedByName && !matchedByUsername) {
|
||||||
s.matchedField = 'alias'
|
matchedField = 'alias'
|
||||||
} else if (matchedByName && !matchedByUsername && !matchedByAlias) {
|
} else if (matchedByName && !matchedByUsername && !matchedByAlias) {
|
||||||
(s as any).matchedField = 'name'
|
matchedField = 'name'
|
||||||
console.log('设置 matchedField=name:', s.displayName)
|
|
||||||
} else {
|
|
||||||
s.matchedField = undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return matchedByName || matchedByUsername || matchedByAlias
|
// ✅ 关键点:返回一个新对象,解耦全局状态
|
||||||
}))
|
return { ...s, matchedField }
|
||||||
|
})
|
||||||
|
)
|
||||||
}, [sessions, searchKeyword, setFilteredSessions])
|
}, [sessions, searchKeyword, setFilteredSessions])
|
||||||
|
|
||||||
// 折叠群列表(独立计算,供折叠 panel 使用)
|
// 折叠群列表(独立计算,供折叠 panel 使用)
|
||||||
@@ -3364,22 +3372,34 @@ function ChatPage(props: ChatPageProps) {
|
|||||||
const folded = sessions.filter(s => s.isFolded)
|
const folded = sessions.filter(s => s.isFolded)
|
||||||
if (!searchKeyword.trim() || !foldedView) return folded
|
if (!searchKeyword.trim() || !foldedView) return folded
|
||||||
const lower = searchKeyword.toLowerCase()
|
const lower = searchKeyword.toLowerCase()
|
||||||
return folded.filter(s => {
|
return folded
|
||||||
|
// 1. 先过滤
|
||||||
|
.filter(s => {
|
||||||
const matchedByName = s.displayName?.toLowerCase().includes(lower)
|
const matchedByName = s.displayName?.toLowerCase().includes(lower)
|
||||||
const matchedByUsername = s.username.toLowerCase().includes(lower)
|
const matchedByUsername = s.username.toLowerCase().includes(lower)
|
||||||
const matchedByAlias = s.alias?.toLowerCase().includes(lower)
|
const matchedByAlias = s.alias?.toLowerCase().includes(lower)
|
||||||
const matchedBySummary = s.summary.toLowerCase().includes(lower)
|
const matchedBySummary = s.summary?.toLowerCase().includes(lower) // 注意:这里有个 summary
|
||||||
|
|
||||||
if (matchedByUsername && !matchedByName && !matchedBySummary && !matchedByAlias) {
|
|
||||||
s.matchedField = 'wxid'
|
|
||||||
} else if (matchedByAlias && !matchedByName && !matchedBySummary && !matchedByUsername) {
|
|
||||||
s.matchedField = 'alias'
|
|
||||||
} else {
|
|
||||||
s.matchedField = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchedByName || matchedByUsername || matchedByAlias || matchedBySummary
|
return matchedByName || matchedByUsername || matchedByAlias || matchedBySummary
|
||||||
})
|
})
|
||||||
|
// 2. 后映射
|
||||||
|
.map(s => {
|
||||||
|
const matchedByName = s.displayName?.toLowerCase().includes(lower)
|
||||||
|
const matchedByUsername = s.username.toLowerCase().includes(lower)
|
||||||
|
const matchedByAlias = s.alias?.toLowerCase().includes(lower)
|
||||||
|
const matchedBySummary = s.summary?.toLowerCase().includes(lower)
|
||||||
|
|
||||||
|
let matchedField: 'wxid' | 'alias' | 'name' | undefined = undefined
|
||||||
|
|
||||||
|
if (matchedByUsername && !matchedByName && !matchedBySummary && !matchedByAlias) {
|
||||||
|
matchedField = 'wxid'
|
||||||
|
} else if (matchedByAlias && !matchedByName && !matchedBySummary && !matchedByUsername) {
|
||||||
|
matchedField = 'alias'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 同样返回新对象
|
||||||
|
return { ...s, matchedField }
|
||||||
|
})
|
||||||
}, [sessions, searchKeyword, foldedView])
|
}, [sessions, searchKeyword, foldedView])
|
||||||
|
|
||||||
const hasSessionRecords = Array.isArray(sessions) && sessions.length > 0
|
const hasSessionRecords = Array.isArray(sessions) && sessions.length > 0
|
||||||
|
|||||||
@@ -1705,7 +1705,7 @@
|
|||||||
|
|
||||||
.wxid-dialog-item {
|
.wxid-dialog-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
padding: 14px 16px;
|
padding: 14px 16px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -1743,6 +1743,66 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wxid-profile-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.wxid-avatar {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-avatar-fallback {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-info-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-profile-mini {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.wxid-avatar {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-avatar-fallback {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-info-col {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 通知过滤双列表容器
|
// 通知过滤双列表容器
|
||||||
.notification-filter-container {
|
.notification-filter-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
Eye, EyeOff, FolderSearch, FolderOpen, Search, Copy,
|
Eye, EyeOff, FolderSearch, FolderOpen, Search, Copy,
|
||||||
RotateCcw, Trash2, Plug, Check, Sun, Moon, Monitor,
|
RotateCcw, Trash2, Plug, Check, Sun, Moon, Monitor,
|
||||||
Palette, Database, HardDrive, Info, RefreshCw, ChevronDown, Download, Mic,
|
Palette, Database, HardDrive, Info, RefreshCw, ChevronDown, Download, Mic,
|
||||||
ShieldCheck, Fingerprint, Lock, KeyRound, Bell, Globe, BarChart2, X
|
ShieldCheck, Fingerprint, Lock, KeyRound, Bell, Globe, BarChart2, X, UserRound
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Avatar } from '../components/Avatar'
|
import { Avatar } from '../components/Avatar'
|
||||||
import './SettingsPage.scss'
|
import './SettingsPage.scss'
|
||||||
@@ -34,6 +34,8 @@ const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [
|
|||||||
interface WxidOption {
|
interface WxidOption {
|
||||||
wxid: string
|
wxid: string
|
||||||
modifiedTime: number
|
modifiedTime: number
|
||||||
|
nickname?: string
|
||||||
|
avatarUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SettingsPageProps {
|
interface SettingsPageProps {
|
||||||
@@ -2137,8 +2139,18 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
|||||||
className={`wxid-dialog-item ${opt.wxid === wxid ? 'active' : ''}`}
|
className={`wxid-dialog-item ${opt.wxid === wxid ? 'active' : ''}`}
|
||||||
onClick={() => handleSelectWxid(opt.wxid)}
|
onClick={() => handleSelectWxid(opt.wxid)}
|
||||||
>
|
>
|
||||||
<span className="wxid-id">{opt.wxid}</span>
|
<div className="wxid-profile-row">
|
||||||
<span className="wxid-date">最后修改 {new Date(opt.modifiedTime).toLocaleString()}</span>
|
{opt.avatarUrl ? (
|
||||||
|
<img src={opt.avatarUrl} alt="avatar" className="wxid-avatar" />
|
||||||
|
) : (
|
||||||
|
<div className="wxid-avatar-fallback"><UserRound size={18}/></div>
|
||||||
|
)}
|
||||||
|
<div className="wxid-info-col">
|
||||||
|
<span className="wxid-id">{opt.nickname || opt.wxid}</span>
|
||||||
|
{opt.nickname && <span className="wxid-date">{opt.wxid}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="wxid-date" style={{marginLeft: 'auto'}}>最后修改 {new Date(opt.modifiedTime).toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -488,6 +488,48 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wxid-profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-avatar-fallback {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-nickname {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wxid-sub {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
.field-with-toggle {
|
.field-with-toggle {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,12 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
|||||||
const [imageAesKey, setImageAesKey] = useState('')
|
const [imageAesKey, setImageAesKey] = useState('')
|
||||||
const [cachePath, setCachePath] = useState('')
|
const [cachePath, setCachePath] = useState('')
|
||||||
const [wxid, setWxid] = useState('')
|
const [wxid, setWxid] = useState('')
|
||||||
const [wxidOptions, setWxidOptions] = useState<Array<{ wxid: string; modifiedTime: number }>>([])
|
const [wxidOptions, setWxidOptions] = useState<Array<{
|
||||||
|
avatarUrl?: string;
|
||||||
|
nickname?: string;
|
||||||
|
wxid: string;
|
||||||
|
modifiedTime: number
|
||||||
|
}>>([])
|
||||||
const [showWxidSelect, setShowWxidSelect] = useState(false)
|
const [showWxidSelect, setShowWxidSelect] = useState(false)
|
||||||
const wxidSelectRef = useRef<HTMLDivElement>(null)
|
const wxidSelectRef = useRef<HTMLDivElement>(null)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
@@ -699,7 +704,17 @@ function WelcomePage({ standalone = false }: WelcomePageProps) {
|
|||||||
setShowWxidSelect(false)
|
setShowWxidSelect(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="wxid-name">{opt.wxid}</span>
|
<div className="wxid-profile">
|
||||||
|
{opt.avatarUrl ? (
|
||||||
|
<img src={opt.avatarUrl} alt="avatar" className="wxid-avatar" />
|
||||||
|
) : (
|
||||||
|
<div className="wxid-avatar-fallback"><UserRound size={14}/></div>
|
||||||
|
)}
|
||||||
|
<div className="wxid-info">
|
||||||
|
<span className="wxid-nickname">{opt.nickname || opt.wxid}</span>
|
||||||
|
{opt.nickname && <span className="wxid-sub">{opt.wxid}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<span className="wxid-time">{formatModifiedTime(opt.modifiedTime)}</span>
|
<span className="wxid-time">{formatModifiedTime(opt.modifiedTime)}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user