Skip to content

Commit 4bce7f5

Browse files
committed
refactor(frontend): 创建独立列表组件,并且初步在一些页面中实验性使用
创建独立列表组件,并且初步在一些页面中实验性使用
1 parent abbc084 commit 4bce7f5

6 files changed

Lines changed: 633 additions & 1127 deletions

File tree

frontend/src/components/dashboard/ArticlesView.jsx

Lines changed: 81 additions & 381 deletions
Large diffs are not rendered by default.

frontend/src/components/dashboard/CommentsReview.jsx

Lines changed: 73 additions & 272 deletions
Large diffs are not rendered by default.

frontend/src/components/dashboard/CommentsView.jsx

Lines changed: 120 additions & 395 deletions
Large diffs are not rendered by default.
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { Search } from 'lucide-react';
2+
import Pagination from './Pagination';
3+
4+
export default function DataTable({
5+
title,
6+
columns = [],
7+
data = [],
8+
keyExtractor = (item, index) => item.id ?? index,
9+
loading = false,
10+
error = null,
11+
emptyText = '暂无数据',
12+
13+
headerExtra = null,
14+
headerActions = null,
15+
16+
searchValue = '',
17+
onSearchChange = null,
18+
onSearchClear = null,
19+
searchPlaceholder = '搜索...',
20+
21+
pagination = null,
22+
23+
onRowClick = null,
24+
}) {
25+
const renderSearch = () => {
26+
if (!onSearchChange) return null;
27+
28+
return (
29+
<div className="mb-6">
30+
<div className="flex items-center gap-3">
31+
<div className="relative flex-1 max-w-md">
32+
<input
33+
type="text"
34+
placeholder={searchPlaceholder}
35+
value={searchValue}
36+
onChange={onSearchChange}
37+
className="w-full pl-10 pr-4 py-2.5 bg-gray-50 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200 text-sm"
38+
/>
39+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
40+
</div>
41+
{searchValue && onSearchClear && (
42+
<button
43+
type="button"
44+
onClick={onSearchClear}
45+
className="px-5 py-2.5 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-all duration-200 text-sm font-medium"
46+
>
47+
清除
48+
</button>
49+
)}
50+
</div>
51+
</div>
52+
);
53+
};
54+
55+
if (loading) {
56+
return (
57+
<div className="bg-white rounded-lg shadow-sm p-6">
58+
<div className="flex items-center justify-between mb-6">
59+
<h2 className="text-xl font-semibold text-gray-900">{title}</h2>
60+
{headerActions && <div className="flex items-center space-x-2">{headerActions}</div>}
61+
</div>
62+
{headerExtra}
63+
{renderSearch()}
64+
<div className="flex justify-center items-center h-64">
65+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
66+
</div>
67+
</div>
68+
);
69+
}
70+
71+
return (
72+
<div className="bg-white rounded-lg shadow-sm p-6">
73+
<div className="flex items-center justify-between mb-6">
74+
<div>
75+
<h2 className="text-xl font-semibold text-gray-900">{title}</h2>
76+
{headerExtra}
77+
</div>
78+
{headerActions && <div className="flex items-center space-x-2">{headerActions}</div>}
79+
</div>
80+
81+
{error && (
82+
<div className="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
83+
<p className="text-red-700">{error}</p>
84+
</div>
85+
)}
86+
87+
{renderSearch()}
88+
89+
{data.length > 0 ? (
90+
<>
91+
<div className="overflow-x-auto">
92+
<table className="min-w-full divide-y divide-gray-200">
93+
<thead className="bg-gray-50">
94+
<tr>
95+
{columns.map((col, i) => (
96+
<th
97+
key={col.key ?? i}
98+
scope="col"
99+
className={`px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider ${col.headerClassName ?? ''}`}
100+
>
101+
{col.label}
102+
</th>
103+
))}
104+
</tr>
105+
</thead>
106+
<tbody className="bg-white divide-y divide-gray-200">
107+
{data.map((item, rowIndex) => {
108+
const rowKey = keyExtractor(item, rowIndex);
109+
return (
110+
<tr
111+
key={rowKey}
112+
className={`hover:bg-gray-50 ${onRowClick ? 'cursor-pointer' : ''}`}
113+
onClick={onRowClick ? () => onRowClick(item) : undefined}
114+
>
115+
{columns.map((col, colIndex) => {
116+
const cellValue = item[col.key];
117+
const rendered = col.render ? col.render(cellValue, item, rowIndex) : cellValue;
118+
return (
119+
<td
120+
key={`${rowKey}-${col.key ?? colIndex}`}
121+
className={`px-6 py-4 whitespace-nowrap text-sm ${col.className ?? 'text-gray-500'}`}
122+
>
123+
{rendered}
124+
</td>
125+
);
126+
})}
127+
</tr>
128+
);
129+
})}
130+
</tbody>
131+
</table>
132+
</div>
133+
134+
{pagination && (
135+
<Pagination
136+
currentPage={pagination.currentPage}
137+
totalPages={pagination.totalPages}
138+
totalItems={pagination.totalItems}
139+
pageSize={pagination.pageSize ?? 10}
140+
onPageChange={pagination.onPageChange}
141+
/>
142+
)}
143+
</>
144+
) : (
145+
<div className="text-center py-12">
146+
<p className="text-gray-500">{emptyText}</p>
147+
</div>
148+
)}
149+
</div>
150+
);
151+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { useState } from 'react';
2+
3+
export default function Pagination({ currentPage, totalPages, totalItems, pageSize = 10, onPageChange }) {
4+
const [jumpPage, setJumpPage] = useState('');
5+
6+
const handlePageChange = (newPage) => {
7+
if (newPage < 1 || newPage > totalPages) return;
8+
onPageChange(newPage);
9+
};
10+
11+
const handleJumpPage = (e) => {
12+
e.preventDefault();
13+
const pageNum = parseInt(jumpPage);
14+
if (!isNaN(pageNum) && pageNum >= 1 && pageNum <= totalPages) {
15+
handlePageChange(pageNum);
16+
setJumpPage('');
17+
}
18+
};
19+
20+
if (totalPages <= 1) return null;
21+
22+
const delta = 2;
23+
const range = [];
24+
for (let i = Math.max(2, currentPage - delta); i <= Math.min(totalPages - 1, currentPage + delta); i++) {
25+
range.push(i);
26+
}
27+
28+
if (currentPage - delta > 2) {
29+
range.unshift('...');
30+
}
31+
if (currentPage + delta < totalPages - 1) {
32+
range.push('...');
33+
}
34+
35+
const pageItems = [1, ...range, totalPages].filter(
36+
(page, index, self) => self.indexOf(page) === index
37+
);
38+
39+
return (
40+
<div className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6 mt-4">
41+
<div className="flex flex-1 justify-between sm:hidden">
42+
<button
43+
onClick={() => handlePageChange(currentPage - 1)}
44+
disabled={currentPage === 1}
45+
className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
46+
>
47+
上一页
48+
</button>
49+
<button
50+
onClick={() => handlePageChange(currentPage + 1)}
51+
disabled={currentPage === totalPages}
52+
className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 disabled:opacity-50"
53+
>
54+
下一页
55+
</button>
56+
</div>
57+
58+
<div className="hidden sm:flex sm:flex-1 sm:items-center sm:justify-between">
59+
<div>
60+
<p className="text-sm text-gray-700">
61+
{totalItems !== undefined ? (
62+
<>
63+
显示第 <span className="font-medium">{(currentPage - 1) * pageSize + 1}</span>{' '}
64+
<span className="font-medium">{Math.min(currentPage * pageSize, totalItems)}</span> 条结果,共{' '}
65+
<span className="font-medium">{totalItems}</span>
66+
</>
67+
) : (
68+
<>
69+
<span className="font-medium">{currentPage}</span> 页,共{' '}
70+
<span className="font-medium">{totalPages}</span>
71+
</>
72+
)}
73+
</p>
74+
</div>
75+
<div className="flex items-center gap-4">
76+
<nav className="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
77+
<button
78+
onClick={() => handlePageChange(1)}
79+
disabled={currentPage === 1}
80+
className="relative inline-flex items-center rounded-l-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50"
81+
>
82+
<span className="sr-only">首页</span>
83+
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
84+
<path fillRule="evenodd" d="M15.707 15.707a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 010 1.414zm-6 0a1 1 0 01-1.414 0l-5-5a1 1 0 010-1.414l5-5a1 1 0 011.414 1.414L5.414 10l4.293 4.293a1 1 0 010 1.414z" clipRule="evenodd" />
85+
</svg>
86+
</button>
87+
88+
<button
89+
onClick={() => handlePageChange(currentPage - 1)}
90+
disabled={currentPage === 1}
91+
className="relative inline-flex items-center px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50"
92+
>
93+
<span className="sr-only">上一页</span>
94+
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
95+
<path fillRule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clipRule="evenodd" />
96+
</svg>
97+
</button>
98+
99+
{pageItems.map((page, index) => (
100+
<button
101+
key={index}
102+
onClick={() => typeof page === 'number' && handlePageChange(page)}
103+
disabled={page === '...' || page === currentPage}
104+
className={`relative inline-flex items-center px-4 py-2 text-sm font-semibold ${
105+
page === currentPage
106+
? 'z-10 bg-blue-600 text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600'
107+
: page === '...'
108+
? 'text-gray-700'
109+
: 'text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50'
110+
}`}
111+
>
112+
{page}
113+
</button>
114+
))}
115+
116+
<button
117+
onClick={() => handlePageChange(currentPage + 1)}
118+
disabled={currentPage === totalPages}
119+
className="relative inline-flex items-center px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50"
120+
>
121+
<span className="sr-only">下一页</span>
122+
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
123+
<path fillRule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clipRule="evenodd" />
124+
</svg>
125+
</button>
126+
127+
<button
128+
onClick={() => handlePageChange(totalPages)}
129+
disabled={currentPage === totalPages}
130+
className="relative inline-flex items-center rounded-r-md px-2 py-2 text-gray-400 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-20 focus:outline-offset-0 disabled:opacity-50"
131+
>
132+
<span className="sr-only">末页</span>
133+
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
134+
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0l5 5a1 1 0 010 1.414l-5 5a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414zm6 0a1 1 0 011.414 0l5 5a1 1 0 010 1.414l-5 5a1 1 0 01-1.414-1.414L14.586 10l-4.293-4.293a1 1 0 010-1.414z" clipRule="evenodd" />
135+
</svg>
136+
</button>
137+
</nav>
138+
139+
<form onSubmit={handleJumpPage} className="flex items-center gap-2">
140+
<span className="text-sm text-gray-700">跳转到</span>
141+
<input
142+
type="number"
143+
min="1"
144+
max={totalPages}
145+
value={jumpPage}
146+
onChange={(e) => setJumpPage(e.target.value)}
147+
className="w-16 px-2 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
148+
placeholder="页码"
149+
/>
150+
<button
151+
type="submit"
152+
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors duration-200"
153+
>
154+
跳转
155+
</button>
156+
</form>
157+
</div>
158+
</div>
159+
</div>
160+
);
161+
}

0 commit comments

Comments
 (0)