重新构建路由和配置样式文件

This commit is contained in:
2025-03-26 10:04:27 +08:00
parent a42a9990bf
commit 97ccf5a077
141 changed files with 88034 additions and 179 deletions
+66
View File
@@ -0,0 +1,66 @@
import { Link, useMatches } from '@remix-run/react';
interface BreadcrumbItem {
title: string;
to?: string;
}
interface BreadcrumbProps {
items?: BreadcrumbItem[];
className?: string;
}
interface Handle {
breadcrumb: string | ((data: any) => string);
}
interface Match {
handle?: Handle;
pathname: string;
data: any;
}
export function Breadcrumb({ items = [], className = '' }: BreadcrumbProps) {
const matches = useMatches() as Match[];
const breadcrumbs = items.length > 0 ? items : matches
.filter(match => match.handle?.breadcrumb)
.map(match => ({
title: typeof match.handle?.breadcrumb === 'function'
? match.handle.breadcrumb(match.data)
: match.handle?.breadcrumb,
to: match.pathname
}));
if (breadcrumbs.length === 0) {
return null;
}
return (
<nav className={`mb-4 ${className}`} aria-label="面包屑导航">
<ol className="flex items-center space-x-2 text-sm text-gray-500">
<li>
<Link to="/" className="hover:text-primary-600 flex items-center">
<i className="ri-home-line mr-1" />
</Link>
</li>
{breadcrumbs.map((item, index) => (
<li key={index} className="flex items-center">
<i className="ri-arrow-right-s-line mx-1 text-gray-400" />
{index === breadcrumbs.length - 1 ? (
<span className="text-gray-900 font-medium">{item.title}</span>
) : (
<Link
to={item.to || '#'}
className="hover:text-primary-600"
>
{item.title}
</Link>
)}
</li>
))}
</ol>
</nav>
);
}
+59
View File
@@ -0,0 +1,59 @@
import React from 'react';
import { Form } from '@remix-run/react';
interface HeaderProps {
username: string;
}
export function Header({ username }: HeaderProps) {
return (
<header className="bg-white shadow-sm py-3 px-6 flex justify-between items-center">
<div className="flex items-center">
<div className="relative">
<input
type="text"
placeholder="搜索..."
className="form-input pl-9 pr-3 py-1.5 w-64"
/>
<i className="ri-search-line absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
</div>
</div>
<div className="flex items-center space-x-4">
<div className="relative">
<button className="flex items-center justify-center w-10 h-10 rounded-full hover:bg-gray-100">
<i className="ri-notification-3-line text-xl text-gray-600"></i>
<span className="absolute top-1 right-1 w-4 h-4 bg-red-500 text-white text-xs flex items-center justify-center rounded-full">3</span>
</button>
</div>
<div className="relative group">
<button className="flex items-center space-x-2 focus:outline-none">
<div className="w-8 h-8 rounded-full bg-primary text-white flex items-center justify-center font-medium">
{username.charAt(0)}
</div>
<span className="text-sm font-medium">{username}</span>
<i className="ri-arrow-down-s-line text-gray-400"></i>
</button>
<div className="absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white hidden group-hover:block z-10">
<a href="#" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i className="ri-user-line mr-2"></i>
</a>
<a href="#" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<i className="ri-settings-3-line mr-2"></i>
</a>
<Form action="/auth/logout" method="post">
<button
type="submit"
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
<i className="ri-logout-box-line mr-2"></i> 退
</button>
</Form>
</div>
</div>
</div>
</header>
);
}
+43
View File
@@ -0,0 +1,43 @@
import React, { useState, useEffect } from 'react';
import { Sidebar } from './Sidebar';
import { Header } from './Header';
import { Breadcrumb } from './Breadcrumb';
interface LayoutProps {
children: React.ReactNode;
}
export function Layout({ children }: LayoutProps) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
// 从本地存储中获取侧边栏状态
useEffect(() => {
const savedState = localStorage.getItem('sidebarCollapsed');
if (savedState) {
setSidebarCollapsed(savedState === 'true');
}
}, []);
const toggleSidebar = () => {
const newState = !sidebarCollapsed;
setSidebarCollapsed(newState);
localStorage.setItem('sidebarCollapsed', String(newState));
};
return (
<div className="layout-container">
<Sidebar
collapsed={sidebarCollapsed}
onToggle={toggleSidebar}
/>
<div className={`main-content ${sidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
{/* <Header username="系统管理员" /> */}
<div className="content-container">
<Breadcrumb className="px-6 pt-4" />
{children}
</div>
</div>
</div>
);
}
+237
View File
@@ -0,0 +1,237 @@
import { useState, useEffect } from 'react';
import { Link, useLocation } from '@remix-run/react';
interface MenuItem {
id: string;
title: string;
path: string;
icon: string;
children?: MenuItem[];
}
interface SidebarProps {
onToggle: () => void;
collapsed: boolean;
}
export function Sidebar({ onToggle, collapsed }: SidebarProps) {
const location = useLocation();
const [expandedMenus, setExpandedMenus] = useState<Record<string, boolean>>({});
const menuItems: MenuItem[] = [
{
id: 'home',
title: '首页',
path: '/',
icon: 'ri-home-line'
},
{
id: 'file-management',
title: '文件管理',
path: '/files',
icon: 'ri-folder-line',
children: [
{
id: 'file-upload',
title: '文件上传',
path: '/files/new',
icon: 'ri-upload-cloud-line'
},
{
id: 'file-list',
title: '文件列表',
path: '/files',
icon: 'ri-file-list-3-line'
}
]
},
{
id: 'rule-management',
title: '评查规则库',
path: '/rules',
icon: 'ri-book-3-line',
children: [
{
id: 'rule-groups',
title: '评查点分组',
path: '/rule-groups',
icon: 'ri-folder-open-line'
},
{
id: 'rule-list',
title: '评查点列表',
path: '/rules',
icon: 'ri-list-check-3'
},
// {
// id: 'rule-new',
// title: '新增评查点',
// path: '/rules/new',
// icon: 'ri-add-circle-line'
// }
]
},
{
id: 'review-management',
title: '评查结果',
path: '/reviews',
icon: 'ri-bar-chart-box-line',
children: [
{
id: 'review-detail',
title: '评查详情',
path: '/reviews',
icon: 'ri-file-chart-line'
}
]
},
{
id: 'system-settings',
title: '系统设置',
path: '/settings',
icon: 'ri-settings-4-line',
children: [
{
id: 'basic-settings',
title: '基础设置',
path: '/settings',
icon: 'ri-equalizer-line'
},
{
id: 'document-types',
title: '文档类型',
path: '/doc-types',
icon: 'ri-file-list-line'
},
{
id: 'prompt-management',
title: '提示词管理',
path: '/prompts',
icon: 'ri-chat-1-line'
}
]
}
];
// 初始化展开状态,默认全部展开
useEffect(() => {
const initialExpandedState: Record<string, boolean> = {};
menuItems.forEach(item => {
if (item.children) {
initialExpandedState[item.id] = true;
}
});
setExpandedMenus(initialExpandedState);
}, []);
const toggleMenu = (id: string, e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
setExpandedMenus(prev => ({
...prev,
[id]: !prev[id]
}));
};
const isActive = (path: string) => {
return location.pathname === path || location.pathname.startsWith(`${path}/`);
};
// 处理侧边栏切换事件
const handleToggleSidebar = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
onToggle();
};
return (
<div className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
<div className="py-6 px-4 border-b border-gray-100 flex justify-between items-center">
<div className="flex items-center">
<i className="ri-file-search-line text-primary text-xl mr-2"></i>
{!collapsed && <h2 className="text-lg font-medium">AI审核系统</h2>}
</div>
<button
className="sidebar-toggle"
onClick={handleToggleSidebar}
aria-label={collapsed ? "展开侧边栏" : "折叠侧边栏"}
type="button"
>
<i className={`${collapsed ? 'ri-menu-unfold-line' : 'ri-menu-fold-line'}`}></i>
</button>
</div>
{!collapsed && (
<div className="user-profile p-4 border-b border-gray-100 flex items-center">
<div className="avatar w-10 h-10 rounded-full bg-primary text-white flex items-center justify-center">
<span></span>
</div>
<div className="ml-3">
<p className="text-sm font-medium"></p>
<p className="text-xs text-gray-500"></p>
</div>
</div>
)}
<div className="py-4 px-[10px]">
{menuItems.map((item) => (
<div key={item.id} className={`${collapsed ? 'px-0' : ''}`}>
{!item.children ? (
<Link
to={item.path}
className={`sidebar-menu-item ${isActive(item.path) ? 'active' : ''} flex items-center ${collapsed ? 'justify-center' : ''}`}
>
<i className={`${item.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
{!collapsed && <span>{item.title}</span>}
</Link>
) : (
<>
<div
className={`sidebar-menu-item flex items-center ${collapsed ? 'justify-center' : 'justify-between'} cursor-pointer`}
onClick={(e) => toggleMenu(item.id, e)}
role="button"
tabIndex={0}
aria-expanded={expandedMenus[item.id] || false}
aria-controls={`submenu-${item.id}`}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleMenu(item.id, e as unknown as React.MouseEvent);
}
}}
>
<div className="flex items-center">
<i className={`${item.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
{!collapsed && <span>{item.title}</span>}
</div>
{!collapsed && (
<i className={`ri-arrow-${expandedMenus[item.id] ? 'down' : 'right'}-s-line`}></i>
)}
</div>
{(expandedMenus[item.id] || collapsed) && (
<div
className={`${collapsed ? 'border-l-0 pl-0' : 'border-l border-gray-100 ml-4 pl-3'}`}
id={`submenu-${item.id}`}
>
{item.children.map((child) => (
<Link
key={child.id}
to={child.path}
className={`sidebar-menu-item ${isActive(child.path) ? 'active' : ''} flex items-center ${collapsed ? 'justify-center' : ''}`}
>
<i className={`${child.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
{!collapsed && <span>{child.title}</span>}
</Link>
))}
</div>
)}
</>
)}
</div>
))}
</div>
</div>
);
}