mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-22 15:39:41 +08:00
@@ -103,7 +103,7 @@ export interface ContactInfo {
|
||||
remark?: string
|
||||
nickname?: string
|
||||
avatarUrl?: string
|
||||
type: 'friend' | 'group' | 'official' | 'other'
|
||||
type: 'friend' | 'group' | 'official' | 'deleted_friend' | 'other'
|
||||
}
|
||||
|
||||
// 表情包缓存
|
||||
@@ -603,7 +603,7 @@ class ChatService {
|
||||
// 使用execQuery直接查询加密的contact.db
|
||||
// kind='contact', path=null表示使用已打开的contact.db
|
||||
const contactQuery = `
|
||||
SELECT username, remark, nick_name, alias, local_type
|
||||
SELECT username, remark, nick_name, alias, local_type, flag
|
||||
FROM contact
|
||||
`
|
||||
|
||||
@@ -663,28 +663,31 @@ class ChatService {
|
||||
}
|
||||
|
||||
// 判断类型 - 正确规则:wxid开头且有alias的是好友
|
||||
let type: 'friend' | 'group' | 'official' | 'other' = 'other'
|
||||
let type: 'friend' | 'group' | 'official' | 'deleted_friend' | 'other' = 'other'
|
||||
const localType = row.local_type || 0
|
||||
|
||||
const flag = Number(row.flag ?? 0)
|
||||
|
||||
if (username.includes('@chatroom')) {
|
||||
type = 'group'
|
||||
} else if (username.startsWith('gh_')) {
|
||||
if (flag === 0) continue
|
||||
type = 'official'
|
||||
} else if (localType === 3 || localType === 4) {
|
||||
if (flag === 0) continue
|
||||
if (flag === 4) continue
|
||||
type = 'official'
|
||||
} else if (username.startsWith('wxid_') && row.alias) {
|
||||
// wxid开头且有alias的是好友
|
||||
type = 'friend'
|
||||
type = flag === 0 ? 'deleted_friend' : 'friend'
|
||||
} else if (localType === 1) {
|
||||
// local_type=1 也是好友
|
||||
type = 'friend'
|
||||
type = flag === 0 ? 'deleted_friend' : 'friend'
|
||||
} else if (localType === 2) {
|
||||
// local_type=2 是群成员但非好友,跳过
|
||||
continue
|
||||
} else if (localType === 0) {
|
||||
// local_type=0 可能是好友或其他,检查是否有备注或昵称
|
||||
if (row.remark || row.nick_name) {
|
||||
type = 'friend'
|
||||
type = flag === 0 ? 'deleted_friend' : 'friend'
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1479,13 +1479,17 @@ class ExportService {
|
||||
result.localPath = thumbResult.localPath
|
||||
}
|
||||
|
||||
// 为每条消息生成稳定且唯一的文件名前缀,避免跨日期/消息发生同名覆盖
|
||||
const messageId = String(msg.localId || Date.now())
|
||||
const imageKey = (imageMd5 || imageDatName || 'image').replace(/[^a-zA-Z0-9_-]/g, '')
|
||||
|
||||
// 从 data URL 或 file URL 获取实际路径
|
||||
let sourcePath = result.localPath
|
||||
if (sourcePath.startsWith('data:')) {
|
||||
// 是 data URL,需要保存为文件
|
||||
const base64Data = sourcePath.split(',')[1]
|
||||
const ext = this.getExtFromDataUrl(sourcePath)
|
||||
const fileName = `${imageMd5 || imageDatName || msg.localId}${ext}`
|
||||
const fileName = `${messageId}_${imageKey}${ext}`
|
||||
const destPath = path.join(imagesDir, fileName)
|
||||
|
||||
fs.writeFileSync(destPath, Buffer.from(base64Data, 'base64'))
|
||||
@@ -1501,7 +1505,7 @@ class ExportService {
|
||||
// 复制文件
|
||||
if (!fs.existsSync(sourcePath)) return null
|
||||
const ext = path.extname(sourcePath) || '.jpg'
|
||||
const fileName = `${imageMd5 || imageDatName || msg.localId}${ext}`
|
||||
const fileName = `${messageId}_${imageKey}${ext}`
|
||||
const destPath = path.join(imagesDir, fileName)
|
||||
|
||||
if (!fs.existsSync(destPath)) {
|
||||
@@ -4769,4 +4773,3 @@ class ExportService {
|
||||
}
|
||||
|
||||
export const exportService = new ExportService()
|
||||
|
||||
|
||||
@@ -6,6 +6,13 @@
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--border-light);
|
||||
|
||||
// 浅色模式下使用不透明背景,避免透明窗口中通知过于透明
|
||||
[data-mode="light"] &,
|
||||
:not([data-mode]) & {
|
||||
background: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||
padding: 12px;
|
||||
@@ -39,7 +46,7 @@
|
||||
backdrop-filter: none !important;
|
||||
-webkit-backdrop-filter: none !important;
|
||||
|
||||
// Ensure background is solid
|
||||
// 确保背景不透明
|
||||
background: var(--bg-secondary, #2c2c2c);
|
||||
color: var(--text-primary, #ffffff);
|
||||
|
||||
|
||||
@@ -1288,6 +1288,21 @@
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.empty-chat-inline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 60px 0;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 14px;
|
||||
|
||||
svg {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.message-list * {
|
||||
-webkit-app-region: no-drag !important;
|
||||
}
|
||||
|
||||
@@ -1261,6 +1261,7 @@ function ChatPage(_props: ChatPageProps) {
|
||||
|
||||
useEffect(() => {
|
||||
if (currentSessionId && messages.length === 0 && !isLoadingMessages && !isLoadingMore) {
|
||||
setHasInitialMessages(false)
|
||||
loadMessages(currentSessionId, 0)
|
||||
}
|
||||
}, [currentSessionId, messages.length, isLoadingMessages, isLoadingMore])
|
||||
@@ -1327,8 +1328,21 @@ function ChatPage(_props: ChatPageProps) {
|
||||
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
|
||||
}, [])
|
||||
|
||||
// 获取当前会话信息
|
||||
const currentSession = Array.isArray(sessions) ? sessions.find(s => s.username === currentSessionId) : undefined
|
||||
// 获取当前会话信息(从通讯录跳转时可能不在 sessions 列表中,构造 fallback)
|
||||
const currentSession = (() => {
|
||||
const found = Array.isArray(sessions) ? sessions.find(s => s.username === currentSessionId) : undefined
|
||||
if (found || !currentSessionId) return found
|
||||
return {
|
||||
username: currentSessionId,
|
||||
type: 0,
|
||||
unreadCount: 0,
|
||||
summary: '',
|
||||
sortTimestamp: 0,
|
||||
lastTimestamp: 0,
|
||||
lastMsgType: 0,
|
||||
displayName: currentSessionId,
|
||||
} as ChatSession
|
||||
})()
|
||||
|
||||
// 判断是否为群聊
|
||||
const isGroupChat = (username: string) => username.includes('@chatroom')
|
||||
@@ -2048,6 +2062,13 @@ function ChatPage(_props: ChatPageProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoadingMessages && messages.length === 0 && !hasMoreMessages && (
|
||||
<div className="empty-chat-inline">
|
||||
<MessageSquare size={32} />
|
||||
<span>该联系人没有聊天记录</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messages.map((msg, index) => {
|
||||
const prevMsg = index > 0 ? messages[index - 1] : undefined
|
||||
const showDateDivider = shouldShowDateDivider(msg, prevMsg)
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
// 左侧联系人面板
|
||||
.contacts-panel {
|
||||
width: 380px;
|
||||
min-width: 380px;
|
||||
width: 400px;
|
||||
min-width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid var(--border-color);
|
||||
@@ -55,6 +55,11 @@
|
||||
.spin {
|
||||
animation: contactsSpin 1s linear infinite;
|
||||
}
|
||||
|
||||
&.export-mode-btn.active {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,8 +236,8 @@
|
||||
padding: 12px;
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s;
|
||||
margin-bottom: 4px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
@@ -242,6 +247,10 @@
|
||||
background: color-mix(in srgb, var(--primary) 12%, transparent);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.contact-select {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -334,6 +343,93 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 右侧详情面板内的联系人资料
|
||||
.detail-profile {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin-bottom: 20px;
|
||||
|
||||
.detail-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(135deg, var(--primary), var(--primary-hover));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
img { width: 100%; height: 100%; object-fit: cover; }
|
||||
span { color: #fff; font-size: 28px; font-weight: 600; }
|
||||
}
|
||||
|
||||
.detail-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-info-list {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
font-size: 13px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: var(--text-tertiary);
|
||||
min-width: 48px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: var(--text-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.goto-chat-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover { background: var(--primary-hover); }
|
||||
}
|
||||
|
||||
.empty-detail {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
// 右侧设置面板
|
||||
.settings-panel {
|
||||
flex: 1;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { Search, RefreshCw, X, User, Users, MessageSquare, Loader2, FolderOpen, Download, ChevronDown } from 'lucide-react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Search, RefreshCw, X, User, Users, MessageSquare, Loader2, FolderOpen, Download, ChevronDown, MessageCircle, UserX } from 'lucide-react'
|
||||
import { useChatStore } from '../stores/chatStore'
|
||||
import './ContactsPage.scss'
|
||||
|
||||
interface ContactInfo {
|
||||
@@ -8,7 +10,7 @@ interface ContactInfo {
|
||||
remark?: string
|
||||
nickname?: string
|
||||
avatarUrl?: string
|
||||
type: 'friend' | 'group' | 'official' | 'other'
|
||||
type: 'friend' | 'group' | 'official' | 'deleted_friend' | 'other'
|
||||
}
|
||||
|
||||
function ContactsPage() {
|
||||
@@ -20,9 +22,16 @@ function ContactsPage() {
|
||||
const [contactTypes, setContactTypes] = useState({
|
||||
friends: true,
|
||||
groups: true,
|
||||
officials: true
|
||||
officials: true,
|
||||
deletedFriends: false
|
||||
})
|
||||
|
||||
// 导出模式与查看详情
|
||||
const [exportMode, setExportMode] = useState(false)
|
||||
const [selectedContact, setSelectedContact] = useState<ContactInfo | null>(null)
|
||||
const navigate = useNavigate()
|
||||
const { setCurrentSession } = useChatStore()
|
||||
|
||||
// 导出相关状态
|
||||
const [exportFormat, setExportFormat] = useState<'json' | 'csv' | 'vcf'>('json')
|
||||
const [exportAvatars, setExportAvatars] = useState(true)
|
||||
@@ -85,6 +94,7 @@ function ContactsPage() {
|
||||
if (c.type === 'friend' && !contactTypes.friends) return false
|
||||
if (c.type === 'group' && !contactTypes.groups) return false
|
||||
if (c.type === 'official' && !contactTypes.officials) return false
|
||||
if (c.type === 'deleted_friend' && !contactTypes.deletedFriends) return false
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -154,6 +164,7 @@ function ContactsPage() {
|
||||
case 'friend': return <User size={14} />
|
||||
case 'group': return <Users size={14} />
|
||||
case 'official': return <MessageSquare size={14} />
|
||||
case 'deleted_friend': return <UserX size={14} />
|
||||
default: return <User size={14} />
|
||||
}
|
||||
}
|
||||
@@ -163,6 +174,7 @@ function ContactsPage() {
|
||||
case 'friend': return '好友'
|
||||
case 'group': return '群聊'
|
||||
case 'official': return '公众号'
|
||||
case 'deleted_friend': return '已删除'
|
||||
default: return '其他'
|
||||
}
|
||||
}
|
||||
@@ -236,9 +248,18 @@ function ContactsPage() {
|
||||
<div className="contacts-panel">
|
||||
<div className="panel-header">
|
||||
<h2>通讯录</h2>
|
||||
<button className="icon-btn" onClick={loadContacts} disabled={isLoading}>
|
||||
<RefreshCw size={18} className={isLoading ? 'spin' : ''} />
|
||||
</button>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<button
|
||||
className={`icon-btn export-mode-btn ${exportMode ? 'active' : ''}`}
|
||||
onClick={() => { setExportMode(!exportMode); setSelectedContact(null) }}
|
||||
title={exportMode ? '退出导出模式' : '进入导出模式'}
|
||||
>
|
||||
<Download size={18} />
|
||||
</button>
|
||||
<button className="icon-btn" onClick={loadContacts} disabled={isLoading}>
|
||||
<RefreshCw size={18} className={isLoading ? 'spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="search-bar">
|
||||
@@ -258,49 +279,41 @@ function ContactsPage() {
|
||||
|
||||
<div className="type-filters">
|
||||
<label className={`filter-chip ${contactTypes.friends ? 'active' : ''}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={contactTypes.friends}
|
||||
onChange={e => setContactTypes({ ...contactTypes, friends: e.target.checked })}
|
||||
/>
|
||||
<User size={16} />
|
||||
<span>好友</span>
|
||||
<input type="checkbox" checked={contactTypes.friends} onChange={e => setContactTypes({ ...contactTypes, friends: e.target.checked })} />
|
||||
<User size={16} /><span>好友</span>
|
||||
</label>
|
||||
<label className={`filter-chip ${contactTypes.groups ? 'active' : ''}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={contactTypes.groups}
|
||||
onChange={e => setContactTypes({ ...contactTypes, groups: e.target.checked })}
|
||||
/>
|
||||
<Users size={16} />
|
||||
<span>群聊</span>
|
||||
<input type="checkbox" checked={contactTypes.groups} onChange={e => setContactTypes({ ...contactTypes, groups: e.target.checked })} />
|
||||
<Users size={16} /><span>群聊</span>
|
||||
</label>
|
||||
<label className={`filter-chip ${contactTypes.officials ? 'active' : ''}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={contactTypes.officials}
|
||||
onChange={e => setContactTypes({ ...contactTypes, officials: e.target.checked })}
|
||||
/>
|
||||
<MessageSquare size={16} />
|
||||
<span>公众号</span>
|
||||
<input type="checkbox" checked={contactTypes.officials} onChange={e => setContactTypes({ ...contactTypes, officials: e.target.checked })} />
|
||||
<MessageSquare size={16} /><span>公众号</span>
|
||||
</label>
|
||||
<label className={`filter-chip ${contactTypes.deletedFriends ? 'active' : ''}`}>
|
||||
<input type="checkbox" checked={contactTypes.deletedFriends} onChange={e => setContactTypes({ ...contactTypes, deletedFriends: e.target.checked })} />
|
||||
<UserX size={16} /><span>已删除</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="contacts-count">
|
||||
共 {filteredContacts.length} 个联系人
|
||||
</div>
|
||||
<div className="selection-toolbar">
|
||||
<label className="checkbox-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allFilteredSelected}
|
||||
onChange={e => toggleAllFilteredSelected(e.target.checked)}
|
||||
disabled={filteredContacts.length === 0}
|
||||
/>
|
||||
<span>全选当前筛选结果</span>
|
||||
</label>
|
||||
<span className="selection-count">已选 {selectedUsernames.size}(当前筛选 {selectedInFilteredCount} / {filteredContacts.length})</span>
|
||||
</div>
|
||||
|
||||
{exportMode && (
|
||||
<div className="selection-toolbar">
|
||||
<label className="checkbox-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allFilteredSelected}
|
||||
onChange={e => toggleAllFilteredSelected(e.target.checked)}
|
||||
disabled={filteredContacts.length === 0}
|
||||
/>
|
||||
<span>全选当前筛选结果</span>
|
||||
</label>
|
||||
<span className="selection-count">已选 {selectedUsernames.size}(当前筛选 {selectedInFilteredCount} / {filteredContacts.length})</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="loading-state">
|
||||
@@ -314,20 +327,29 @@ function ContactsPage() {
|
||||
) : (
|
||||
<div className="contacts-list">
|
||||
{filteredContacts.map(contact => {
|
||||
const isSelected = selectedUsernames.has(contact.username)
|
||||
const isChecked = selectedUsernames.has(contact.username)
|
||||
const isActive = !exportMode && selectedContact?.username === contact.username
|
||||
return (
|
||||
<div
|
||||
key={contact.username}
|
||||
className={`contact-item ${isSelected ? 'selected' : ''}`}
|
||||
onClick={() => toggleContactSelected(contact.username, !isSelected)}
|
||||
className={`contact-item ${exportMode && isChecked ? 'selected' : ''} ${isActive ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
if (exportMode) {
|
||||
toggleContactSelected(contact.username, !isChecked)
|
||||
} else {
|
||||
setSelectedContact(isActive ? null : contact)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<label className="contact-select" onClick={e => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={e => toggleContactSelected(contact.username, e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
{exportMode && (
|
||||
<label className="contact-select" onClick={e => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
onChange={e => toggleContactSelected(contact.username, e.target.checked)}
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
<div className="contact-avatar">
|
||||
{contact.avatarUrl ? (
|
||||
<img src={contact.avatarUrl} alt="" />
|
||||
@@ -352,90 +374,129 @@ function ContactsPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧:导出设置 */}
|
||||
<div className="settings-panel">
|
||||
<div className="panel-header">
|
||||
<h2>导出设置</h2>
|
||||
</div>
|
||||
{/* 右侧面板 */}
|
||||
{exportMode ? (
|
||||
<div className="settings-panel">
|
||||
<div className="panel-header">
|
||||
<h2>导出设置</h2>
|
||||
</div>
|
||||
|
||||
<div className="settings-content">
|
||||
<div className="setting-section">
|
||||
<h3>导出格式</h3>
|
||||
<div className="format-select" ref={formatDropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`select-trigger ${showFormatSelect ? 'open' : ''}`}
|
||||
onClick={() => setShowFormatSelect(!showFormatSelect)}
|
||||
>
|
||||
<span className="select-value">{getOptionLabel(exportFormat)}</span>
|
||||
<ChevronDown size={16} />
|
||||
<div className="settings-content">
|
||||
<div className="setting-section">
|
||||
<h3>导出格式</h3>
|
||||
<div className="format-select" ref={formatDropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`select-trigger ${showFormatSelect ? 'open' : ''}`}
|
||||
onClick={() => setShowFormatSelect(!showFormatSelect)}
|
||||
>
|
||||
<span className="select-value">{getOptionLabel(exportFormat)}</span>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
{showFormatSelect && (
|
||||
<div className="select-dropdown">
|
||||
{exportFormatOptions.map(option => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={`select-option ${exportFormat === option.value ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setExportFormat(option.value as 'json' | 'csv' | 'vcf')
|
||||
setShowFormatSelect(false)
|
||||
}}
|
||||
>
|
||||
<span className="option-label">{option.label}</span>
|
||||
<span className="option-desc">{option.desc}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="setting-section">
|
||||
<h3>导出选项</h3>
|
||||
<label className="checkbox-item">
|
||||
<input type="checkbox" checked={exportAvatars} onChange={e => setExportAvatars(e.target.checked)} />
|
||||
<span>导出头像</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="setting-section">
|
||||
<h3>导出位置</h3>
|
||||
<div className="export-path-display">
|
||||
<FolderOpen size={16} />
|
||||
<span>{exportFolder || '未设置'}</span>
|
||||
</div>
|
||||
<button className="select-folder-btn" onClick={selectExportFolder}>
|
||||
<FolderOpen size={16} />
|
||||
<span>选择导出目录</span>
|
||||
</button>
|
||||
{showFormatSelect && (
|
||||
<div className="select-dropdown">
|
||||
{exportFormatOptions.map(option => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
className={`select-option ${exportFormat === option.value ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
setExportFormat(option.value as 'json' | 'csv' | 'vcf')
|
||||
setShowFormatSelect(false)
|
||||
}}
|
||||
>
|
||||
<span className="option-label">{option.label}</span>
|
||||
<span className="option-desc">{option.desc}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="export-action">
|
||||
<button
|
||||
className="export-btn"
|
||||
onClick={startExport}
|
||||
disabled={!exportFolder || isExporting || selectedUsernames.size === 0}
|
||||
>
|
||||
{isExporting ? (
|
||||
<><Loader2 size={18} className="spin" /><span>导出中...</span></>
|
||||
) : (
|
||||
<><Download size={18} /><span>开始导出</span></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="setting-section">
|
||||
<h3>导出选项</h3>
|
||||
<label className="checkbox-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={exportAvatars}
|
||||
onChange={e => setExportAvatars(e.target.checked)}
|
||||
/>
|
||||
<span>导出头像</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="setting-section">
|
||||
<h3>导出位置</h3>
|
||||
<div className="export-path-display">
|
||||
<FolderOpen size={16} />
|
||||
<span>{exportFolder || '未设置'}</span>
|
||||
</div>
|
||||
<button className="select-folder-btn" onClick={selectExportFolder}>
|
||||
<FolderOpen size={16} />
|
||||
<span>选择导出目录</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : selectedContact ? (
|
||||
<div className="settings-panel">
|
||||
<div className="panel-header">
|
||||
<h2>联系人详情</h2>
|
||||
</div>
|
||||
<div className="settings-content">
|
||||
<div className="detail-profile">
|
||||
<div className="detail-avatar">
|
||||
{selectedContact.avatarUrl ? (
|
||||
<img src={selectedContact.avatarUrl} alt="" />
|
||||
) : (
|
||||
<span>{getAvatarLetter(selectedContact.displayName)}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="detail-name">{selectedContact.displayName}</div>
|
||||
<div className={`contact-type ${selectedContact.type}`}>
|
||||
{getContactTypeIcon(selectedContact.type)}
|
||||
<span>{getContactTypeName(selectedContact.type)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="export-action">
|
||||
<button
|
||||
className="export-btn"
|
||||
onClick={startExport}
|
||||
disabled={!exportFolder || isExporting || selectedUsernames.size === 0}
|
||||
>
|
||||
{isExporting ? (
|
||||
<>
|
||||
<Loader2 size={18} className="spin" />
|
||||
<span>导出中...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download size={18} />
|
||||
<span>开始导出</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<div className="detail-info-list">
|
||||
<div className="detail-row"><span className="detail-label">用户名</span><span className="detail-value">{selectedContact.username}</span></div>
|
||||
<div className="detail-row"><span className="detail-label">昵称</span><span className="detail-value">{selectedContact.nickname || selectedContact.displayName}</span></div>
|
||||
{selectedContact.remark && <div className="detail-row"><span className="detail-label">备注</span><span className="detail-value">{selectedContact.remark}</span></div>}
|
||||
<div className="detail-row"><span className="detail-label">类型</span><span className="detail-value">{getContactTypeName(selectedContact.type)}</span></div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="goto-chat-btn"
|
||||
onClick={() => {
|
||||
setCurrentSession(selectedContact.username)
|
||||
navigate('/chat')
|
||||
}}
|
||||
>
|
||||
<MessageCircle size={18} />
|
||||
<span>查看聊天记录</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="settings-panel">
|
||||
<div className="empty-detail">
|
||||
<User size={48} />
|
||||
<span>点击左侧联系人查看详情</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { NotificationToast, type NotificationData } from '../components/NotificationToast'
|
||||
import { useThemeStore } from '../stores/themeStore'
|
||||
import '../components/NotificationToast.scss'
|
||||
import './NotificationWindow.scss'
|
||||
|
||||
export default function NotificationWindow() {
|
||||
const { currentTheme, themeMode } = useThemeStore()
|
||||
const [notification, setNotification] = useState<NotificationData | null>(null)
|
||||
const [prevNotification, setPrevNotification] = useState<NotificationData | null>(null)
|
||||
|
||||
@@ -17,6 +19,12 @@ export default function NotificationWindow() {
|
||||
|
||||
const notificationRef = useRef<NotificationData | null>(null)
|
||||
|
||||
// 应用主题到通知窗口
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-theme', currentTheme)
|
||||
document.documentElement.setAttribute('data-mode', themeMode)
|
||||
}, [currentTheme, themeMode])
|
||||
|
||||
useEffect(() => {
|
||||
notificationRef.current = notification
|
||||
}, [notification])
|
||||
|
||||
Reference in New Issue
Block a user