From d63f1e0d79c92ddfabc98305dcdbfe308ad4bef5 Mon Sep 17 00:00:00 2001 From: xuncha <1658671838@qq.com> Date: Tue, 27 Jan 2026 19:39:53 +0800 Subject: [PATCH] =?UTF-8?q?ui=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/ContactsPage.scss | 839 ++++++++++++++++++++---------------- src/pages/ContactsPage.tsx | 177 +++++--- 2 files changed, 586 insertions(+), 430 deletions(-) diff --git a/src/pages/ContactsPage.scss b/src/pages/ContactsPage.scss index 2bfb47c..685ef4a 100644 --- a/src/pages/ContactsPage.scss +++ b/src/pages/ContactsPage.scss @@ -1,39 +1,49 @@ .contacts-page { display: flex; - flex-direction: column; - height: 100%; - padding: 2rem; - gap: 1.5rem; + height: calc(100% + 48px); + margin: -24px; + background: var(--bg-primary); overflow: hidden; - .page-header { + // 左侧联系人面板 + .contacts-panel { + width: 380px; + min-width: 380px; + display: flex; + flex-direction: column; + border-right: 1px solid var(--border-color); + background: var(--card-bg); + } + + .panel-header { display: flex; align-items: center; justify-content: space-between; - gap: 1rem; + padding: 20px 24px; + border-bottom: 1px solid var(--border-color); - h1 { - font-size: 1.75rem; + h2 { + font-size: 18px; font-weight: 600; color: var(--text-primary); margin: 0; } .icon-btn { + width: 32px; + height: 32px; + border: none; + background: var(--bg-tertiary); + border-radius: 8px; + cursor: pointer; display: flex; align-items: center; justify-content: center; - width: 36px; - height: 36px; - border: none; - border-radius: 8px; - background: var(--bg-secondary); color: var(--text-secondary); - cursor: pointer; transition: all 0.2s; - &:hover:not(:disabled) { - background: var(--bg-tertiary); + &:hover { + background: var(--bg-hover); color: var(--text-primary); } @@ -42,423 +52,502 @@ cursor: not-allowed; } - svg.spin { - animation: spin 1s linear infinite; + .spin { + animation: contactsSpin 1s linear infinite; } } } - .contacts-filters { + .search-bar { display: flex; - flex-direction: column; - gap: 1rem; + align-items: center; + gap: 10px; + margin: 16px 20px; + padding: 10px 14px; + background: var(--bg-secondary); + border-radius: 10px; + border: 1px solid var(--border-color); + transition: border-color 0.2s; - .search-bar { - position: relative; + &:focus-within { + border-color: var(--primary); + } + + svg { + color: var(--text-tertiary); + flex-shrink: 0; + } + + input { + flex: 1; + border: none; + background: none; + outline: none; + font-size: 14px; + color: var(--text-primary); + + &::placeholder { + color: var(--text-tertiary); + } + } + + .clear-btn { + background: none; + border: none; + padding: 4px; + cursor: pointer; + color: var(--text-tertiary); display: flex; align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - background: var(--bg-secondary); - border: 1px solid var(--border-primary); - border-radius: 12px; - transition: all 0.2s; + justify-content: center; + border-radius: 4px; - &:focus-within { - border-color: var(--brand-primary); - box-shadow: 0 0 0 3px var(--brand-primary-alpha); + &:hover { + background: var(--bg-hover); + color: var(--text-primary); + } + } + } + + .type-filters { + display: flex; + gap: 16px; + padding: 0 20px 12px; + flex-wrap: wrap; + + .filter-checkbox { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + user-select: none; + font-size: 14px; + color: var(--text-primary); + + input[type="checkbox"] { + position: absolute; + opacity: 0; + pointer-events: none; + + &:checked+.custom-checkbox { + background: var(--primary); + border-color: var(--primary); + + &::after { + opacity: 1; + transform: scale(1); + } + } + } + + .custom-checkbox { + position: relative; + width: 18px; + height: 18px; + border: 2px solid var(--border-color); + border-radius: 4px; + background: var(--bg-primary); + transition: all 0.2s; + + &::after { + content: ''; + position: absolute; + left: 4px; + top: 1px; + width: 4px; + height: 8px; + border: solid white; + border-width: 0 2px 2px 0; + transform: rotate(45deg) scale(0); + opacity: 0; + transition: all 0.2s; + } } svg { - color: var(--text-tertiary); + color: var(--text-secondary); flex-shrink: 0; } - input { - flex: 1; - border: none; - background: transparent; - color: var(--text-primary); - font-size: 0.9375rem; - outline: none; - - &::placeholder { - color: var(--text-tertiary); - } + &:hover .custom-checkbox { + border-color: var(--primary); } - - .clear-btn { - display: flex; - align-items: center; - justify-content: center; - padding: 4px; - border: none; - background: transparent; - color: var(--text-tertiary); - cursor: pointer; - border-radius: 4px; - transition: all 0.2s; - - &:hover { - background: var(--bg-tertiary); - color: var(--text-secondary); - } - } - } - - .type-filters { - display: flex; - gap: 1.5rem; - flex-wrap: wrap; - - .filter-checkbox { - display: flex; - align-items: center; - gap: 0.5rem; - cursor: pointer; - user-select: none; - - input[type="checkbox"] { - position: absolute; - opacity: 0; - pointer-events: none; - - &:checked+.custom-checkbox { - background: var(--brand-primary); - border-color: var(--brand-primary); - - &::after { - opacity: 1; - transform: scale(1); - } - } - } - - .custom-checkbox { - position: relative; - width: 18px; - height: 18px; - border: 2px solid var(--border-secondary); - border-radius: 4px; - background: var(--bg-primary); - transition: all 0.2s; - - &::after { - content: ''; - position: absolute; - left: 4px; - top: 1px; - width: 4px; - height: 8px; - border: solid white; - border-width: 0 2px 2px 0; - transform: rotate(45deg) scale(0); - opacity: 0; - transition: all 0.2s; - } - } - - svg { - color: var(--text-secondary); - flex-shrink: 0; - } - - span { - color: var(--text-primary); - font-size: 0.9375rem; - } - - &:hover .custom-checkbox { - border-color: var(--brand-primary); - } - } - } - - .contacts-count { - font-size: 0.875rem; - color: var(--text-tertiary); } } - .export-section { - padding: 1.5rem; - background: var(--bg-secondary); - border: 1px solid var(--border-primary); - border-radius: 12px; + .contacts-count { + padding: 0 20px 12px; + font-size: 13px; + color: var(--text-secondary); + } + + .loading-state, + .empty-state { + flex: 1; display: flex; flex-direction: column; - gap: 1rem; + align-items: center; + justify-content: center; + gap: 12px; + color: var(--text-tertiary); + font-size: 14px; - h3 { - margin: 0; - font-size: 1rem; - font-weight: 600; - color: var(--text-primary); + .spin { + animation: contactsSpin 1s linear infinite; + } + } + + .contacts-list { + flex: 1; + overflow-y: auto; + padding: 0 12px 12px; + + &::-webkit-scrollbar { + width: 6px; } - .export-format { - display: flex; - align-items: center; - gap: 0.75rem; + &::-webkit-scrollbar-thumb { + background: var(--text-tertiary); + border-radius: 3px; + opacity: 0.3; + } + } - label { - font-size: 0.9375rem; - color: var(--text-secondary); - white-space: nowrap; - } + .contact-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + border-radius: 10px; + transition: all 0.2s; + margin-bottom: 4px; - select { - padding: 0.5rem 0.75rem; - border: 1px solid var(--border-primary); - border-radius: 8px; - background: var(--bg-primary); - color: var(--text-primary); - font-size: 0.9375rem; - cursor: pointer; - transition: all 0.2s; - - &:hover { - border-color: var(--brand-primary); - } - - &:focus { - outline: none; - border-color: var(--brand-primary); - box-shadow: 0 0 0 3px var(--brand-primary-alpha); - } - } + &:hover { + background: var(--bg-hover); } - .export-options { - display: flex; - gap: 1rem; - - label { - display: flex; - align-items: center; - gap: 0.5rem; - cursor: pointer; - - input[type="checkbox"] { - width: 16px; - height: 16px; - cursor: pointer; - } - - span { - font-size: 0.9375rem; - color: var(--text-primary); - } - } - } - - .export-folder { - .folder-btn { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem; - border: 1px solid var(--border-primary); - border-radius: 8px; - background: var(--bg-primary); - color: var(--text-secondary); - cursor: pointer; - transition: all 0.2s; - font-size: 0.9375rem; - - &:hover { - border-color: var(--brand-primary); - color: var(--text-primary); - } - - span { - flex: 1; - text-align: left; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - } - - .export-action-btn { + .contact-avatar { + width: 44px; + height: 44px; + border-radius: 10px; + background: linear-gradient(135deg, var(--primary), var(--primary-hover)); display: flex; align-items: center; justify-content: center; - gap: 0.5rem; - padding: 0.75rem 1.5rem; - border: none; - border-radius: 8px; - background: #3b82f6; - color: #ffffff; - font-size: 0.9375rem; + overflow: hidden; + flex-shrink: 0; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + + span { + color: #fff; + font-size: 16px; + font-weight: 600; + } + } + + .contact-info { + flex: 1; + min-width: 0; + } + + .contact-name { + font-size: 14px; font-weight: 500; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .contact-remark { + font-size: 12px; + color: var(--text-tertiary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 2px; + } + + .contact-type { + display: flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 6px; + font-size: 12px; + font-weight: 500; + flex-shrink: 0; + + &.friend { + background: rgba(var(--primary-rgb), 0.1); + color: var(--primary); + } + + &.group { + background: rgba(52, 211, 153, 0.1); + color: rgb(52, 211, 153); + } + + &.official { + background: rgba(251, 191, 36, 0.1); + color: rgb(251, 191, 36); + } + + svg { + flex-shrink: 0; + } + } + } + + // 右侧设置面板 + .settings-panel { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + } + + .settings-content { + flex: 1; + overflow-y: auto; + padding: 20px 24px; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: var(--text-tertiary); + border-radius: 3px; + } + } + + .setting-section { + margin-bottom: 28px; + + h3 { + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + margin: 0 0 14px; + } + } + + .format-select { + position: relative; + /* margin-bottom 移到 .setting-section */ + + .select-trigger { + width: 100%; + padding: 10px 16px; + border: 1px solid var(--border-color); + border-radius: 9999px; + /* Rounded pill shape */ + font-size: 14px; + background: var(--bg-primary); + color: var(--text-primary); + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; cursor: pointer; transition: all 0.2s; - &:hover:not(:disabled) { - background: #2563eb; + &:hover { + border-color: var(--text-tertiary); } - &:disabled { - opacity: 0.5; - cursor: not-allowed; - background: #9ca3af; + &.open { + border-color: var(--primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent); } } + + .select-value { + flex: 1; + min-width: 0; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .select-dropdown { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + background: color-mix(in srgb, var(--bg-primary) 85%, var(--bg-secondary)); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 6px; + box-shadow: var(--shadow-md); + z-index: 20; + max-height: 260px; + overflow-y: auto; + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + } + + .select-option { + width: 100%; + text-align: left; + display: flex; + flex-direction: column; + gap: 4px; + padding: 10px 12px; + border: none; + border-radius: 10px; + background: transparent; + cursor: pointer; + transition: all 0.15s; + color: var(--text-primary); + font-size: 14px; + + &:hover { + background: var(--bg-tertiary); + } + + &.active { + background: color-mix(in srgb, var(--primary) 12%, transparent); + color: var(--primary); + + .option-desc { + color: var(--primary); + } + } + } + + .option-label { + font-weight: 500; + } + + .option-desc { + font-size: 12px; + color: var(--text-tertiary); + } } - .contacts-content { - flex: 1; - overflow: hidden; + .checkbox-item { display: flex; - flex-direction: column; + align-items: center; + gap: 10px; + cursor: pointer; + font-size: 14px; + color: var(--text-primary); - .loading-state, - .empty-state { + input[type="checkbox"] { + width: 18px; + height: 18px; + accent-color: var(--primary); + cursor: pointer; + } + } + + .export-path-display { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + background: var(--bg-secondary); + border-radius: 10px; + font-size: 13px; + color: var(--text-primary); + margin-bottom: 12px; + + svg { + color: var(--primary); + flex-shrink: 0; + } + + span { flex: 1; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 1rem; - color: var(--text-tertiary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } - svg.spin { - animation: spin 1s linear infinite; - color: var(--brand-primary); + .select-folder-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 10px 16px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + font-size: 13px; + font-weight: 500; + color: var(--text-primary); + cursor: pointer; + transition: all 0.2s; + + &:hover { + background: var(--bg-hover); + border-color: var(--primary); + color: var(--primary); + + svg { + color: var(--primary); } } - .contacts-list { - flex: 1; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 0.5rem; - padding-right: 0.5rem; + &:active { + transform: scale(0.98); + } - &::-webkit-scrollbar { - width: 6px; - } + svg { + color: var(--text-secondary); + transition: color 0.2s; + } + } - &::-webkit-scrollbar-track { - background: transparent; - } + .export-action { + padding: 20px 24px; + border-top: 1px solid var(--border-color); + } - &::-webkit-scrollbar-thumb { - background: var(--border-secondary); - border-radius: 3px; + .export-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 14px 24px; + background: var(--primary); + color: #fff; + border: none; + border-radius: 12px; + font-size: 15px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; - &:hover { - background: var(--border-primary); - } - } + &:hover:not(:disabled) { + background: var(--primary-hover); + } - .contact-item { - display: flex; - align-items: center; - gap: 1rem; - padding: 1rem; - background: var(--bg-secondary); - border: 1px solid var(--border-primary); - border-radius: 12px; - transition: all 0.2s; + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } - &:hover { - background: var(--bg-tertiary); - border-color: var(--border-secondary); - } - - .contact-avatar { - width: 48px; - height: 48px; - border-radius: 50%; - overflow: hidden; - background: var(--bg-tertiary); - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - - img { - width: 100%; - height: 100%; - object-fit: cover; - } - - span { - font-size: 1.25rem; - font-weight: 600; - color: var(--text-secondary); - } - } - - .contact-info { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 0.25rem; - - .contact-name { - font-size: 1rem; - font-weight: 500; - color: var(--text-primary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - .contact-remark { - font-size: 0.875rem; - color: var(--text-tertiary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - - .contact-type { - display: flex; - align-items: center; - gap: 0.375rem; - padding: 0.375rem 0.75rem; - border-radius: 6px; - font-size: 0.8125rem; - font-weight: 500; - flex-shrink: 0; - - &.friend { - background: var(--brand-primary-alpha); - color: var(--brand-primary); - } - - &.group { - background: rgba(52, 211, 153, 0.1); - color: rgb(52, 211, 153); - } - - &.official { - background: rgba(251, 191, 36, 0.1); - color: rgb(251, 191, 36); - } - - &.other { - background: var(--bg-tertiary); - color: var(--text-tertiary); - } - - svg { - flex-shrink: 0; - } - } - } + .spin { + animation: contactsSpin 1s linear infinite; } } } -@keyframes spin { +@keyframes contactsSpin { from { transform: rotate(0deg); } diff --git a/src/pages/ContactsPage.tsx b/src/pages/ContactsPage.tsx index e60d1a9..1e8ce72 100644 --- a/src/pages/ContactsPage.tsx +++ b/src/pages/ContactsPage.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect, useCallback } from 'react' -import { Search, RefreshCw, X, User, Users, MessageSquare, Loader2, FolderOpen, Download } from 'lucide-react' +import { useState, useEffect, useCallback, useRef } from 'react' +import { Search, RefreshCw, X, User, Users, MessageSquare, Loader2, FolderOpen, Download, ChevronDown } from 'lucide-react' import './ContactsPage.scss' interface ContactInfo { @@ -27,6 +27,8 @@ function ContactsPage() { const [exportAvatars, setExportAvatars] = useState(true) const [exportFolder, setExportFolder] = useState('') const [isExporting, setIsExporting] = useState(false) + const [showFormatSelect, setShowFormatSelect] = useState(false) + const formatDropdownRef = useRef(null) // 加载通讯录 const loadContacts = useCallback(async () => { @@ -102,6 +104,18 @@ function ContactsPage() { setFilteredContacts(filtered) }, [searchKeyword, contacts, contactTypes]) + // 点击外部关闭下拉菜单 + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node + if (showFormatSelect && formatDropdownRef.current && !formatDropdownRef.current.contains(target)) { + setShowFormatSelect(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, [showFormatSelect]) + const getAvatarLetter = (name: string) => { if (!name) return '?' return [...name][0] || '?' @@ -173,16 +187,27 @@ function ContactsPage() { } } + const exportFormatOptions = [ + { value: 'json', label: 'JSON', desc: '详细格式,包含完整联系人信息' }, + { value: 'csv', label: 'CSV (Excel)', desc: '电子表格格式,适合Excel查看' }, + { value: 'vcf', label: 'VCF (vCard)', desc: '标准名片格式,支持导入手机' } + ] + + const getOptionLabel = (value: string) => { + return exportFormatOptions.find(opt => opt.value === value)?.label || value + } + return (
-
-

通讯录

- -
+ {/* 左侧:联系人列表 */} +
+
+

通讯录

+ +
-
setContactTypes(prev => ({ ...prev, friends: e.target.checked }))} + onChange={e => setContactTypes({ ...contactTypes, friends: e.target.checked })} />
@@ -213,7 +238,7 @@ function ContactsPage() { setContactTypes(prev => ({ ...prev, groups: e.target.checked }))} + onChange={e => setContactTypes({ ...contactTypes, groups: e.target.checked })} />
@@ -223,7 +248,7 @@ function ContactsPage() { setContactTypes(prev => ({ ...prev, officials: e.target.checked }))} + onChange={e => setContactTypes({ ...contactTypes, officials: e.target.checked })} />
@@ -234,50 +259,7 @@ function ContactsPage() {
共 {filteredContacts.length} 个联系人
-
- {/* 导出区域 */} -
-

导出通讯录

- -
- - -
- -
- -
- -
- -
- - -
- -
{isLoading ? (
@@ -313,6 +295,91 @@ function ContactsPage() {
)}
+ + {/* 右侧:导出设置 */} +
+
+

导出设置

+
+ +
+
+

导出格式

+
+ + {showFormatSelect && ( +
+ {exportFormatOptions.map(option => ( + + ))} +
+ )} +
+
+ +
+

导出选项

+ +
+ +
+

导出位置

+
+ + {exportFolder || '未设置'} +
+ +
+
+ +
+ +
+
) }