mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
新的提交
This commit is contained in:
214
src/components/DateRangePicker.scss
Normal file
214
src/components/DateRangePicker.scss
Normal file
@@ -0,0 +1,214 @@
|
||||
.date-range-picker {
|
||||
position: relative;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
.picker-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 12px;
|
||||
height: 32px;
|
||||
background: var(--bg-tertiary);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
svg {
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: var(--bg-hover);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
color: var(--text-tertiary);
|
||||
margin-left: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.picker-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
background: var(--card-bg);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--border-color);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
animation: dropdownFadeIn 0.15s ease-out;
|
||||
}
|
||||
|
||||
@keyframes dropdownFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.quick-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
border-right: 1px solid var(--border-color);
|
||||
min-width: 100px;
|
||||
|
||||
.quick-option {
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
text-align: left;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.calendar-section {
|
||||
padding: 16px;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.month-year {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.weekday-header {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
padding: 8px 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
aspect-ratio: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
color: var(--text-tertiary);
|
||||
border-radius: 8px;
|
||||
cursor: default;
|
||||
position: relative;
|
||||
|
||||
&.valid {
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&.today {
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
&.in-range {
|
||||
background: var(--primary-light);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&.start {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border-radius: 8px 0 0 8px;
|
||||
|
||||
&.end {
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&.end {
|
||||
background: var(--primary);
|
||||
color: #fff;
|
||||
border-radius: 0 8px 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.selection-hint {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
}
|
||||
204
src/components/DateRangePicker.tsx
Normal file
204
src/components/DateRangePicker.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Calendar, ChevronLeft, ChevronRight, X } from 'lucide-react'
|
||||
import './DateRangePicker.scss'
|
||||
|
||||
interface DateRangePickerProps {
|
||||
startDate: string
|
||||
endDate: string
|
||||
onStartDateChange: (date: string) => void
|
||||
onEndDateChange: (date: string) => void
|
||||
onRangeComplete?: () => void
|
||||
}
|
||||
|
||||
const MONTH_NAMES = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
|
||||
const WEEKDAY_NAMES = ['日', '一', '二', '三', '四', '五', '六']
|
||||
|
||||
// 快捷选项
|
||||
const QUICK_OPTIONS = [
|
||||
{ label: '最近7天', days: 7 },
|
||||
{ label: '最近30天', days: 30 },
|
||||
{ label: '最近90天', days: 90 },
|
||||
{ label: '最近一年', days: 365 },
|
||||
{ label: '全部时间', days: 0 },
|
||||
]
|
||||
|
||||
function DateRangePicker({ startDate, endDate, onStartDateChange, onEndDateChange, onRangeComplete }: DateRangePickerProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [currentMonth, setCurrentMonth] = useState(new Date())
|
||||
const [selectingStart, setSelectingStart] = useState(true)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 点击外部关闭
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [isOpen])
|
||||
|
||||
const formatDisplayDate = (dateStr: string) => {
|
||||
if (!dateStr) return ''
|
||||
const date = new Date(dateStr)
|
||||
return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`
|
||||
}
|
||||
|
||||
const getDisplayText = () => {
|
||||
if (!startDate && !endDate) return '选择时间范围'
|
||||
if (startDate && endDate) return `${formatDisplayDate(startDate)} - ${formatDisplayDate(endDate)}`
|
||||
if (startDate) return `${formatDisplayDate(startDate)} - ?`
|
||||
return `? - ${formatDisplayDate(endDate)}`
|
||||
}
|
||||
|
||||
const handleQuickOption = (days: number) => {
|
||||
if (days === 0) {
|
||||
onStartDateChange('')
|
||||
onEndDateChange('')
|
||||
} else {
|
||||
const end = new Date()
|
||||
const start = new Date()
|
||||
start.setDate(start.getDate() - days)
|
||||
onStartDateChange(start.toISOString().split('T')[0])
|
||||
onEndDateChange(end.toISOString().split('T')[0])
|
||||
}
|
||||
setIsOpen(false)
|
||||
setTimeout(() => onRangeComplete?.(), 0)
|
||||
}
|
||||
|
||||
const handleClear = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onStartDateChange('')
|
||||
onEndDateChange('')
|
||||
}
|
||||
|
||||
|
||||
const getDaysInMonth = (date: Date) => {
|
||||
return new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate()
|
||||
}
|
||||
|
||||
const getFirstDayOfMonth = (date: Date) => {
|
||||
return new Date(date.getFullYear(), date.getMonth(), 1).getDay()
|
||||
}
|
||||
|
||||
const handleDateClick = (day: number) => {
|
||||
const dateStr = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
||||
|
||||
if (selectingStart) {
|
||||
onStartDateChange(dateStr)
|
||||
if (endDate && dateStr > endDate) {
|
||||
onEndDateChange('')
|
||||
}
|
||||
setSelectingStart(false)
|
||||
} else {
|
||||
if (dateStr < startDate) {
|
||||
onStartDateChange(dateStr)
|
||||
onEndDateChange(startDate)
|
||||
} else {
|
||||
onEndDateChange(dateStr)
|
||||
}
|
||||
setSelectingStart(true)
|
||||
setIsOpen(false)
|
||||
setTimeout(() => onRangeComplete?.(), 0)
|
||||
}
|
||||
}
|
||||
|
||||
const isInRange = (day: number) => {
|
||||
if (!startDate || !endDate) return false
|
||||
const dateStr = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
||||
return dateStr >= startDate && dateStr <= endDate
|
||||
}
|
||||
|
||||
const isStartDate = (day: number) => {
|
||||
const dateStr = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
||||
return dateStr === startDate
|
||||
}
|
||||
|
||||
const isEndDate = (day: number) => {
|
||||
const dateStr = `${currentMonth.getFullYear()}-${String(currentMonth.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`
|
||||
return dateStr === endDate
|
||||
}
|
||||
|
||||
const isToday = (day: number) => {
|
||||
const today = new Date()
|
||||
return currentMonth.getFullYear() === today.getFullYear() &&
|
||||
currentMonth.getMonth() === today.getMonth() &&
|
||||
day === today.getDate()
|
||||
}
|
||||
|
||||
const renderCalendar = () => {
|
||||
const daysInMonth = getDaysInMonth(currentMonth)
|
||||
const firstDay = getFirstDayOfMonth(currentMonth)
|
||||
const days: (number | null)[] = []
|
||||
|
||||
for (let i = 0; i < firstDay; i++) {
|
||||
days.push(null)
|
||||
}
|
||||
for (let i = 1; i <= daysInMonth; i++) {
|
||||
days.push(i)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="calendar-grid">
|
||||
{WEEKDAY_NAMES.map(name => (
|
||||
<div key={name} className="weekday-header">{name}</div>
|
||||
))}
|
||||
{days.map((day, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`calendar-day ${day ? 'valid' : ''} ${day && isInRange(day) ? 'in-range' : ''} ${day && isStartDate(day) ? 'start' : ''} ${day && isEndDate(day) ? 'end' : ''} ${day && isToday(day) ? 'today' : ''}`}
|
||||
onClick={() => day && handleDateClick(day)}
|
||||
>
|
||||
{day}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="date-range-picker" ref={containerRef}>
|
||||
<button className="picker-trigger" onClick={() => setIsOpen(!isOpen)}>
|
||||
<Calendar size={14} />
|
||||
<span className="picker-text">{getDisplayText()}</span>
|
||||
{(startDate || endDate) && (
|
||||
<button className="clear-btn" onClick={handleClear}>
|
||||
<X size={12} />
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="picker-dropdown">
|
||||
<div className="quick-options">
|
||||
{QUICK_OPTIONS.map(opt => (
|
||||
<button key={opt.label} className="quick-option" onClick={() => handleQuickOption(opt.days)}>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="calendar-section">
|
||||
<div className="calendar-header">
|
||||
<button className="nav-btn" onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1))}>
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
<span className="month-year">{currentMonth.getFullYear()}年 {MONTH_NAMES[currentMonth.getMonth()]}</span>
|
||||
<button className="nav-btn" onClick={() => setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1))}>
|
||||
<ChevronRight size={16} />
|
||||
</button>
|
||||
</div>
|
||||
{renderCalendar()}
|
||||
<div className="selection-hint">
|
||||
{selectingStart ? '请选择开始日期' : '请选择结束日期'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DateRangePicker
|
||||
29
src/components/RouteGuard.tsx
Normal file
29
src/components/RouteGuard.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useAppStore } from '../stores/appStore'
|
||||
|
||||
interface RouteGuardProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
// 不需要数据库连接的页面
|
||||
const PUBLIC_ROUTES = ['/', '/home', '/settings', '/data-management']
|
||||
|
||||
function RouteGuard({ children }: RouteGuardProps) {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const isDbConnected = useAppStore(state => state.isDbConnected)
|
||||
|
||||
useEffect(() => {
|
||||
const isPublicRoute = PUBLIC_ROUTES.includes(location.pathname)
|
||||
|
||||
// 未连接数据库且不在公开页面,跳转到欢迎页
|
||||
if (!isDbConnected && !isPublicRoute) {
|
||||
navigate('/', { replace: true })
|
||||
}
|
||||
}, [isDbConnected, location.pathname, navigate])
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
export default RouteGuard
|
||||
107
src/components/Sidebar.scss
Normal file
107
src/components/Sidebar.scss
Normal file
@@ -0,0 +1,107 @@
|
||||
.sidebar {
|
||||
width: 200px;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 0;
|
||||
transition: width 0.25s ease;
|
||||
|
||||
&.collapsed {
|
||||
width: 64px;
|
||||
|
||||
.nav-menu,
|
||||
.sidebar-footer {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 9999px;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 0 8px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 12px;
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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);
|
||||
}
|
||||
}
|
||||
112
src/components/Sidebar.tsx
Normal file
112
src/components/Sidebar.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { useState } from 'react'
|
||||
import { NavLink, useLocation } from 'react-router-dom'
|
||||
import { Home, MessageSquare, BarChart3, Users, FileText, Database, Settings, ChevronLeft, ChevronRight, Download } from 'lucide-react'
|
||||
import './Sidebar.scss'
|
||||
|
||||
function Sidebar() {
|
||||
const location = useLocation()
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
const isActive = (path: string) => {
|
||||
return location.pathname === path || location.pathname.startsWith(`${path}/`)
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
|
||||
<nav className="nav-menu">
|
||||
{/* 首页 */}
|
||||
<NavLink
|
||||
to="/home"
|
||||
className={`nav-item ${isActive('/home') ? 'active' : ''}`}
|
||||
title={collapsed ? '首页' : undefined}
|
||||
>
|
||||
<span className="nav-icon"><Home size={20} /></span>
|
||||
<span className="nav-label">首页</span>
|
||||
</NavLink>
|
||||
|
||||
{/* 聊天 */}
|
||||
<NavLink
|
||||
to="/chat"
|
||||
className={`nav-item ${isActive('/chat') ? 'active' : ''}`}
|
||||
title={collapsed ? '聊天' : undefined}
|
||||
>
|
||||
<span className="nav-icon"><MessageSquare size={20} /></span>
|
||||
<span className="nav-label">聊天</span>
|
||||
</NavLink>
|
||||
|
||||
{/* 私聊分析 */}
|
||||
<NavLink
|
||||
to="/analytics"
|
||||
className={`nav-item ${isActive('/analytics') ? 'active' : ''}`}
|
||||
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>
|
||||
</NavLink>
|
||||
|
||||
{/* 年度报告 */}
|
||||
<NavLink
|
||||
to="/annual-report"
|
||||
className={`nav-item ${isActive('/annual-report') ? 'active' : ''}`}
|
||||
title={collapsed ? '年度报告' : undefined}
|
||||
>
|
||||
<span className="nav-icon"><FileText size={20} /></span>
|
||||
<span className="nav-label">年度报告</span>
|
||||
</NavLink>
|
||||
|
||||
{/* 导出 */}
|
||||
<NavLink
|
||||
to="/export"
|
||||
className={`nav-item ${isActive('/export') ? 'active' : ''}`}
|
||||
title={collapsed ? '导出' : undefined}
|
||||
>
|
||||
<span className="nav-icon"><Download size={20} /></span>
|
||||
<span className="nav-label">导出</span>
|
||||
</NavLink>
|
||||
|
||||
{/* 数据管理 */}
|
||||
<NavLink
|
||||
to="/data-management"
|
||||
className={`nav-item ${isActive('/data-management') ? 'active' : ''}`}
|
||||
title={collapsed ? '数据管理' : undefined}
|
||||
>
|
||||
<span className="nav-icon"><Database size={20} /></span>
|
||||
<span className="nav-label">数据管理</span>
|
||||
</NavLink>
|
||||
</nav>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<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>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
export default Sidebar
|
||||
23
src/components/TitleBar.scss
Normal file
23
src/components/TitleBar.scss
Normal file
@@ -0,0 +1,23 @@
|
||||
.title-bar {
|
||||
height: 41px;
|
||||
background: var(--bg-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
-webkit-app-region: drag;
|
||||
flex-shrink: 0;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title-logo {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.titles {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
12
src/components/TitleBar.tsx
Normal file
12
src/components/TitleBar.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import './TitleBar.scss'
|
||||
|
||||
function TitleBar() {
|
||||
return (
|
||||
<div className="title-bar">
|
||||
<img src="./logo.png" alt="WeFlow" className="title-logo" />
|
||||
<span className="titles">WeFlow</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TitleBar
|
||||
Reference in New Issue
Block a user