完成评查点分组列表和评查点列表的页面,封装部分组件,重新构造样式文件结构

This commit is contained in:
2025-03-26 18:39:42 +08:00
parent 97ccf5a077
commit d9b9ce4676
34 changed files with 3281 additions and 3777 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ export function Layout({ children }: LayoutProps) {
<div className={`main-content ${sidebarCollapsed ? 'sidebar-collapsed' : ''}`}>
{/* <Header username="系统管理员" /> */}
<div className="content-container">
<Breadcrumb className="px-6 pt-4" />
<Breadcrumb />
{children}
</div>
</div>
+35 -10
View File
@@ -58,17 +58,23 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
icon: 'ri-folder-open-line'
},
{
id: 'rule-list',
id: 'rules-list',
title: '评查点列表',
path: '/rules',
icon: 'ri-list-check-3'
},
// {
// id: 'rule-new',
// title: '新增评查点',
// path: '/rules/new',
// icon: 'ri-add-circle-line'
// }
{
id: 'rules-file',
title: '评查文件列表',
path: '/rules/files',
icon: 'ri-list-check-2'
},
{
id: 'rule-new',
title: '新增评查点',
path: '/rules/new',
icon: 'ri-add-circle-line'
}
]
},
{
@@ -125,9 +131,12 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
}, []);
const toggleMenu = (id: string, e: React.MouseEvent) => {
// 防止事件冒泡和默认行为
e.preventDefault();
e.stopPropagation();
// console.log('%c父菜单展开/折叠 ===> ', 'background: #f5222d; color: white; padding: 2px 4px; border-radius: 2px;', id);
setExpandedMenus(prev => ({
...prev,
[id]: !prev[id]
@@ -140,11 +149,19 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
// 处理侧边栏切换事件
const handleToggleSidebar = (e: React.MouseEvent) => {
// console.log('%c侧边栏折叠/展开 ===> ', 'background: #1890ff; color: white; padding: 2px 4px; border-radius: 2px;');
e.preventDefault();
e.stopPropagation();
onToggle();
};
// 处理子菜单项点击事件
const handleSubMenuClick = (child: MenuItem, e: React.MouseEvent) => {
// 需要阻止冒泡,否则会触发父级菜单的展开/折叠事件
e.stopPropagation();
// console.log('%c子菜单点击 ===> ', 'background: #00684a; color: white; padding: 2px 4px; border-radius: 2px;', child.title, '路径:', child.path);
};
return (
<div className={`sidebar ${collapsed ? 'collapsed' : ''}`}>
<div className="py-6 px-4 border-b border-gray-100 flex justify-between items-center">
@@ -181,6 +198,10 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
<Link
to={item.path}
className={`sidebar-menu-item ${isActive(item.path) ? 'active' : ''} flex items-center ${collapsed ? 'justify-center' : ''}`}
onClick={(e) => {
e.stopPropagation();
// console.log('%c单级菜单点击 ===> ', 'background: #52c41a; color: white; padding: 2px 4px; border-radius: 2px;', item.title, '路径:', item.path);
}}
>
<i className={`${item.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
{!collapsed && <span>{item.title}</span>}
@@ -188,8 +209,11 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
) : (
<>
<div
className={`sidebar-menu-item flex items-center ${collapsed ? 'justify-center' : 'justify-between'} cursor-pointer`}
onClick={(e) => toggleMenu(item.id, e)}
className={`sidebar-menu-item flex items-center ${collapsed ? 'justify-center' : 'justify-between'} cursor-pointer z-10`}
onClick={(e) => {
// console.log('%c父菜单点击 ===> ', 'background: #722ed1; color: white; padding: 2px 4px; border-radius: 2px;', item.title);
toggleMenu(item.id, e);
}}
role="button"
tabIndex={0}
aria-expanded={expandedMenus[item.id] || false}
@@ -212,7 +236,7 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
{(expandedMenus[item.id] || collapsed) && (
<div
className={`${collapsed ? 'border-l-0 pl-0' : 'border-l border-gray-100 ml-4 pl-3'}`}
className={`submenu-container ${collapsed ? 'border-l-0 pl-0' : 'border-l border-gray-100 ml-4 pl-3'} z-20`}
id={`submenu-${item.id}`}
>
{item.children.map((child) => (
@@ -220,6 +244,7 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
key={child.id}
to={child.path}
className={`sidebar-menu-item ${isActive(child.path) ? 'active' : ''} flex items-center ${collapsed ? 'justify-center' : ''}`}
onClick={(e) => handleSubMenuClick(child, e)}
>
<i className={`${child.icon} ${collapsed ? 'text-xl' : 'mr-3'}`}></i>
{!collapsed && <span>{child.title}</span>}
+72
View File
@@ -0,0 +1,72 @@
// app/components/ui/Modal.tsx
import React, { useEffect } from 'react';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: React.ReactNode;
children: React.ReactNode;
footer?: React.ReactNode;
width?: number | string;
}
export function Modal({ isOpen, onClose, title, children, footer, width = 500 }: ModalProps) {
// 点击ESC键关闭模态框
useEffect(() => {
const handleEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
onClose();
}
};
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}, [isOpen, onClose]);
// 禁用背景滚动
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
if (!isOpen) return null;
return (
<div
className="modal-backdrop"
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div
className="modal-content"
style={{ maxWidth: typeof width === 'number' ? `${width}px` : width }}
onClick={e => e.stopPropagation()}
>
<div className="modal-header">
<h3 className="text-lg font-medium">{title}</h3>
<button
className="text-gray-400 hover:text-gray-500"
onClick={onClose}
aria-label="关闭"
>
<i className="ri-close-line"></i>
</button>
</div>
<div className="modal-body py-4">
{children}
</div>
{footer && (
<div className="modal-footer flex justify-end space-x-2 pt-4 border-t border-gray-100">
{footer}
</div>
)}
</div>
</div>
);
}
+103
View File
@@ -0,0 +1,103 @@
// app/components/ui/Pagination.tsx
import React from 'react';
interface PaginationProps {
currentPage: number;
total: number;
pageSize: number;
onChange: (page: number) => void;
onPageSizeChange?: (pageSize: number) => void;
pageSizeOptions?: number[];
}
export function Pagination({
currentPage,
total,
pageSize,
onChange,
onPageSizeChange,
pageSizeOptions = [10, 20, 50]
}: PaginationProps) {
const totalPages = Math.ceil(total / pageSize);
// 生成页码数组
const getPageNumbers = () => {
const pages = [];
const maxVisiblePages = 5;
if (totalPages <= maxVisiblePages) {
// 总页数小于等于最大可见页数,显示所有页码
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else if (currentPage <= 3) {
// 当前页靠近开始
for (let i = 1; i <= maxVisiblePages; i++) {
pages.push(i);
}
} else if (currentPage >= totalPages - 2) {
// 当前页靠近结尾
for (let i = totalPages - maxVisiblePages + 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
// 当前页在中间
for (let i = currentPage - 2; i <= currentPage + 2; i++) {
pages.push(i);
}
}
return pages;
};
return (
<div className="flex justify-between items-center p-4">
{onPageSizeChange && (
<div className="ant-pagination-options">
<span className="text-sm mr-2"> {total} </span>
<select
className="form-select ant-pagination-options-size-changer"
value={pageSize}
onChange={(e) => onPageSizeChange(Number(e.target.value))}
>
{pageSizeOptions.map(size => (
<option key={size} value={size}>{size} /</option>
))}
</select>
</div>
)}
<div className="ant-pagination">
<button
className={`ant-pagination-item ant-pagination-prev ${currentPage <= 1 ? 'ant-pagination-disabled' : ''}`}
onClick={() => currentPage > 1 && onChange(currentPage - 1)}
disabled={currentPage <= 1}
aria-label="上一页"
>
<i className="ri-arrow-left-s-line"></i>
</button>
{getPageNumbers().map(page => (
<button
key={page}
className={`ant-pagination-item ${page === currentPage ? 'ant-pagination-item-active' : ''}`}
onClick={() => onChange(page)}
aria-label={`${page}`}
aria-current={page === currentPage ? 'page' : undefined}
>
{page}
</button>
))}
<button
className={`ant-pagination-item ant-pagination-next ${currentPage >= totalPages ? 'ant-pagination-disabled' : ''}`}
onClick={() => currentPage < totalPages && onChange(currentPage + 1)}
disabled={currentPage >= totalPages}
aria-label="下一页"
>
<i className="ri-arrow-right-s-line"></i>
</button>
</div>
</div>
);
}
+53
View File
@@ -0,0 +1,53 @@
import React from 'react';
import { Button } from './Button';
interface SearchBoxProps {
placeholder?: string;
defaultValue?: string;
onSearch: (value: string) => void;
buttonText?: string;
className?: string;
name?: string;
}
export function SearchBox({
placeholder = '请输入搜索内容',
defaultValue = '',
onSearch,
buttonText = '搜索',
className = '',
name = 'keyword'
}: SearchBoxProps) {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const value = formData.get(name) as string;
onSearch(value);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// 对于没有按钮的输入框,我们希望在输入时就触发搜索
if (className.includes('form-input-only')) {
onSearch(e.target.value);
}
};
return (
<form onSubmit={handleSubmit} className={`search-box ${className}`}>
<input
type="text"
id={name}
name={name}
className={className.includes('form-input-only') ? "form-input" : "form-input rounded-r-none"}
placeholder={placeholder}
defaultValue={defaultValue}
onChange={handleChange}
/>
{buttonText && !className.includes('form-input-only') && (
<Button type="primary" icon="ri-search-line" className="rounded-l-none">
{buttonText}
</Button>
)}
</form>
);
}
+33
View File
@@ -0,0 +1,33 @@
type StatusType = 'success' | 'error' | 'warning' | 'default' | 'processing';
interface StatusDotProps {
status: StatusType | boolean;
text?: string;
className?: string;
}
export function StatusDot({
status,
text,
className = ''
}: StatusDotProps) {
// 如果status是布尔值,则转换为对应的状态类型
const statusType = typeof status === 'boolean'
? (status ? 'success' : 'default')
: status;
// 如果没有提供文本,则根据状态类型提供默认文本
const statusText = text ?? (
statusType === 'success' ? '启用' :
statusType === 'error' ? '禁用' :
statusType === 'warning' ? '警告' :
statusType === 'processing' ? '处理中' : '未知'
);
return (
<span className={`inline-flex items-center ${className}`}>
<i className={`status-dot status-dot-${statusType}`}></i>
{statusText}
</span>
);
}
+3 -46
View File
@@ -3,58 +3,15 @@ import React from 'react';
export type TagColor = 'blue' | 'green' | 'cyan' | 'purple' | 'orange' | 'red' | 'default';
interface TagProps {
children: React.ReactNode;
color?: TagColor;
closable?: boolean;
onClose?: (e: React.MouseEvent<HTMLElement>) => void;
children: React.ReactNode;
className?: string;
}
export function Tag({
children,
color = 'default',
closable = false,
onClose,
className = '',
}: TagProps) {
const baseClasses = 'ant-tag';
const colorClasses = {
default: '',
blue: 'ant-tag-blue',
green: 'ant-tag-green',
cyan: 'ant-tag-cyan',
purple: 'ant-tag-purple',
orange: 'ant-tag-orange',
red: 'ant-tag-red'
};
const classes = [
baseClasses,
colorClasses[color],
className
].filter(Boolean).join(' ');
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
onClose?.(e as unknown as React.MouseEvent<HTMLElement>);
}
};
export function Tag({ color = 'default', children, className = '' }: TagProps) {
return (
<span className={classes}>
<span className={`ant-tag ant-tag-${color} ${className}`}>
{children}
{closable && (
<i
className="ri-close-line ml-1 cursor-pointer text-opacity-60 hover:text-opacity-100"
onClick={onClose}
onKeyDown={handleKeyDown}
role="button"
tabIndex={0}
aria-label="关闭标签"
></i>
)}
</span>
);
}