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

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
officials: boolean
}
selectedUsernames?: string[]
}
/**
@@ -40,6 +41,11 @@ class ContactExportService {
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) {
return { success: false, error: '没有符合条件的联系人' }
}

View File

@@ -174,6 +174,24 @@
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,
.empty-state {
flex: 1;
@@ -214,11 +232,30 @@
border-radius: 10px;
transition: all 0.2s;
margin-bottom: 4px;
cursor: pointer;
&: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 {
width: 44px;
height: 44px;
@@ -548,4 +585,4 @@
to {
transform: rotate(360deg);
}
}
}

View File

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