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

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