mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
Merge pull request #404 from aits2026/codex/ts0306-01-export-opt
feat: 优化聊天分析、设置弹窗与导出会话交互
This commit is contained in:
136
src/components/ChatAnalysisHeader.scss
Normal file
136
src/components/ChatAnalysisHeader.scss
Normal file
@@ -0,0 +1,136 @@
|
||||
.chat-analysis-header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-height: 28px;
|
||||
padding: 4px 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-analysis-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-analysis-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
|
||||
.chat-analysis-breadcrumb-separator {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-analysis-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-analysis-current-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
.current {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
svg {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.open svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-analysis-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 10px);
|
||||
right: 0;
|
||||
min-width: 120px;
|
||||
padding: 6px;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.chat-analysis-menu-item {
|
||||
width: 100%;
|
||||
display: block;
|
||||
padding: 9px 12px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-analysis-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-left: auto;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.chat-analysis-header {
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.chat-analysis-breadcrumb {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 4px;
|
||||
}
|
||||
|
||||
.chat-analysis-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
105
src/components/ChatAnalysisHeader.tsx
Normal file
105
src/components/ChatAnalysisHeader.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { ChevronDown, ChevronLeft } from 'lucide-react'
|
||||
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import './ChatAnalysisHeader.scss'
|
||||
|
||||
export type ChatAnalysisMode = 'private' | 'group'
|
||||
|
||||
interface ChatAnalysisHeaderProps {
|
||||
currentMode: ChatAnalysisMode
|
||||
actions?: ReactNode
|
||||
}
|
||||
|
||||
const MODE_CONFIG: Record<ChatAnalysisMode, { label: string; path: string }> = {
|
||||
private: {
|
||||
label: '私聊分析',
|
||||
path: '/analytics/private'
|
||||
},
|
||||
group: {
|
||||
label: '群聊分析',
|
||||
path: '/analytics/group'
|
||||
}
|
||||
}
|
||||
|
||||
function ChatAnalysisHeader({ currentMode, actions }: ChatAnalysisHeaderProps) {
|
||||
const navigate = useNavigate()
|
||||
const currentLabel = MODE_CONFIG[currentMode].label
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null)
|
||||
const alternateMode = useMemo(
|
||||
() => (currentMode === 'private' ? 'group' : 'private'),
|
||||
[currentMode]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuOpen) return
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (!dropdownRef.current?.contains(event.target as Node)) {
|
||||
setMenuOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setMenuOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
}
|
||||
}, [menuOpen])
|
||||
|
||||
return (
|
||||
<div className="chat-analysis-header">
|
||||
<div className="chat-analysis-breadcrumb">
|
||||
<button
|
||||
type="button"
|
||||
className="chat-analysis-back"
|
||||
onClick={() => navigate('/analytics')}
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
<span>聊天分析</span>
|
||||
</button>
|
||||
<span className="chat-analysis-breadcrumb-separator">/</span>
|
||||
<div className="chat-analysis-dropdown" ref={dropdownRef}>
|
||||
<button
|
||||
type="button"
|
||||
className={`chat-analysis-current-trigger ${menuOpen ? 'open' : ''}`}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={menuOpen}
|
||||
onClick={() => setMenuOpen((prev) => !prev)}
|
||||
>
|
||||
<span className="current">{currentLabel}</span>
|
||||
<ChevronDown size={14} />
|
||||
</button>
|
||||
|
||||
{menuOpen && (
|
||||
<div className="chat-analysis-menu" role="menu" aria-label="切换聊天分析类型">
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className="chat-analysis-menu-item"
|
||||
onClick={() => {
|
||||
setMenuOpen(false)
|
||||
navigate(MODE_CONFIG[alternateMode].path)
|
||||
}}
|
||||
>
|
||||
{MODE_CONFIG[alternateMode].label}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{actions ? <div className="chat-analysis-actions">{actions}</div> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatAnalysisHeader
|
||||
@@ -43,31 +43,52 @@
|
||||
.sidebar-user-card-wrap {
|
||||
position: relative;
|
||||
margin: 0 12px 10px;
|
||||
--sidebar-user-menu-width: 172px;
|
||||
}
|
||||
|
||||
.sidebar-user-clear-trigger {
|
||||
.sidebar-user-menu {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
right: auto;
|
||||
bottom: calc(100% + 8px);
|
||||
width: max(100%, var(--sidebar-user-menu-width));
|
||||
z-index: 12;
|
||||
border: 1px solid rgba(255, 59, 48, 0.28);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
background: var(--bg-secondary-solid, var(--bg-primary));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12);
|
||||
}
|
||||
|
||||
.sidebar-user-menu-item {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: var(--bg-secondary);
|
||||
color: #d93025;
|
||||
padding: 8px 10px;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
padding: 9px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12);
|
||||
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||
text-align: left;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 59, 48, 0.08);
|
||||
border-color: rgba(255, 59, 48, 0.46);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: #d93025;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 59, 48, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,26 +265,6 @@
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-tertiary);
|
||||
cursor: pointer;
|
||||
border-radius: 9999px;
|
||||
transition: all 0.2s ease;
|
||||
margin-top: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-clear-dialog-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { NavLink, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { Home, MessageSquare, BarChart3, Users, FileText, Settings, ChevronLeft, ChevronRight, Download, Aperture, UserCircle, Lock, LockOpen, ChevronUp, Trash2 } from 'lucide-react'
|
||||
import { Home, MessageSquare, BarChart3, FileText, Settings, Download, Aperture, UserCircle, Lock, LockOpen, ChevronUp, Trash2 } from 'lucide-react'
|
||||
import { useAppStore } from '../stores/appStore'
|
||||
import * as configService from '../services/config'
|
||||
import { onExportSessionStatus, requestExportSessionStatus } from '../services/exportBridge'
|
||||
@@ -62,10 +62,13 @@ const normalizeAccountId = (value?: string | null): string => {
|
||||
return suffixMatch ? suffixMatch[1] : trimmed
|
||||
}
|
||||
|
||||
function Sidebar() {
|
||||
interface SidebarProps {
|
||||
collapsed: boolean
|
||||
}
|
||||
|
||||
function Sidebar({ collapsed }: SidebarProps) {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const [authEnabled, setAuthEnabled] = useState(false)
|
||||
const [activeExportTaskCount, setActiveExportTaskCount] = useState(0)
|
||||
const [userProfile, setUserProfile] = useState<SidebarUserProfile>({
|
||||
@@ -279,6 +282,15 @@ function Sidebar() {
|
||||
setShowClearAccountDialog(true)
|
||||
}
|
||||
|
||||
const openSettingsFromAccountMenu = () => {
|
||||
setIsAccountMenuOpen(false)
|
||||
navigate('/settings', {
|
||||
state: {
|
||||
backgroundLocation: location
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleConfirmClearAccountData = async () => {
|
||||
if (!canConfirmClear || isClearingAccountData) return
|
||||
setIsClearingAccountData(true)
|
||||
@@ -375,24 +387,14 @@ function Sidebar() {
|
||||
<span className="nav-label">通讯录</span>
|
||||
</NavLink>
|
||||
|
||||
{/* 私聊分析 */}
|
||||
{/* 聊天分析 */}
|
||||
<NavLink
|
||||
to="/analytics"
|
||||
className={`nav-item ${isActive('/analytics') ? 'active' : ''}`}
|
||||
title={collapsed ? '私聊分析' : undefined}
|
||||
title={collapsed ? '聊天分析' : undefined}
|
||||
>
|
||||
<span className="nav-icon"><BarChart3 size={20} /></span>
|
||||
<span className="nav-label">私聊分析</span>
|
||||
</NavLink>
|
||||
|
||||
{/* 群聊分析 */}
|
||||
<NavLink
|
||||
to="/group-analytics"
|
||||
className={`nav-item ${isActive('/group-analytics') ? 'active' : ''}`}
|
||||
title={collapsed ? '群聊分析' : undefined}
|
||||
>
|
||||
<span className="nav-icon"><Users size={20} /></span>
|
||||
<span className="nav-label">群聊分析</span>
|
||||
<span className="nav-label">聊天分析</span>
|
||||
</NavLink>
|
||||
|
||||
{/* 年度报告 */}
|
||||
@@ -427,16 +429,48 @@ function Sidebar() {
|
||||
</nav>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<button
|
||||
className="nav-item"
|
||||
onClick={() => {
|
||||
if (authEnabled) {
|
||||
setLocked(true)
|
||||
return
|
||||
}
|
||||
navigate('/settings', {
|
||||
state: {
|
||||
initialTab: 'security',
|
||||
backgroundLocation: location
|
||||
}
|
||||
})
|
||||
}}
|
||||
title={collapsed ? (authEnabled ? '锁定' : '未锁定') : undefined}
|
||||
>
|
||||
<span className="nav-icon">{authEnabled ? <Lock size={20} /> : <LockOpen size={20} />}</span>
|
||||
<span className="nav-label">{authEnabled ? '锁定' : '未锁定'}</span>
|
||||
</button>
|
||||
|
||||
<div className="sidebar-user-card-wrap" ref={accountCardWrapRef}>
|
||||
{isAccountMenuOpen && (
|
||||
<button
|
||||
className="sidebar-user-clear-trigger"
|
||||
onClick={openClearAccountDialog}
|
||||
type="button"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
<span>清除此账号所有数据</span>
|
||||
</button>
|
||||
<div className="sidebar-user-menu" role="menu" aria-label="账号菜单">
|
||||
<button
|
||||
className="sidebar-user-menu-item"
|
||||
onClick={openSettingsFromAccountMenu}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
>
|
||||
<Settings size={14} />
|
||||
<span>设置</span>
|
||||
</button>
|
||||
<button
|
||||
className="sidebar-user-menu-item danger"
|
||||
onClick={openClearAccountDialog}
|
||||
type="button"
|
||||
role="menuitem"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
<span>清除数据</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`sidebar-user-card ${isAccountMenuOpen ? 'menu-open' : ''}`}
|
||||
@@ -465,40 +499,6 @@ function Sidebar() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="nav-item"
|
||||
onClick={() => {
|
||||
if (authEnabled) {
|
||||
setLocked(true)
|
||||
return
|
||||
}
|
||||
navigate('/settings', { state: { initialTab: 'security' } })
|
||||
}}
|
||||
title={collapsed ? (authEnabled ? '锁定' : '未锁定') : undefined}
|
||||
>
|
||||
<span className="nav-icon">{authEnabled ? <Lock size={20} /> : <LockOpen size={20} />}</span>
|
||||
<span className="nav-label">{authEnabled ? '锁定' : '未锁定'}</span>
|
||||
</button>
|
||||
|
||||
<NavLink
|
||||
to="/settings"
|
||||
className={`nav-item ${isActive('/settings') ? 'active' : ''}`}
|
||||
title={collapsed ? '设置' : undefined}
|
||||
>
|
||||
<span className="nav-icon">
|
||||
<Settings size={20} />
|
||||
</span>
|
||||
<span className="nav-label">设置</span>
|
||||
</NavLink>
|
||||
|
||||
<button
|
||||
className="collapse-btn"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
title={collapsed ? '展开菜单' : '收起菜单'}
|
||||
>
|
||||
{collapsed ? <ChevronRight size={18} /> : <ChevronLeft size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showClearAccountDialog && (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { Search, User, X, Loader2 } from 'lucide-react'
|
||||
import { Search, User, X, Loader2, CheckSquare, Square, Download } from 'lucide-react'
|
||||
import { Avatar } from '../Avatar'
|
||||
|
||||
interface Contact {
|
||||
@@ -25,7 +25,12 @@ interface SnsFilterPanelProps {
|
||||
setContactSearch: (val: string) => void
|
||||
loading?: boolean
|
||||
contactsCountProgress?: ContactsCountProgress
|
||||
selectedContactUsernames: string[]
|
||||
activeContactUsername?: string
|
||||
onOpenContactTimeline: (contact: Contact) => void
|
||||
onToggleContactSelected: (contact: Contact) => void
|
||||
onClearSelectedContacts: () => void
|
||||
onExportSelectedContacts: () => void
|
||||
}
|
||||
|
||||
export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
||||
@@ -37,12 +42,21 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
||||
setContactSearch,
|
||||
loading,
|
||||
contactsCountProgress,
|
||||
onOpenContactTimeline
|
||||
selectedContactUsernames,
|
||||
activeContactUsername,
|
||||
onOpenContactTimeline,
|
||||
onToggleContactSelected,
|
||||
onClearSelectedContacts,
|
||||
onExportSelectedContacts
|
||||
}) => {
|
||||
const filteredContacts = contacts.filter(c =>
|
||||
(c.displayName || '').toLowerCase().includes(contactSearch.toLowerCase()) ||
|
||||
c.username.toLowerCase().includes(contactSearch.toLowerCase())
|
||||
)
|
||||
const selectedContactLookup = React.useMemo(
|
||||
() => new Set(selectedContactUsernames),
|
||||
[selectedContactUsernames]
|
||||
)
|
||||
|
||||
const clearFilters = () => {
|
||||
setSearchKeyword('')
|
||||
@@ -122,35 +136,69 @@ export const SnsFilterPanel: React.FC<SnsFilterPanelProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="contact-interaction-hint">
|
||||
点左侧可多选下载,点右侧可查看单人详情
|
||||
</div>
|
||||
|
||||
<div className="contact-list-scroll">
|
||||
{filteredContacts.map(contact => {
|
||||
const isPostCountReady = contact.postCountStatus === 'ready'
|
||||
const isSelected = selectedContactLookup.has(contact.username)
|
||||
const isActive = activeContactUsername === contact.username
|
||||
return (
|
||||
<div
|
||||
key={contact.username}
|
||||
className="contact-row"
|
||||
onClick={() => onOpenContactTimeline(contact)}
|
||||
>
|
||||
<Avatar src={contact.avatarUrl} name={contact.displayName} size={36} shape="rounded" />
|
||||
<div className="contact-meta">
|
||||
<span className="contact-name">{contact.displayName}</span>
|
||||
<div
|
||||
key={contact.username}
|
||||
className={`contact-row${isSelected ? ' is-selected' : ''}${isActive ? ' is-active' : ''}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={`contact-select-btn${isSelected ? ' checked' : ''}`}
|
||||
onClick={() => onToggleContactSelected(contact)}
|
||||
title={isSelected ? `取消选择 ${contact.displayName}` : `选择 ${contact.displayName}`}
|
||||
aria-pressed={isSelected}
|
||||
>
|
||||
{isSelected ? <CheckSquare size={16} /> : <Square size={16} />}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="contact-main-btn"
|
||||
onClick={() => onOpenContactTimeline(contact)}
|
||||
title={`查看 ${contact.displayName} 的朋友圈`}
|
||||
>
|
||||
<Avatar src={contact.avatarUrl} name={contact.displayName} size={36} shape="rounded" />
|
||||
<div className="contact-meta">
|
||||
<span className="contact-name">{contact.displayName}</span>
|
||||
</div>
|
||||
<div className="contact-post-count-wrap">
|
||||
{isPostCountReady ? (
|
||||
<span className="contact-post-count">{Math.max(0, Math.floor(Number(contact.postCount || 0)))}条</span>
|
||||
) : (
|
||||
<span className="contact-post-count-loading" title="统计中">
|
||||
<Loader2 size={13} className="spinning" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div className="contact-post-count-wrap">
|
||||
{isPostCountReady ? (
|
||||
<span className="contact-post-count">{Math.max(0, Math.floor(Number(contact.postCount || 0)))}条</span>
|
||||
) : (
|
||||
<span className="contact-post-count-loading" title="统计中">
|
||||
<Loader2 size={13} className="spinning" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{filteredContacts.length === 0 && (
|
||||
<div className="empty-state">{getEmptyStateText()}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedContactUsernames.length > 0 && (
|
||||
<div className="contact-batch-bar">
|
||||
<span className="contact-batch-summary">已选 {selectedContactUsernames.length} 人</span>
|
||||
<button type="button" className="contact-batch-btn" onClick={onClearSelectedContacts}>
|
||||
清空
|
||||
</button>
|
||||
<button type="button" className="contact-batch-btn primary" onClick={onExportSelectedContacts}>
|
||||
<Download size={14} />
|
||||
<span>下载所选</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
-webkit-app-region: drag;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
z-index: 2101;
|
||||
}
|
||||
|
||||
// 繁花如梦:标题栏毛玻璃
|
||||
@@ -16,6 +19,12 @@
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
.title-brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title-logo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
@@ -27,3 +36,24 @@
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.title-sidebar-toggle {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text-tertiary);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react'
|
||||
import './TitleBar.scss'
|
||||
|
||||
interface TitleBarProps {
|
||||
title?: string
|
||||
sidebarCollapsed?: boolean
|
||||
onToggleSidebar?: () => void
|
||||
}
|
||||
|
||||
function TitleBar({ title }: TitleBarProps = {}) {
|
||||
function TitleBar({ title, sidebarCollapsed = false, onToggleSidebar }: TitleBarProps = {}) {
|
||||
return (
|
||||
<div className="title-bar">
|
||||
<img src="./logo.png" alt="WeFlow" className="title-logo" />
|
||||
<span className="titles">{title || 'WeFlow'}</span>
|
||||
<div className="title-brand">
|
||||
<img src="./logo.png" alt="WeFlow" className="title-logo" />
|
||||
<span className="titles">{title || 'WeFlow'}</span>
|
||||
{onToggleSidebar ? (
|
||||
<button
|
||||
type="button"
|
||||
className="title-sidebar-toggle"
|
||||
onClick={onToggleSidebar}
|
||||
title={sidebarCollapsed ? '展开菜单' : '收起菜单'}
|
||||
aria-label={sidebarCollapsed ? '展开菜单' : '收起菜单'}
|
||||
>
|
||||
{sidebarCollapsed ? <PanelLeftOpen size={16} /> : <PanelLeftClose size={16} />}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user