通讯录可勾选部分好友导出

This commit is contained in:
xuncha
2026-02-19 16:49:46 +08:00
parent 89783b4d45
commit 25325e80ee
3 changed files with 130 additions and 23 deletions

View File

@@ -10,6 +10,7 @@ interface ContactExportOptions {
groups: boolean groups: boolean
officials: boolean officials: boolean
} }
selectedUsernames?: string[]
} }
/** /**
@@ -40,6 +41,11 @@ class ContactExportService {
return true return true
}) })
if (Array.isArray(options.selectedUsernames) && options.selectedUsernames.length > 0) {
const selectedSet = new Set(options.selectedUsernames)
contacts = contacts.filter(c => selectedSet.has(c.username))
}
if (contacts.length === 0) { if (contacts.length === 0) {
return { success: false, error: '没有符合条件的联系人' } return { success: false, error: '没有符合条件的联系人' }
} }

View File

@@ -174,6 +174,24 @@
color: var(--text-secondary); color: var(--text-secondary);
} }
.selection-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 0 20px 12px;
.checkbox-item {
font-size: 13px;
color: var(--text-secondary);
}
.selection-count {
font-size: 12px;
color: var(--text-tertiary);
}
}
.loading-state, .loading-state,
.empty-state { .empty-state {
flex: 1; flex: 1;
@@ -214,11 +232,30 @@
border-radius: 10px; border-radius: 10px;
transition: all 0.2s; transition: all 0.2s;
margin-bottom: 4px; margin-bottom: 4px;
cursor: pointer;
&:hover { &:hover {
background: var(--bg-hover); background: var(--bg-hover);
} }
&.selected {
background: color-mix(in srgb, var(--primary) 12%, transparent);
}
.contact-select {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
accent-color: var(--primary);
}
}
.contact-avatar { .contact-avatar {
width: 44px; width: 44px;
height: 44px; height: 44px;

View File

@@ -14,6 +14,7 @@ interface ContactInfo {
function ContactsPage() { function ContactsPage() {
const [contacts, setContacts] = useState<ContactInfo[]>([]) const [contacts, setContacts] = useState<ContactInfo[]>([])
const [filteredContacts, setFilteredContacts] = useState<ContactInfo[]>([]) const [filteredContacts, setFilteredContacts] = useState<ContactInfo[]>([])
const [selectedUsernames, setSelectedUsernames] = useState<Set<string>>(new Set())
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [searchKeyword, setSearchKeyword] = useState('') const [searchKeyword, setSearchKeyword] = useState('')
const [contactTypes, setContactTypes] = useState({ const [contactTypes, setContactTypes] = useState({
@@ -62,6 +63,7 @@ function ContactsPage() {
setContacts(contactsResult.contacts) setContacts(contactsResult.contacts)
setFilteredContacts(contactsResult.contacts) setFilteredContacts(contactsResult.contacts)
setSelectedUsernames(new Set())
} }
} catch (e) { } catch (e) {
console.error('加载通讯录失败:', e) console.error('加载通讯录失败:', e)
@@ -111,6 +113,37 @@ function ContactsPage() {
return () => document.removeEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside)
}, [showFormatSelect]) }, [showFormatSelect])
const selectedInFilteredCount = filteredContacts.reduce((count, contact) => {
return selectedUsernames.has(contact.username) ? count + 1 : count
}, 0)
const allFilteredSelected = filteredContacts.length > 0 && selectedInFilteredCount === filteredContacts.length
const toggleContactSelected = (username: string, checked: boolean) => {
setSelectedUsernames(prev => {
const next = new Set(prev)
if (checked) {
next.add(username)
} else {
next.delete(username)
}
return next
})
}
const toggleAllFilteredSelected = (checked: boolean) => {
setSelectedUsernames(prev => {
const next = new Set(prev)
filteredContacts.forEach(contact => {
if (checked) {
next.add(contact.username)
} else {
next.delete(contact.username)
}
})
return next
})
}
const getAvatarLetter = (name: string) => { const getAvatarLetter = (name: string) => {
if (!name) return '?' if (!name) return '?'
return [...name][0] || '?' return [...name][0] || '?'
@@ -154,6 +187,10 @@ function ContactsPage() {
alert('请先选择导出位置') alert('请先选择导出位置')
return return
} }
if (selectedUsernames.size === 0) {
alert('请至少选择一个联系人')
return
}
setIsExporting(true) setIsExporting(true)
try { try {
@@ -164,7 +201,8 @@ function ContactsPage() {
friends: contactTypes.friends, friends: contactTypes.friends,
groups: contactTypes.groups, groups: contactTypes.groups,
officials: contactTypes.officials officials: contactTypes.officials
} },
selectedUsernames: Array.from(selectedUsernames)
} }
const result = await window.electronAPI.export.exportContacts(exportFolder, exportOptions) const result = await window.electronAPI.export.exportContacts(exportFolder, exportOptions)
@@ -251,6 +289,18 @@ function ContactsPage() {
<div className="contacts-count"> <div className="contacts-count">
{filteredContacts.length} {filteredContacts.length}
</div> </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>
{isLoading ? ( {isLoading ? (
<div className="loading-state"> <div className="loading-state">
@@ -263,8 +313,21 @@ function ContactsPage() {
</div> </div>
) : ( ) : (
<div className="contacts-list"> <div className="contacts-list">
{filteredContacts.map(contact => ( {filteredContacts.map(contact => {
<div key={contact.username} className="contact-item"> const isSelected = selectedUsernames.has(contact.username)
return (
<div
key={contact.username}
className={`contact-item ${isSelected ? 'selected' : ''}`}
onClick={() => toggleContactSelected(contact.username, !isSelected)}
>
<label className="contact-select" onClick={e => e.stopPropagation()}>
<input
type="checkbox"
checked={isSelected}
onChange={e => toggleContactSelected(contact.username, e.target.checked)}
/>
</label>
<div className="contact-avatar"> <div className="contact-avatar">
{contact.avatarUrl ? ( {contact.avatarUrl ? (
<img src={contact.avatarUrl} alt="" /> <img src={contact.avatarUrl} alt="" />
@@ -283,7 +346,8 @@ function ContactsPage() {
<span>{getContactTypeName(contact.type)}</span> <span>{getContactTypeName(contact.type)}</span>
</div> </div>
</div> </div>
))} )
})}
</div> </div>
)} )}
</div> </div>
@@ -356,7 +420,7 @@ function ContactsPage() {
<button <button
className="export-btn" className="export-btn"
onClick={startExport} onClick={startExport}
disabled={!exportFolder || isExporting} disabled={!exportFolder || isExporting || selectedUsernames.size === 0}
> >
{isExporting ? ( {isExporting ? (
<> <>