mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
feat: show settings as modal dialog
This commit is contained in:
50
src/App.tsx
50
src/App.tsx
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Routes, Route, Navigate, useNavigate, useLocation } from 'react-router-dom'
|
import { Routes, Route, Navigate, useNavigate, useLocation, type Location } from 'react-router-dom'
|
||||||
import TitleBar from './components/TitleBar'
|
import TitleBar from './components/TitleBar'
|
||||||
import Sidebar from './components/Sidebar'
|
import Sidebar from './components/Sidebar'
|
||||||
import RouteGuard from './components/RouteGuard'
|
import RouteGuard from './components/RouteGuard'
|
||||||
@@ -47,6 +47,13 @@ function RouteStateRedirect({ to }: { to: string }) {
|
|||||||
function App() {
|
function App() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
|
const settingsBackgroundRef = useRef<Location>({
|
||||||
|
pathname: '/home',
|
||||||
|
search: '',
|
||||||
|
hash: '',
|
||||||
|
state: null,
|
||||||
|
key: 'settings-fallback'
|
||||||
|
} as Location)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setDbConnected,
|
setDbConnected,
|
||||||
@@ -70,7 +77,12 @@ function App() {
|
|||||||
const isChatHistoryWindow = location.pathname.startsWith('/chat-history/')
|
const isChatHistoryWindow = location.pathname.startsWith('/chat-history/')
|
||||||
const isStandaloneChatWindow = location.pathname === '/chat-window'
|
const isStandaloneChatWindow = location.pathname === '/chat-window'
|
||||||
const isNotificationWindow = location.pathname === '/notification-window'
|
const isNotificationWindow = location.pathname === '/notification-window'
|
||||||
const isExportRoute = location.pathname === '/export'
|
const isSettingsRoute = location.pathname === '/settings'
|
||||||
|
const settingsRouteState = location.state as { backgroundLocation?: Location; initialTab?: unknown } | null
|
||||||
|
const routeLocation = isSettingsRoute
|
||||||
|
? settingsRouteState?.backgroundLocation ?? settingsBackgroundRef.current
|
||||||
|
: location
|
||||||
|
const isExportRoute = routeLocation.pathname === '/export'
|
||||||
const [themeHydrated, setThemeHydrated] = useState(false)
|
const [themeHydrated, setThemeHydrated] = useState(false)
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
||||||
|
|
||||||
@@ -89,6 +101,12 @@ function App() {
|
|||||||
// 数据收集同意状态
|
// 数据收集同意状态
|
||||||
const [showAnalyticsConsent, setShowAnalyticsConsent] = useState(false)
|
const [showAnalyticsConsent, setShowAnalyticsConsent] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (location.pathname !== '/settings') {
|
||||||
|
settingsBackgroundRef.current = location
|
||||||
|
}
|
||||||
|
}, [location])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const root = document.documentElement
|
const root = document.documentElement
|
||||||
const body = document.body
|
const body = document.body
|
||||||
@@ -437,6 +455,25 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 主窗口 - 完整布局
|
// 主窗口 - 完整布局
|
||||||
|
const handleCloseSettings = () => {
|
||||||
|
const backgroundLocation = settingsRouteState?.backgroundLocation ?? settingsBackgroundRef.current
|
||||||
|
if (backgroundLocation.pathname === '/settings') {
|
||||||
|
navigate('/home', { replace: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
navigate(
|
||||||
|
{
|
||||||
|
pathname: backgroundLocation.pathname,
|
||||||
|
search: backgroundLocation.search,
|
||||||
|
hash: backgroundLocation.hash
|
||||||
|
},
|
||||||
|
{
|
||||||
|
replace: true,
|
||||||
|
state: backgroundLocation.state
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-container">
|
<div className="app-container">
|
||||||
<div className="window-drag-region" aria-hidden="true" />
|
<div className="window-drag-region" aria-hidden="true" />
|
||||||
@@ -568,7 +605,7 @@ function App() {
|
|||||||
<ExportPage />
|
<ExportPage />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Routes>
|
<Routes location={routeLocation}>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/home" element={<HomePage />} />
|
<Route path="/home" element={<HomePage />} />
|
||||||
<Route path="/chat" element={<ChatPage />} />
|
<Route path="/chat" element={<ChatPage />} />
|
||||||
@@ -584,7 +621,6 @@ function App() {
|
|||||||
<Route path="/dual-report" element={<DualReportPage />} />
|
<Route path="/dual-report" element={<DualReportPage />} />
|
||||||
<Route path="/dual-report/view" element={<DualReportWindow />} />
|
<Route path="/dual-report/view" element={<DualReportWindow />} />
|
||||||
|
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
|
||||||
<Route path="/export" element={<div className="export-route-anchor" aria-hidden="true" />} />
|
<Route path="/export" element={<div className="export-route-anchor" aria-hidden="true" />} />
|
||||||
<Route path="/sns" element={<SnsPage />} />
|
<Route path="/sns" element={<SnsPage />} />
|
||||||
<Route path="/contacts" element={<ContactsPage />} />
|
<Route path="/contacts" element={<ContactsPage />} />
|
||||||
@@ -593,6 +629,10 @@ function App() {
|
|||||||
</RouteGuard>
|
</RouteGuard>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isSettingsRoute && (
|
||||||
|
<SettingsPage onClose={handleCloseSettings} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -284,7 +284,11 @@ function Sidebar({ collapsed }: SidebarProps) {
|
|||||||
|
|
||||||
const openSettingsFromAccountMenu = () => {
|
const openSettingsFromAccountMenu = () => {
|
||||||
setIsAccountMenuOpen(false)
|
setIsAccountMenuOpen(false)
|
||||||
navigate('/settings')
|
navigate('/settings', {
|
||||||
|
state: {
|
||||||
|
backgroundLocation: location
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirmClearAccountData = async () => {
|
const handleConfirmClearAccountData = async () => {
|
||||||
@@ -432,7 +436,12 @@ function Sidebar({ collapsed }: SidebarProps) {
|
|||||||
setLocked(true)
|
setLocked(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
navigate('/settings', { state: { initialTab: 'security' } })
|
navigate('/settings', {
|
||||||
|
state: {
|
||||||
|
initialTab: 'security',
|
||||||
|
backgroundLocation: location
|
||||||
|
}
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
title={collapsed ? (authEnabled ? '锁定' : '未锁定') : undefined}
|
title={collapsed ? (authEnabled ? '锁定' : '未锁定') : undefined}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,17 +1,38 @@
|
|||||||
|
.settings-modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 41px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 2050;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 28px 32px;
|
||||||
|
background: rgba(15, 23, 42, 0.28);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
.settings-page {
|
.settings-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
width: min(1160px, calc(100vw - 96px));
|
||||||
margin: -24px;
|
height: min(820px, calc(100vh - 120px));
|
||||||
|
max-height: 100%;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 24px;
|
||||||
|
box-shadow: 0 28px 80px rgba(15, 23, 42, 0.22);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-header {
|
.settings-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 20px;
|
gap: 20px;
|
||||||
|
margin-bottom: 18px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@@ -22,29 +43,76 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-title-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.settings-actions {
|
.settings-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-close-btn {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-color: rgba(139, 115, 85, 0.28);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-layout {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-tabs {
|
.settings-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
flex-direction: column;
|
||||||
padding: 4px;
|
gap: 6px;
|
||||||
background: var(--bg-tertiary);
|
padding: 12px;
|
||||||
border-radius: 12px;
|
width: 220px;
|
||||||
margin-bottom: 20px;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: fit-content;
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-btn {
|
.tab-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 10px 18px;
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 11px 14px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@@ -67,6 +135,7 @@
|
|||||||
.settings-body {
|
.settings-body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
min-width: 0;
|
||||||
padding-right: 8px;
|
padding-right: 8px;
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
@@ -85,8 +154,10 @@
|
|||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-radius: 16px;
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 20px;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
|
min-height: 100%;
|
||||||
|
|
||||||
.section-desc {
|
.section-desc {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -932,7 +1003,7 @@
|
|||||||
padding: 10px 24px;
|
padding: 10px 24px;
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
z-index: 100;
|
z-index: 2200;
|
||||||
animation: slideDown 0.3s ease;
|
animation: slideDown 0.3s ease;
|
||||||
|
|
||||||
&.success {
|
&.success {
|
||||||
@@ -946,6 +1017,27 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.settings-modal-overlay {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-page {
|
||||||
|
width: min(100%, calc(100vw - 40px));
|
||||||
|
height: min(100%, calc(100vh - 82px));
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tabs {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 220px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes slideDown {
|
@keyframes slideDown {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
Eye, EyeOff, FolderSearch, FolderOpen, Search, Copy,
|
Eye, EyeOff, FolderSearch, FolderOpen, Search, Copy,
|
||||||
RotateCcw, Trash2, Plug, Check, Sun, Moon, Monitor,
|
RotateCcw, Trash2, Plug, Check, Sun, Moon, Monitor,
|
||||||
Palette, Database, HardDrive, Info, RefreshCw, ChevronDown, Download, Mic,
|
Palette, Database, HardDrive, Info, RefreshCw, ChevronDown, Download, Mic,
|
||||||
ShieldCheck, Fingerprint, Lock, KeyRound, Bell, Globe, BarChart2
|
ShieldCheck, Fingerprint, Lock, KeyRound, Bell, Globe, BarChart2, X
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { Avatar } from '../components/Avatar'
|
import { Avatar } from '../components/Avatar'
|
||||||
import './SettingsPage.scss'
|
import './SettingsPage.scss'
|
||||||
@@ -36,7 +36,11 @@ interface WxidOption {
|
|||||||
modifiedTime: number
|
modifiedTime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function SettingsPage() {
|
interface SettingsPageProps {
|
||||||
|
onClose?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function SettingsPage({ onClose }: SettingsPageProps = {}) {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const {
|
const {
|
||||||
isDbConnected,
|
isDbConnected,
|
||||||
@@ -195,6 +199,17 @@ function SettingsPage() {
|
|||||||
setActiveTab(initialTab)
|
setActiveTab(initialTab)
|
||||||
}, [location.state])
|
}, [location.state])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!onClose) return
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}, [onClose])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => {
|
const removeDb = window.electronAPI.key.onDbKeyStatus((payload: { message: string; level: number }) => {
|
||||||
setDbKeyStatus(payload.message)
|
setDbKeyStatus(payload.message)
|
||||||
@@ -2049,7 +2064,8 @@ function SettingsPage() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings-page">
|
<div className="settings-modal-overlay" onClick={() => onClose?.()}>
|
||||||
|
<div className="settings-page" onClick={(event) => event.stopPropagation()}>
|
||||||
{message && <div className={`message-toast ${message.success ? 'success' : 'error'}`}>{message.text}</div>}
|
{message && <div className={`message-toast ${message.success ? 'success' : 'error'}`}>{message.text}</div>}
|
||||||
|
|
||||||
{/* 多账号选择对话框 */}
|
{/* 多账号选择对话框 */}
|
||||||
@@ -2080,17 +2096,30 @@ function SettingsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="settings-header">
|
<div className="settings-header">
|
||||||
|
<div className="settings-title-block">
|
||||||
<h1>设置</h1>
|
<h1>设置</h1>
|
||||||
|
<p>在这里集中调整 WeFlow 的功能、外观与数据行为。</p>
|
||||||
|
</div>
|
||||||
<div className="settings-actions">
|
<div className="settings-actions">
|
||||||
<button className="btn btn-secondary" onClick={handleTestConnection} disabled={isLoading || isTesting}>
|
<button className="btn btn-secondary" onClick={handleTestConnection} disabled={isLoading || isTesting}>
|
||||||
<Plug size={16} /> {isTesting ? '测试中...' : '测试连接'}
|
<Plug size={16} /> {isTesting ? '测试中...' : '测试连接'}
|
||||||
</button>
|
</button>
|
||||||
|
{onClose && (
|
||||||
|
<button type="button" className="settings-close-btn" onClick={onClose} aria-label="关闭设置">
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-tabs">
|
<div className="settings-layout">
|
||||||
|
<div className="settings-tabs" role="tablist" aria-label="设置项">
|
||||||
{tabs.map(tab => (
|
{tabs.map(tab => (
|
||||||
<button key={tab.id} className={`tab-btn ${activeTab === tab.id ? 'active' : ''}`} onClick={() => setActiveTab(tab.id)}>
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
className={`tab-btn ${activeTab === tab.id ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
>
|
||||||
<tab.icon size={16} />
|
<tab.icon size={16} />
|
||||||
<span>{tab.label}</span>
|
<span>{tab.label}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -2108,7 +2137,8 @@ function SettingsPage() {
|
|||||||
{activeTab === 'security' && renderSecurityTab()}
|
{activeTab === 'security' && renderSecurityTab()}
|
||||||
{activeTab === 'about' && renderAboutTab()}
|
{activeTab === 'about' && renderAboutTab()}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user