feat: 统一全局 Header 结构
- 新增 AppHeader 组件(Logo + 台入口 + 用户状态) - 新增 UserDropdown 组件(用户下拉菜单) - 新增 AppLayout 布局组件 - 移除 SidebarBrand 和 SidebarUser 组件 - 修改各台页面,移除侧边栏中的品牌区和用户区 - 修改 HomePage,移除独立 header/footer - 修改 Layout 组件,简化为 sidebar + content - 账户设置改为弹框形式,不中断用户操作 - 更新 README.md 布局系统说明 - 同步 delta specs 到主 specs
This commit is contained in:
11
src/App.jsx
11
src/App.jsx
@@ -1,5 +1,6 @@
|
||||
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
|
||||
import { UserProvider } from './contexts/UserContext.jsx';
|
||||
import AppLayout from './components/layout/AppLayout.jsx';
|
||||
import HomePage from './pages/HomePage.jsx';
|
||||
import LoginPage from './pages/LoginPage.jsx';
|
||||
import ConsolePage from './pages/ConsolePage.jsx';
|
||||
@@ -11,11 +12,13 @@ function App() {
|
||||
<UserProvider>
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/console" element={<ConsolePage />} />
|
||||
<Route path="/admin" element={<AdminPage />} />
|
||||
<Route path="/developer" element={<DeveloperPage />} />
|
||||
<Route element={<AppLayout />}>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/console" element={<ConsolePage />} />
|
||||
<Route path="/admin" element={<AdminPage />} />
|
||||
<Route path="/developer" element={<DeveloperPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
</UserProvider>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { FiMenu } from 'react-icons/fi';
|
||||
|
||||
function Layout({ sidebar, headerTitle, children, sidebarClassName = 'sidebar', contentClassName = '' }) {
|
||||
function Layout({ sidebar, children, sidebarClassName = 'sidebar', contentClassName = '' }) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
const toggleSidebar = () => {
|
||||
@@ -17,14 +16,6 @@ function Layout({ sidebar, headerTitle, children, sidebarClassName = 'sidebar',
|
||||
{sidebar}
|
||||
</aside>
|
||||
<main className="main-content">
|
||||
<header className="header">
|
||||
<div className="header-left">
|
||||
<div className="mobile-menu-btn" onClick={toggleSidebar}>
|
||||
<FiMenu />
|
||||
</div>
|
||||
<div className="header-title">{headerTitle}</div>
|
||||
</div>
|
||||
</header>
|
||||
<div className={`page-content ${contentClassName}`}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useUserContext } from '../../contexts/UserContext.jsx';
|
||||
import Toast from '../common/Toast.jsx';
|
||||
|
||||
function AccountPage() {
|
||||
const { user } = useUserContext();
|
||||
const [profileToast, setProfileToast] = useState(null);
|
||||
const [passwordErrors, setPasswordErrors] = useState({});
|
||||
const [passwordForm, setPasswordForm] = useState({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
|
||||
const handleProfileSave = () => {
|
||||
setProfileToast({ type: 'success', message: '保存成功' });
|
||||
setTimeout(() => setProfileToast(null), 3000);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (field, value) => {
|
||||
setPasswordForm(prev => ({ ...prev, [field]: value }));
|
||||
setPasswordErrors(prev => ({ ...prev, [field]: '' }));
|
||||
};
|
||||
|
||||
const handlePasswordSubmit = () => {
|
||||
const errors = {};
|
||||
if (!passwordForm.currentPassword) {
|
||||
errors.currentPassword = '请输入当前密码';
|
||||
}
|
||||
if (!passwordForm.newPassword) {
|
||||
errors.newPassword = '请输入新密码';
|
||||
}
|
||||
if (!passwordForm.confirmPassword) {
|
||||
errors.confirmPassword = '请再次输入新密码';
|
||||
} else if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||
errors.confirmPassword = '两次输入的密码不一致';
|
||||
}
|
||||
setPasswordErrors(errors);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="card-title">账号信息</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div style={{ textAlign: 'center', marginBottom: '24px', paddingBottom: '24px', borderBottom: '1px solid var(--color-border-2)' }}>
|
||||
<div style={{
|
||||
width: '100px',
|
||||
height: '100px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #3B82F6, #8B5CF6)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
fontSize: '36px',
|
||||
margin: '0 auto 12px'
|
||||
}}>{user.avatar}</div>
|
||||
<button className="btn btn-sm">更换头像</button>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-col">
|
||||
<div className="form-group">
|
||||
<label className="form-label">用户名</label>
|
||||
<input type="text" className="form-control" defaultValue="zhangsan" readOnly style={{ background: '#F8FAFC' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-col">
|
||||
<div className="form-group">
|
||||
<label className="form-label">姓名</label>
|
||||
<input type="text" className="form-control" defaultValue={user.name} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<div className="form-col">
|
||||
<div className="form-group">
|
||||
<label className="form-label">邮箱</label>
|
||||
<input type="email" className="form-control" defaultValue="zhangsan@example.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-col">
|
||||
<div className="form-group">
|
||||
<label className="form-label">手机号</label>
|
||||
<input type="text" className="form-control" defaultValue="138****8888" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">所属部门</label>
|
||||
<input type="text" className="form-control" defaultValue={user.role} readOnly style={{ background: '#F8FAFC' }} />
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={handleProfileSave}>保存修改</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="card-title">修改密码</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="form-group">
|
||||
<label className="form-label">当前密码</label>
|
||||
<input
|
||||
type="password"
|
||||
className={`form-control ${passwordErrors.currentPassword ? 'is-invalid' : ''}`}
|
||||
placeholder="请输入当前密码"
|
||||
value={passwordForm.currentPassword}
|
||||
onChange={e => handlePasswordChange('currentPassword', e.target.value)}
|
||||
/>
|
||||
{passwordErrors.currentPassword && (
|
||||
<div className="form-error">{passwordErrors.currentPassword}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">新密码</label>
|
||||
<input
|
||||
type="password"
|
||||
className={`form-control ${passwordErrors.newPassword ? 'is-invalid' : ''}`}
|
||||
placeholder="请输入新密码"
|
||||
value={passwordForm.newPassword}
|
||||
onChange={e => handlePasswordChange('newPassword', e.target.value)}
|
||||
/>
|
||||
{passwordErrors.newPassword && (
|
||||
<div className="form-error">{passwordErrors.newPassword}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">确认新密码</label>
|
||||
<input
|
||||
type="password"
|
||||
className={`form-control ${passwordErrors.confirmPassword ? 'is-invalid' : ''}`}
|
||||
placeholder="请再次输入新密码"
|
||||
value={passwordForm.confirmPassword}
|
||||
onChange={e => handlePasswordChange('confirmPassword', e.target.value)}
|
||||
/>
|
||||
{passwordErrors.confirmPassword && (
|
||||
<div className="form-error">{passwordErrors.confirmPassword}</div>
|
||||
)}
|
||||
</div>
|
||||
<button className="btn btn-primary" onClick={handlePasswordSubmit}>更新密码</button>
|
||||
</div>
|
||||
</div>
|
||||
<Toast
|
||||
visible={!!profileToast}
|
||||
type={profileToast?.type}
|
||||
message={profileToast?.message}
|
||||
onClose={() => setProfileToast(null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountPage;
|
||||
@@ -1,11 +1,21 @@
|
||||
import { FiX } from 'react-icons/fi';
|
||||
|
||||
function Modal({ visible, title, children, onConfirm, onCancel, confirmText = '确定', cancelText = '取消' }) {
|
||||
function Modal({
|
||||
visible,
|
||||
title,
|
||||
children,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
confirmText = '确定',
|
||||
cancelText = '取消',
|
||||
showConfirm = true,
|
||||
width
|
||||
}) {
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onCancel}>
|
||||
<div className="modal" onClick={e => e.stopPropagation()}>
|
||||
<div className="modal" onClick={e => e.stopPropagation()} style={width ? { width } : undefined}>
|
||||
<div className="modal-header">
|
||||
<div className="modal-title">{title}</div>
|
||||
<div className="modal-close" onClick={onCancel}>
|
||||
@@ -15,10 +25,12 @@ function Modal({ visible, title, children, onConfirm, onCancel, confirmText = '
|
||||
<div className="modal-body">
|
||||
{children}
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button className="btn" onClick={onCancel}>{cancelText}</button>
|
||||
<button className="btn btn-primary" onClick={onConfirm}>{confirmText}</button>
|
||||
</div>
|
||||
{showConfirm && (
|
||||
<div className="modal-footer">
|
||||
<button className="btn" onClick={onCancel}>{cancelText}</button>
|
||||
<button className="btn btn-primary" onClick={onConfirm}>{confirmText}</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
114
src/components/layout/AppHeader.jsx
Normal file
114
src/components/layout/AppHeader.jsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useLocation, useNavigate, Link } from 'react-router-dom';
|
||||
import { FiSettings, FiCode, FiUsers, FiMenu, FiX } from 'react-icons/fi';
|
||||
import Modal from '../common/Modal.jsx';
|
||||
import UserDropdown from './UserDropdown.jsx';
|
||||
|
||||
const PLATFORMS = [
|
||||
{ id: 'console', name: '工作台', path: '/console', icon: FiSettings },
|
||||
{ id: 'developer', name: '开发台', path: '/developer', icon: FiCode },
|
||||
{ id: 'admin', name: '管理台', path: '/admin', icon: FiUsers },
|
||||
];
|
||||
|
||||
function AppHeader() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [confirmModal, setConfirmModal] = useState({ visible: false, platform: null });
|
||||
const mobileMenuRef = useRef(null);
|
||||
|
||||
const currentPlatform = PLATFORMS.find(p => location.pathname.startsWith(p.path))?.id || null;
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event) {
|
||||
if (mobileMenuRef.current && !mobileMenuRef.current.contains(event.target)) {
|
||||
setMobileMenuOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handlePlatformClick = (platform) => {
|
||||
if (platform.id === currentPlatform) return;
|
||||
setConfirmModal({ visible: true, platform });
|
||||
setMobileMenuOpen(false);
|
||||
};
|
||||
|
||||
const handleConfirmSwitch = () => {
|
||||
if (confirmModal.platform) {
|
||||
navigate(confirmModal.platform.path);
|
||||
}
|
||||
setConfirmModal({ visible: false, platform: null });
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="app-header">
|
||||
<div className="app-header__left">
|
||||
<Link to="/" className="app-header__brand">
|
||||
<div className="sidebar-logo-icon">
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<span className="app-header__title">GrandClaw</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="app-header__right">
|
||||
<nav className="app-header__nav">
|
||||
{PLATFORMS.map(platform => {
|
||||
const Icon = platform.icon;
|
||||
return (
|
||||
<div
|
||||
key={platform.id}
|
||||
className={`app-header__nav-item ${currentPlatform === platform.id ? 'active' : ''}`}
|
||||
onClick={() => handlePlatformClick(platform)}
|
||||
>
|
||||
<Icon />
|
||||
<span>{platform.name}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<UserDropdown />
|
||||
|
||||
<div className="app-header__mobile" ref={mobileMenuRef}>
|
||||
<div className="app-header__mobile-btn" onClick={() => setMobileMenuOpen(!mobileMenuOpen)}>
|
||||
{mobileMenuOpen ? <FiX /> : <FiMenu />}
|
||||
</div>
|
||||
{mobileMenuOpen && (
|
||||
<div className="app-header__mobile-menu">
|
||||
{PLATFORMS.map(platform => {
|
||||
const Icon = platform.icon;
|
||||
return (
|
||||
<div
|
||||
key={platform.id}
|
||||
className={`app-header__mobile-item ${currentPlatform === platform.id ? 'active' : ''}`}
|
||||
onClick={() => handlePlatformClick(platform)}
|
||||
>
|
||||
<Icon />
|
||||
<span>{platform.name}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
visible={confirmModal.visible}
|
||||
title="切换确认"
|
||||
onConfirm={handleConfirmSwitch}
|
||||
onCancel={() => setConfirmModal({ visible: false, platform: null })}
|
||||
confirmText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<p>切换到{confirmModal.platform?.name}?</p>
|
||||
</Modal>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppHeader;
|
||||
15
src/components/layout/AppLayout.jsx
Normal file
15
src/components/layout/AppLayout.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import AppHeader from './AppHeader.jsx';
|
||||
|
||||
function AppLayout() {
|
||||
return (
|
||||
<div className="app-layout">
|
||||
<AppHeader />
|
||||
<main className="app-layout__main">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppLayout;
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* SidebarBrand - 侧边栏品牌区域组件
|
||||
* 统一显示 GrandClaw 品牌标识和副标题
|
||||
*
|
||||
* @param {Object} props - 组件属性
|
||||
* @param {string} [props.subtitle] - 副标题文本(如"企业级AI平台"、"运营管理台"、"技能开发台")
|
||||
*/
|
||||
function SidebarBrand({ subtitle = '企业级AI平台' }) {
|
||||
return (
|
||||
<div className="sidebar-brand">
|
||||
<div className="sidebar-logo-icon">
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div className="sidebar-brand-text">
|
||||
<div className="sidebar-logo">GrandClaw</div>
|
||||
<div className="sidebar-subtitle">{subtitle}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SidebarBrand;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useUserContext } from '../../contexts/UserContext.jsx';
|
||||
|
||||
/**
|
||||
* SidebarUser - 侧边栏用户信息组件
|
||||
* 从 UserContext 获取用户信息并显示
|
||||
*
|
||||
* @param {Object} props - 组件属性
|
||||
* @param {Function} [props.onClick] - 点击回调函数
|
||||
* @param {string} [props.wrapperClassName] - 包装器类名(如"chat-sidebar-user"、"admin-sidebar-user")
|
||||
* @param {string} [props.infoClassName] - 信息容器类名(如"chat-sidebar-user-info"、"admin-sidebar-user-info")
|
||||
* @param {string} [props.nameClassName] - 姓名容器类名(如"chat-sidebar-user-name"、"admin-sidebar-user-name")
|
||||
* @param {string} [props.roleClassName] - 角色容器类名(如"chat-sidebar-user-role"、"admin-sidebar-user-role")
|
||||
*/
|
||||
function SidebarUser({
|
||||
onClick,
|
||||
wrapperClassName = 'chat-sidebar-user',
|
||||
infoClassName = 'chat-sidebar-user-info',
|
||||
nameClassName = 'chat-sidebar-user-name',
|
||||
roleClassName = 'chat-sidebar-user-role',
|
||||
}) {
|
||||
const { user } = useUserContext();
|
||||
|
||||
return (
|
||||
<div className={wrapperClassName} onClick={onClick}>
|
||||
<div className="user-avatar">{user.avatar}</div>
|
||||
<div className={infoClassName}>
|
||||
<div className={nameClassName}>{user.name}</div>
|
||||
<div className={roleClassName}>{user.role}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SidebarUser;
|
||||
222
src/components/layout/UserDropdown.jsx
Normal file
222
src/components/layout/UserDropdown.jsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { FiChevronDown, FiUser, FiLogOut } from 'react-icons/fi';
|
||||
import { useUserContext } from '../../contexts/UserContext.jsx';
|
||||
import Modal from '../common/Modal.jsx';
|
||||
import Toast from '../common/Toast.jsx';
|
||||
|
||||
function UserDropdown() {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useUserContext();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [showAccountModal, setShowAccountModal] = useState(false);
|
||||
const [profileToast, setProfileToast] = useState(null);
|
||||
const [passwordErrors, setPasswordErrors] = useState({});
|
||||
const [passwordForm, setPasswordForm] = useState({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
});
|
||||
const dropdownRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event) {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleAccountClick = () => {
|
||||
setIsOpen(false);
|
||||
setShowAccountModal(true);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
setIsOpen(false);
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const handleProfileSave = () => {
|
||||
setProfileToast({ type: 'success', message: '保存成功' });
|
||||
setTimeout(() => setProfileToast(null), 3000);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (field, value) => {
|
||||
setPasswordForm(prev => ({ ...prev, [field]: value }));
|
||||
setPasswordErrors(prev => ({ ...prev, [field]: '' }));
|
||||
};
|
||||
|
||||
const handlePasswordSubmit = () => {
|
||||
const errors = {};
|
||||
if (!passwordForm.currentPassword) {
|
||||
errors.currentPassword = '请输入当前密码';
|
||||
}
|
||||
if (!passwordForm.newPassword) {
|
||||
errors.newPassword = '请输入新密码';
|
||||
}
|
||||
if (!passwordForm.confirmPassword) {
|
||||
errors.confirmPassword = '请再次输入新密码';
|
||||
} else if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||
errors.confirmPassword = '两次输入的密码不一致';
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length === 0) {
|
||||
setProfileToast({ type: 'success', message: '密码更新成功' });
|
||||
setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
|
||||
setTimeout(() => setProfileToast(null), 3000);
|
||||
} else {
|
||||
setPasswordErrors(errors);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="app-header__user" ref={dropdownRef}>
|
||||
<div className="app-header__user-trigger" onClick={() => setIsOpen(!isOpen)}>
|
||||
<div className="user-avatar">{user.avatar}</div>
|
||||
<span className="app-header__user-name">{user.name}</span>
|
||||
<FiChevronDown className={`app-header__user-arrow ${isOpen ? 'open' : ''}`} />
|
||||
</div>
|
||||
{isOpen && (
|
||||
<div className="app-header__dropdown">
|
||||
<div className="app-header__dropdown-item" onClick={handleAccountClick}>
|
||||
<FiUser />
|
||||
<span>账户设置</span>
|
||||
</div>
|
||||
<div className="app-header__dropdown-item" onClick={handleLogout}>
|
||||
<FiLogOut />
|
||||
<span>退出登录</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
visible={showAccountModal}
|
||||
title="账户设置"
|
||||
onCancel={() => setShowAccountModal(false)}
|
||||
showConfirm={false}
|
||||
width="720px"
|
||||
>
|
||||
<div className="account-modal-content">
|
||||
<div style={{ textAlign: 'center', marginBottom: '24px', paddingBottom: '24px', borderBottom: '1px solid var(--color-border-2)' }}>
|
||||
<div style={{
|
||||
width: '80px',
|
||||
height: '80px',
|
||||
borderRadius: '50%',
|
||||
background: 'linear-gradient(135deg, #3B82F6, #8B5CF6)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#fff',
|
||||
fontSize: '32px',
|
||||
margin: '0 auto 12px'
|
||||
}}>{user.avatar}</div>
|
||||
<button className="btn btn-sm">更换头像</button>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-col">
|
||||
<div className="form-group">
|
||||
<label className="form-label">用户名</label>
|
||||
<input type="text" className="form-control" defaultValue="zhangsan" readOnly style={{ background: '#F8FAFC' }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-col">
|
||||
<div className="form-group">
|
||||
<label className="form-label">姓名</label>
|
||||
<input type="text" className="form-control" defaultValue={user.name} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-col">
|
||||
<div className="form-group">
|
||||
<label className="form-label">邮箱</label>
|
||||
<input type="email" className="form-control" defaultValue="zhangsan@example.com" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-col">
|
||||
<div className="form-group">
|
||||
<label className="form-label">手机号</label>
|
||||
<input type="text" className="form-control" defaultValue="138****8888" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">所属部门</label>
|
||||
<input type="text" className="form-control" defaultValue={user.role} readOnly style={{ background: '#F8FAFC' }} />
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px', textAlign: 'right' }}>
|
||||
<button className="btn btn-primary" onClick={handleProfileSave}>保存修改</button>
|
||||
</div>
|
||||
|
||||
<div style={{ borderTop: '1px solid var(--color-border-2)', paddingTop: '20px', marginTop: '20px' }}>
|
||||
<h4 style={{ fontSize: '15px', fontWeight: '600', marginBottom: '16px', color: 'var(--color-text-1)' }}>修改密码</h4>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">当前密码</label>
|
||||
<input
|
||||
type="password"
|
||||
className={`form-control ${passwordErrors.currentPassword ? 'is-invalid' : ''}`}
|
||||
placeholder="请输入当前密码"
|
||||
value={passwordForm.currentPassword}
|
||||
onChange={e => handlePasswordChange('currentPassword', e.target.value)}
|
||||
/>
|
||||
{passwordErrors.currentPassword && (
|
||||
<div className="form-error">{passwordErrors.currentPassword}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">新密码</label>
|
||||
<input
|
||||
type="password"
|
||||
className={`form-control ${passwordErrors.newPassword ? 'is-invalid' : ''}`}
|
||||
placeholder="请输入新密码"
|
||||
value={passwordForm.newPassword}
|
||||
onChange={e => handlePasswordChange('newPassword', e.target.value)}
|
||||
/>
|
||||
{passwordErrors.newPassword && (
|
||||
<div className="form-error">{passwordErrors.newPassword}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">确认新密码</label>
|
||||
<input
|
||||
type="password"
|
||||
className={`form-control ${passwordErrors.confirmPassword ? 'is-invalid' : ''}`}
|
||||
placeholder="请再次输入新密码"
|
||||
value={passwordForm.confirmPassword}
|
||||
onChange={e => handlePasswordChange('confirmPassword', e.target.value)}
|
||||
/>
|
||||
{passwordErrors.confirmPassword && (
|
||||
<div className="form-error">{passwordErrors.confirmPassword}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<button className="btn btn-primary" onClick={handlePasswordSubmit}>更新密码</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Toast
|
||||
visible={!!profileToast}
|
||||
type={profileToast?.type}
|
||||
message={profileToast?.message}
|
||||
onClose={() => setProfileToast(null)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default UserDropdown;
|
||||
@@ -2,8 +2,6 @@ import { useState, useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { FiHome, FiBarChart2, FiUsers, FiList, FiCheckCircle, FiActivity, FiSettings } from 'react-icons/fi';
|
||||
import Layout from '../components/Layout.jsx';
|
||||
import SidebarBrand from '../components/layout/SidebarBrand.jsx';
|
||||
import SidebarUser from '../components/layout/SidebarUser.jsx';
|
||||
import SidebarNavItem from '../components/layout/SidebarNavItem.jsx';
|
||||
import usePageState from '../hooks/usePageState.js';
|
||||
import { ADMIN_PAGES } from '../constants/pages.js';
|
||||
@@ -20,7 +18,6 @@ import ConsoleReviewListPage from './console/ConsoleReviewListPage.jsx';
|
||||
import ConsoleReviewDetailPage from './console/ConsoleReviewDetailPage.jsx';
|
||||
import ModelConfigsPage from './admin/ModelConfigsPage.jsx';
|
||||
import AddModelConfigPage from './admin/AddModelConfigPage.jsx';
|
||||
import AccountPage from '../components/account/AccountPage.jsx';
|
||||
|
||||
function AdminPage() {
|
||||
const location = useLocation();
|
||||
@@ -107,30 +104,13 @@ function AdminPage() {
|
||||
onBack={() => navigateTo('modelConfigs')}
|
||||
editData={editData}
|
||||
/>;
|
||||
case 'account':
|
||||
return <AccountPage />;
|
||||
default:
|
||||
return <div>Page not found</div>;
|
||||
}
|
||||
};
|
||||
|
||||
const getPageTitle = () => {
|
||||
if (editData && (currentPage === 'addDepartment' || currentPage === 'addUser' || currentPage === 'addProject' || currentPage === 'addModelConfig')) {
|
||||
const prefix = '编辑';
|
||||
const nameMap = { addDepartment: '部门', addUser: '用户', addProject: '项目', addModelConfig: '配置' };
|
||||
return prefix + nameMap[currentPage];
|
||||
}
|
||||
if (currentPage === 'reviewDetail') {
|
||||
return reviewType === 'version' ? '版本审核' : '下架审核';
|
||||
}
|
||||
return ADMIN_PAGES[currentPage]?.title || '';
|
||||
};
|
||||
|
||||
const sidebar = (
|
||||
<>
|
||||
<div className="admin-sidebar-header">
|
||||
<SidebarBrand subtitle="运营管理台" />
|
||||
</div>
|
||||
<nav className="admin-sidebar-nav">
|
||||
<SidebarNavItem
|
||||
icon={<FiHome />}
|
||||
@@ -196,20 +176,12 @@ function AdminPage() {
|
||||
textClassName="admin-nav-text"
|
||||
/>
|
||||
</nav>
|
||||
<SidebarUser
|
||||
onClick={() => navigateTo('account')}
|
||||
wrapperClassName="admin-sidebar-user"
|
||||
infoClassName="admin-sidebar-user-info"
|
||||
nameClassName="admin-sidebar-user-name"
|
||||
roleClassName="admin-sidebar-user-role"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
sidebar={sidebar}
|
||||
headerTitle={getPageTitle()}
|
||||
sidebarClassName="admin-sidebar"
|
||||
>
|
||||
{renderPage()}
|
||||
|
||||
@@ -3,8 +3,6 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { FiPlus, FiClock, FiList, FiUsers, FiBox } from 'react-icons/fi';
|
||||
import { FaPuzzlePiece } from 'react-icons/fa';
|
||||
import Layout from '../components/Layout.jsx';
|
||||
import SidebarBrand from '../components/layout/SidebarBrand.jsx';
|
||||
import SidebarUser from '../components/layout/SidebarUser.jsx';
|
||||
import SidebarNavItem from '../components/layout/SidebarNavItem.jsx';
|
||||
import usePageState from '../hooks/usePageState.js';
|
||||
import { CONSOLE_PAGES } from '../constants/pages.js';
|
||||
@@ -18,7 +16,6 @@ import SkillConfigPage from './console/SkillConfigPage.jsx';
|
||||
import LogsPage from './console/LogsPage.jsx';
|
||||
import TasksPage from './console/TasksPage.jsx';
|
||||
import TaskDetailPage from './console/TaskDetailPage.jsx';
|
||||
import AccountPage from '../components/account/AccountPage.jsx';
|
||||
import ProjectsPage from './console/ProjectsPage.jsx';
|
||||
import MemberConfigPage from './console/MemberConfigPage.jsx';
|
||||
import AddMemberPage from './console/AddMemberPage.jsx';
|
||||
@@ -120,8 +117,6 @@ function ConsolePage() {
|
||||
taskId={currentTaskId}
|
||||
onBack={() => switchPage('scheduledTasks')}
|
||||
/>;
|
||||
case 'account':
|
||||
return <AccountPage />;
|
||||
case 'projects':
|
||||
return <ProjectsPage onAddMember={() => switchPage('addMember')} />;
|
||||
case 'memberConfig':
|
||||
@@ -149,8 +144,6 @@ function ConsolePage() {
|
||||
const sidebar = (
|
||||
<>
|
||||
<div className="chat-sidebar-header">
|
||||
<SidebarBrand subtitle="企业级AI平台" />
|
||||
<div className="sidebar-divider"></div>
|
||||
<button className="btn btn-primary" style={{ width: '100%' }} onClick={createNewChat}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}>
|
||||
<FiPlus /> 新建对话
|
||||
@@ -209,14 +202,12 @@ function ConsolePage() {
|
||||
onClick={() => switchPage('projects')}
|
||||
/>
|
||||
</div>
|
||||
<SidebarUser onClick={() => switchPage('account')} />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
sidebar={sidebar}
|
||||
headerTitle={getPageTitle()}
|
||||
sidebarClassName="chat-sidebar"
|
||||
contentClassName={currentPage === 'chat' ? 'page-content-full' : ''}
|
||||
>
|
||||
|
||||
@@ -3,8 +3,6 @@ import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { FiPlus, FiTerminal, FiHome } from 'react-icons/fi';
|
||||
import { FaPuzzlePiece } from 'react-icons/fa';
|
||||
import Layout from '../components/Layout.jsx';
|
||||
import SidebarBrand from '../components/layout/SidebarBrand.jsx';
|
||||
import SidebarUser from '../components/layout/SidebarUser.jsx';
|
||||
import SidebarNavItem from '../components/layout/SidebarNavItem.jsx';
|
||||
import usePageState from '../hooks/usePageState.js';
|
||||
import { DEVELOPER_PAGES } from '../constants/pages.js';
|
||||
@@ -15,7 +13,6 @@ import MySkillsPage from './developer/MySkillsPage.jsx';
|
||||
import UploadSkillPage from './developer/UploadSkillPage.jsx';
|
||||
import NewVersionPage from './developer/NewVersionPage.jsx';
|
||||
import DevDocsPage from './developer/DevDocsPage.jsx';
|
||||
import AccountPage from '../components/account/AccountPage.jsx';
|
||||
import SkillEditorPage from './developer/SkillEditorPage.jsx';
|
||||
import UpdateSkillInfoPage from './developer/UpdateSkillInfoPage.jsx';
|
||||
import UploadVersionPage from './developer/UploadVersionPage.jsx';
|
||||
@@ -103,8 +100,6 @@ function DeveloperPage() {
|
||||
return <UploadSkillPage onBack={() => switchPage('mySkills')} />;
|
||||
case 'devDocs':
|
||||
return <DevDocsPage />;
|
||||
case 'devAccount':
|
||||
return <AccountPage />;
|
||||
case 'skillEditor':
|
||||
return <SkillEditorPage
|
||||
skillId={currentSkillId}
|
||||
@@ -124,15 +119,9 @@ function DeveloperPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const getPageTitle = () => {
|
||||
return DEVELOPER_PAGES[currentPage]?.title || '';
|
||||
};
|
||||
|
||||
const sidebar = (
|
||||
<>
|
||||
<div className="chat-sidebar-header">
|
||||
<SidebarBrand subtitle="技能开发台" />
|
||||
<div className="sidebar-divider"></div>
|
||||
<button className="btn btn-primary" style={{ width: '100%' }} onClick={createNewProject}>
|
||||
<span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '6px' }}>
|
||||
<FiPlus /> 创建技能
|
||||
@@ -175,14 +164,12 @@ function DeveloperPage() {
|
||||
onClick={() => switchPage('devDocs')}
|
||||
/>
|
||||
</div>
|
||||
<SidebarUser onClick={() => switchPage('devAccount')} />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout
|
||||
sidebar={sidebar}
|
||||
headerTitle={getPageTitle()}
|
||||
sidebarClassName="chat-sidebar"
|
||||
>
|
||||
{renderPage()}
|
||||
|
||||
@@ -1,33 +1,10 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FiSettings, FiCode, FiUsers, FiMonitor, FiList, FiLogIn } from 'react-icons/fi';
|
||||
import { FiMonitor, FiList } from 'react-icons/fi';
|
||||
import { FaRobot, FaPuzzlePiece } from 'react-icons/fa';
|
||||
|
||||
function HomePage() {
|
||||
return (
|
||||
<div className="home-layout">
|
||||
<header className="home-header">
|
||||
<div className="home-logo">
|
||||
<div className="sidebar-logo-icon">
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
GrandClaw
|
||||
</div>
|
||||
<nav className="home-nav">
|
||||
<Link to="/console" state={{ fromHome: true }}>
|
||||
<FiSettings /> 工作台
|
||||
</Link>
|
||||
<Link to="/developer?init=true" state={{ fromHome: true }}>
|
||||
<FiCode /> 开发台
|
||||
</Link>
|
||||
<Link to="/admin" state={{ fromHome: true }}>
|
||||
<FiUsers /> 管理台
|
||||
</Link>
|
||||
<Link to="/login" className="home-nav-login">
|
||||
<FiLogIn /> 登录
|
||||
</Link>
|
||||
</nav>
|
||||
</header>
|
||||
<main className="home-main">
|
||||
<div className="home-badge">
|
||||
<span className="home-badge-dot"></span>
|
||||
@@ -66,9 +43,6 @@ function HomePage() {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="home-footer">
|
||||
© 2026 GrandClaw Team · 前端原型演示
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
@forward 'password-input';
|
||||
@forward 'search-bar';
|
||||
@forward 'stat-card';
|
||||
@forward 'header';
|
||||
|
||||
.page-back-btn {
|
||||
display: inline-flex;
|
||||
|
||||
251
src/styles/components/header/_index.scss
Normal file
251
src/styles/components/header/_index.scss
Normal file
@@ -0,0 +1,251 @@
|
||||
// AppHeader 组件样式
|
||||
|
||||
@use '../../tokens' as *;
|
||||
|
||||
// 全局布局容器
|
||||
.app-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background: var(--color-bg-1);
|
||||
}
|
||||
|
||||
.app-layout__main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
// 头部容器
|
||||
.app-header {
|
||||
height: var(--header-height);
|
||||
background: var(--color-bg-1);
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: $z-index-header;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 左侧品牌区
|
||||
.app-header__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-header__brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
text-decoration: none;
|
||||
color: var(--color-text-1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.app-header__title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
// 右侧功能区
|
||||
.app-header__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
// 导航区
|
||||
.app-header__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.app-header__nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
color: var(--color-text-2);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
transition: all 0.2s;
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-1);
|
||||
background: var(--color-bg-2);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
}
|
||||
|
||||
// 用户状态区
|
||||
.app-header__user {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.app-header__user-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-2);
|
||||
}
|
||||
}
|
||||
|
||||
.app-header__user-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.app-header__user-arrow {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--color-text-3);
|
||||
transition: transform 0.2s;
|
||||
|
||||
&.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉菜单
|
||||
.app-header__dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
min-width: 160px;
|
||||
background: var(--color-bg-1);
|
||||
border: 1px solid var(--color-border-2);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.1);
|
||||
overflow: hidden;
|
||||
z-index: $z-index-modal;
|
||||
}
|
||||
|
||||
.app-header__dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-2);
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端菜单
|
||||
.app-header__mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-header__mobile-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-md);
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-2);
|
||||
}
|
||||
}
|
||||
|
||||
.app-header__mobile-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 16px;
|
||||
margin-top: 4px;
|
||||
min-width: 160px;
|
||||
background: var(--color-bg-1);
|
||||
border: 1px solid var(--color-border-2);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.1);
|
||||
overflow: hidden;
|
||||
z-index: $z-index-modal;
|
||||
}
|
||||
|
||||
.app-header__mobile-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-2);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-2);
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
}
|
||||
|
||||
// 响应式 - 移动端
|
||||
@include mobile {
|
||||
.app-header__nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.app-header__mobile {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.app-header__user-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,6 @@
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.admin-sidebar-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
}
|
||||
|
||||
.admin-sidebar-nav {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -68,41 +63,6 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// 管理台用户区域
|
||||
.admin-sidebar-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--color-border-2);
|
||||
background: var(--color-bg-2);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-3);
|
||||
}
|
||||
}
|
||||
|
||||
.admin-sidebar-user-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.admin-sidebar-user-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
margin-bottom: 2px;
|
||||
@include text-truncate;
|
||||
}
|
||||
|
||||
.admin-sidebar-user-role {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
@include text-truncate;
|
||||
}
|
||||
|
||||
// 管理台内容区
|
||||
.admin-layout__content {
|
||||
flex: 1;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
.app-shell,
|
||||
.layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
background: var(--color-bg-1);
|
||||
border-right: 1px solid var(--color-border-2);
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
z-index: $z-index-sidebar;
|
||||
@@ -24,23 +24,6 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
// 侧边栏头部
|
||||
.sidebar-header {
|
||||
height: var(--header-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// 品牌区
|
||||
.sidebar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sidebar-logo-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
@@ -87,20 +70,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-brand-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-1);
|
||||
letter-spacing: -0.3px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
// 导航区
|
||||
.sidebar__nav,
|
||||
.sidebar-menu {
|
||||
@@ -114,13 +83,6 @@
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.sidebar-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
// 导航项 - 统一使用 .nav-item
|
||||
.nav-item,
|
||||
.menu-item {
|
||||
@@ -178,34 +140,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 顶部栏
|
||||
.app-shell__header,
|
||||
.header {
|
||||
height: var(--header-height);
|
||||
background: var(--color-bg-1);
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: $z-index-header;
|
||||
}
|
||||
|
||||
.header__left,
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
// 用户头像
|
||||
.user-avatar {
|
||||
width: 34px;
|
||||
@@ -290,10 +224,6 @@
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-menu-btn {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
@@ -177,41 +177,6 @@
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
// 侧边栏用户状态区域
|
||||
.chat-sidebar-user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border-top: 1px solid var(--color-border-2);
|
||||
background: var(--color-bg-1);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-bg-2);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-sidebar-user-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-sidebar-user-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
margin-bottom: 2px;
|
||||
@include text-truncate;
|
||||
}
|
||||
|
||||
.chat-sidebar-user-role {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
@include text-truncate;
|
||||
}
|
||||
|
||||
// 侧边栏项目切换区域
|
||||
.chat-sidebar-project {
|
||||
padding: 16px;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
@use '../tokens' as *;
|
||||
|
||||
.home-layout {
|
||||
min-height: 100vh;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(180deg, #F8FAFC 0%, #FFFFFF 100%);
|
||||
@@ -33,116 +33,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.home-header {
|
||||
padding: 0 48px;
|
||||
height: 68px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.home-logo {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: var(--color-text-1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
letter-spacing: -0.3px;
|
||||
|
||||
.sidebar-logo-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: linear-gradient(135deg, #3B82F6 0%, #8B5CF6 100%);
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 14px;
|
||||
height: 10px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
border-radius: 4px 4px 2px 2px;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 10px;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
border-radius: 2px 2px 4px 4px;
|
||||
bottom: 4px;
|
||||
}
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background: rgba(59, 130, 246, 0.9);
|
||||
border-radius: 50%;
|
||||
top: 9px;
|
||||
z-index: 1;
|
||||
|
||||
&:nth-child(1) { left: 9px; }
|
||||
&:nth-child(2) { right: 9px; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.home-nav {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
|
||||
a {
|
||||
color: var(--color-text-2);
|
||||
text-decoration: none;
|
||||
padding: 9px 16px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-1);
|
||||
background: var(--color-bg-2);
|
||||
}
|
||||
|
||||
svg, .home-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.home-nav-login {
|
||||
color: #3B82F6;
|
||||
font-weight: 600;
|
||||
|
||||
&:hover {
|
||||
color: #2563EB;
|
||||
background: #EFF6FF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.home-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -300,22 +190,8 @@
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.home-footer {
|
||||
padding: 28px;
|
||||
text-align: center;
|
||||
color: var(--color-text-4);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@include mobile {
|
||||
.home-header {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.home-title {
|
||||
font-size: 36px;
|
||||
}
|
||||
@@ -338,8 +214,4 @@
|
||||
.home-features {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.home-nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user