feat: 统一全局 Header 结构

- 新增 AppHeader 组件(Logo + 台入口 + 用户状态)
- 新增 UserDropdown 组件(用户下拉菜单)
- 新增 AppLayout 布局组件
- 移除 SidebarBrand 和 SidebarUser 组件
- 修改各台页面,移除侧边栏中的品牌区和用户区
- 修改 HomePage,移除独立 header/footer
- 修改 Layout 组件,简化为 sidebar + content
- 账户设置改为弹框形式,不中断用户操作
- 更新 README.md 布局系统说明
- 同步 delta specs 到主 specs
This commit is contained in:
2026-03-27 12:27:38 +08:00
parent ce9ebe5784
commit 7f493aa921
22 changed files with 793 additions and 608 deletions

View File

@@ -96,8 +96,8 @@ pnpm build # 验证打包不运行pnpm dev会挂起流程
src/
├── components/ # 组件库
│ ├── common/ # 通用组件 (Modal, Toast, EmptyState等)
│ ├── layout/ # 布局组件 (SidebarBrand, SidebarUser等)
│ ├── Layout.jsx # 主布局组件
│ ├── layout/ # 布局组件 (AppHeader, AppLayout, UserDropdown等)
│ ├── Layout.jsx # 主布局组件sidebar + content
│ └── ListSelector.jsx # 列表选择器
├── contexts/ # 全局状态 (UserContext)
@@ -264,14 +264,15 @@ export default Example;
| 组件 | 路径 | 用途 |
|------|------|------|
| Layout | `components/Layout.jsx` | 主布局sidebar+header+main |
| Layout | `components/Layout.jsx` | 主布局sidebar + content |
| AppLayout | `components/layout/AppLayout.jsx` | 全局布局header + main |
| AppHeader | `components/layout/AppHeader.jsx` | 统一导航头部 |
| UserDropdown | `components/layout/UserDropdown.jsx` | 用户下拉菜单 |
| Modal | `components/common/Modal.jsx` | 确认弹窗 |
| Toast | `components/common/Toast.jsx` | 消息提示 |
| EmptyState | `components/common/EmptyState.jsx` | 空状态展示 |
| SearchBar | `components/common/SearchBar.jsx` | 搜索框 |
| StatusBadge | `components/common/StatusBadge.jsx` | 状态标签 |
| SidebarBrand | `components/layout/SidebarBrand.jsx` | 侧边栏品牌 |
| SidebarUser | `components/layout/SidebarUser.jsx` | 侧边栏用户信息 |
| SidebarNavItem | `components/layout/SidebarNavItem.jsx` | 侧边栏导航项 |
---
@@ -284,14 +285,21 @@ export default Example;
// App.jsx
<HashRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/console" element={<ConsolePage />} />
<Route path="/admin" element={<AdminPage />} />
<Route path="/developer" element={<DeveloperPage />} />
<Route path="/login" element={<LoginPage />} />
<Route element={<AppLayout />}>
<Route path="/" element={<HomePage />} />
<Route path="/console" element={<ConsolePage />} />
<Route path="/admin" element={<AdminPage />} />
<Route path="/developer" element={<DeveloperPage />} />
</Route>
</Routes>
</HashRouter>
```
**说明**
- `AppLayout` 包裹所有需要统一 Header 的页面
- 登录页独立,不使用 `AppLayout`
### 子页面路由
每个主页面内部管理子页面:
@@ -425,4 +433,4 @@ api.logs.filter({ user, type, status });
---
*最后更新2026-03-26*
*最后更新2026-03-27*

View File

@@ -1,15 +1,25 @@
## ADDED Requirements
## Purpose
布局系统提供应用级外壳布局、聊天页面布局、管理台布局等核心页面骨架。
## Requirements
### Requirement: 应用外壳布局
布局系统 SHALL 提供应用级外壳布局,包含侧边栏、顶部栏、主内容区。
布局系统 SHALL 提供应用级外壳布局,包含全局头部、可选侧边栏、主内容区。
#### Scenario: 基础应用布局
- **WHEN** 开发者需要管理控制台布局
- **THEN** 系统 SHALL 提供 `.app-shell` 类,包含 `.app-shell__sidebar``.app-shell__header``.app-shell__main` 区域
- **THEN** 系统 SHALL 提供 `.app-layout` 类,包含 `.app-layout__header``.app-layout__sidebar`(可选)`.app-layout__main` 区域
#### Scenario: 侧边栏结构
- **WHEN** 侧边栏需要品牌区、导航区、用户区
- **THEN** 系统 SHALL 提供 `.sidebar__brand``.sidebar__nav``.sidebar__user` 元素类
#### Scenario: 全局头部
- **WHEN** 页面需要统一导航
- **THEN** 系统 SHALL `.app-layout__header` 区域渲染 `AppHeader` 组件
- **AND** 头部固定在页面顶部
#### Scenario: 侧边栏可选
- **WHEN** 页面需要侧边栏(如工作台、开发台、管理台)
- **THEN** 系统 SHALL 渲染 `.app-layout__sidebar` 区域
- **WHEN** 页面不需要侧边栏(如主页)
- **THEN** 系统 SHALL 不渲染侧边栏区域
#### Scenario: 导航项
- **WHEN** 侧边栏需要导航菜单
@@ -27,16 +37,13 @@
- **WHEN** 导航项需要文本和额外信息
- **THEN** 系统 SHALL 提供 `.nav-item__text``.nav-item__meta` 元素类
#### Scenario: 顶部栏结构
- **WHEN** 顶部栏需要左侧标题区和右侧操作区
- **THEN** 系统 SHALL 提供 `.header__left``.header__right` 元素类
### Requirement: 聊天页面布局
布局系统 SHALL 提供聊天页面专用布局。
布局系统 SHALL 提供聊天页面专用布局,侧边栏不包含品牌区和用户区
#### Scenario: 聊天布局容器
- **WHEN** 开发者需要聊天界面布局
- **THEN** 系统 SHALL 提供 `.chat-layout` 类,包含 `.chat-layout__header``.chat-layout__sidebar``.chat-layout__content`
- **THEN** 系统 SHALL 提供 `.chat-layout` 类,包含 `.chat-layout__sidebar``.chat-layout__content`
- **AND** 侧边栏不包含品牌区(`.sidebar-brand`
#### Scenario: 会话列表
- **WHEN** 侧边栏需要展示会话列表
@@ -46,12 +53,23 @@
- **WHEN** 需要展示消息和输入区
- **THEN** 系统 SHALL 提供 `.chat-content__messages``.chat-content__input` 区域
#### Scenario: 侧边栏不含用户区
- **WHEN** 聊天页面侧边栏渲染时
- **THEN** 系统 SHALL 不显示用户状态区域
- **AND** 用户状态统一在全局头部显示
### Requirement: 管理台布局
布局系统 SHALL 提供管理台页面布局。
布局系统 SHALL 提供管理台页面布局,侧边栏不包含品牌区和用户区
#### Scenario: 管理台侧边栏
- **WHEN** 管理台需要独立导航结构
- **THEN** 系统 SHALL 提供 `.admin-layout` 类,包含 `.admin-layout__sidebar``.admin-layout__content`
- **AND** 侧边栏不包含品牌区(`.admin-sidebar-header`)和用户区(`.admin-sidebar-user`
#### Scenario: 侧边栏不含用户区
- **WHEN** 管理台侧边栏渲染时
- **THEN** 系统 SHALL 不显示用户状态区域
- **AND** 用户状态统一在全局头部显示
### Requirement: 页面内容区
布局系统 SHALL 提供标准化的页面内容容器。

View File

@@ -0,0 +1,111 @@
## Purpose
统一的全局导航头部,包含品牌标识、台入口切换和用户状态管理。
## Requirements
### Requirement: 统一导航头部
系统 SHALL 提供统一的全局导航头部组件,包含品牌标识、台入口切换和用户状态。
#### Scenario: 头部基础结构
- **WHEN** 页面需要显示导航头部
- **THEN** 系统 SHALL 提供 `AppHeader` 组件
- **AND** 组件包含左侧品牌区和右侧功能区
#### Scenario: 品牌区展示
- **WHEN** 用户查看头部左侧
- **THEN** 系统 SHALL 显示 Logo 图标和 "GrandClaw" 标题
- **AND** Logo 使用 `.sidebar-logo-icon` 样式类
### Requirement: 台入口切换
系统 SHALL 在头部提供三个台的入口导航,支持切换确认和高亮状态。
#### Scenario: 台入口展示
- **WHEN** 用户查看头部右侧
- **THEN** 系统 SHALL 显示"工作台"、"开发台"、"管理台"三个入口
- **AND** 每个入口包含图标和文字
#### Scenario: 当前台高亮
- **WHEN** 用户在某个台内(如工作台)
- **THEN** 系统 SHALL 为当前台入口添加高亮样式
- **AND** 其他台入口保持默认样式
#### Scenario: 台切换确认
- **WHEN** 用户点击非当前台的入口
- **THEN** 系统 SHALL 显示确认对话框
- **AND** 对话框提示"切换到[台名称]"
- **AND** 用户确认后执行跳转
#### Scenario: 台入口路由
- **WHEN** 用户确认切换
- **THEN** 系统 SHALL 跳转到对应的路由
- **AND** 工作台路由为 `/console`
- **AND** 开发台路由为 `/developer`
- **AND** 管理台路由为 `/admin`
### Requirement: 用户状态区域
系统 SHALL 在头部右侧显示当前用户状态,支持已登录和未登录两种状态。
#### Scenario: 已登录状态展示
- **WHEN** 用户已登录
- **THEN** 系统 SHALL 显示用户头像、用户名和下拉图标
- **AND** 头像使用 `user-avatar` 样式类
#### Scenario: 未登录状态展示
- **WHEN** 用户未登录
- **THEN** 系统 SHALL 显示"登录"按钮
- **AND** 点击后跳转到登录页
### Requirement: 用户下拉菜单
系统 SHALL 提供用户下拉菜单,包含账户设置和退出登录选项。
#### Scenario: 下拉菜单展开
- **WHEN** 用户点击用户状态区域
- **THEN** 系统 SHALL 展开下拉菜单
- **AND** 菜单包含"账户设置"和"退出登录"选项
#### Scenario: 账户设置入口
- **WHEN** 用户点击"账户设置"
- **THEN** 系统 SHALL 打开账户设置弹框
- **AND** 下拉菜单收起
- **AND** 弹框宽度为 720px
- **AND** 弹框内按钮靠右对齐
#### Scenario: 退出登录
- **WHEN** 用户点击"退出登录"
- **THEN** 系统 SHALL 执行退出操作
- **AND** 跳转到登录页
#### Scenario: 点击外部关闭
- **WHEN** 下拉菜单展开时
- **AND** 用户点击菜单外部区域
- **THEN** 系统 SHALL 收起下拉菜单
### Requirement: 头部移动端适配
系统 SHALL 在移动端适配头部布局,将台入口收起到汉堡菜单。
#### Scenario: 移动端台入口收起
- **WHEN** 屏幕宽度小于 768px
- **THEN** 系统 SHALL 隐藏台入口的直接显示
- **AND** 显示汉堡菜单按钮
#### Scenario: 移动端菜单展开
- **WHEN** 用户点击汉堡菜单按钮
- **THEN** 系统 SHALL 展开移动端菜单
- **AND** 菜单包含三个台入口选项
#### Scenario: 移动端用户状态保留
- **WHEN** 屏幕宽度小于 768px
- **THEN** 系统 SHALL 保持用户状态区域可见
- **AND** 用户状态不收起到汉堡菜单中
### Requirement: 头部布局路由控制
系统 SHALL 通过路由配置控制头部的显示与隐藏。
#### Scenario: 需要 Header 的页面
- **WHEN** 用户访问主页、工作台、开发台、管理台
- **THEN** 系统 SHALL 显示统一头部
#### Scenario: 不需要 Header 的页面
- **WHEN** 用户访问登录页
- **THEN** 系统 SHALL 不显示头部
- **AND** 页面使用独立布局

View File

@@ -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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>
);

View 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;

View 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;

View File

@@ -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;

View File

@@ -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;

View 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;

View File

@@ -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()}

View File

@@ -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' : ''}
>

View File

@@ -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()}

View File

@@ -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>
);
}

View File

@@ -16,6 +16,7 @@
@forward 'password-input';
@forward 'search-bar';
@forward 'stat-card';
@forward 'header';
.page-back-btn {
display: inline-flex;

View 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;
}
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}
}