feat: 统一分页组件并添加批量删除功能

为多个页面组件添加统一的分页统计显示和批量删除功能
在日志管理页面添加批量删除接口和前端实现
优化表格分页配置,统一显示总条目数和分页选项
This commit is contained in:
2025-09-25 23:52:01 +08:00
parent a71bbb0961
commit 214605d912
14 changed files with 303 additions and 48 deletions

View File

@ -12,6 +12,9 @@ export default function Departments(){
const [loading, setLoading] = useState(false)
const [data, setData] = useState([] as DeptItem[])
const [keyword, setKeyword] = useState('')
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [createOpen, setCreateOpen] = useState(false)
const [editOpen, setEditOpen] = useState(false)
@ -20,18 +23,31 @@ export default function Departments(){
const [editForm] = Form.useForm()
const [searchForm] = Form.useForm()
const fetchList = async (kw: string = keyword) => {
const fetchList = async (kw: string = keyword, p: number = page, ps: number = pageSize) => {
setLoading(true)
try{
const { data } = await api.get('/departments', { params: { keyword: kw } })
if(data?.code === 0){ setData(data.data || []) } else { throw new Error(data?.message || '获取部门失败') }
const { data } = await api.get('/departments', { params: { keyword: kw, page: p, page_size: ps } })
if(data?.code === 0){
const payload = data.data
if (payload && Array.isArray(payload.items)) {
const items = payload.items as DeptItem[]
setData(items)
setTotal(Number(payload.total ?? items.length ?? 0))
setPage(Number(payload.page ?? p))
setPageSize(Number(payload.page_size ?? ps))
} else {
const list = (payload || []) as DeptItem[]
setData(list)
setTotal(Array.isArray(list) ? list.length : 0)
}
} else { throw new Error(data?.message || '获取部门失败') }
}catch(e: any){ message.error(e.message || '获取部门失败') } finally { setLoading(false) }
}
const didInitFetchRef = useRef(false)
useEffect(()=>{
if(didInitFetchRef.current) return
didInitFetchRef.current = true
fetchList('')
fetchList('', 1, pageSize)
}, [])
// 构建部门树用于树形选择
@ -182,25 +198,36 @@ export default function Departments(){
return (
<div>
<PageHeader items={["系统管理","部门管理"]} title="" />
<Form form={searchForm} layout="inline" onFinish={(vals: any)=>{ const kw = String(vals?.keyword ?? '').trim(); setKeyword(kw); fetchList(kw) }} style={{ marginBottom: 12 }}>
<Form form={searchForm} layout="inline" onFinish={(vals: any)=>{ const kw = String(vals?.keyword ?? '').trim(); setKeyword(kw); fetchList(kw, 1, pageSize) }} style={{ marginBottom: 12 }}>
<Form.Item name="keyword">
<Input allowClear placeholder="搜索部门" style={{ width: 320 }} />
</Form.Item>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit"></Button>
<Button onClick={()=>{ searchForm.resetFields(); setKeyword(''); fetchList('') }}></Button>
<Button onClick={()=>{ searchForm.resetFields(); setKeyword(''); fetchList('', 1, 10) }}></Button>
<Button type="primary" onClick={onCreate}></Button>
</Space>
</Form.Item>
</Form>
{/* 总条目数展示:在表格上方显示当前总数 */}
<div style={{ marginBottom: 8, color: '#666' }}>{total}</div>
<Table<DeptItem>
rowKey="id"
loading={loading}
dataSource={treeDataForTable}
columns={columns}
expandable={{ expandedRowKeys, onExpandedRowsChange: (keys) => setExpandedRowKeys(keys as React.Key[]), indentSize: 18, rowExpandable: (record: DeptItem) => Array.isArray(record.children) && record.children.length > 0 }}
pagination={false}
// 展示分页统计:显示总条目数(与流程日志一致的受控分页)
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
showTotal: (t, range) => `${range[0]}-${range[1]} 条 / 共 ${t}`,
onChange: (p, ps) => { setPage(p); if (ps) setPageSize(ps) }
}}
/>
<Modal title="新增部门" open={createOpen} onOk={handleCreate} onCancel={() => setCreateOpen(false)} okText="创建" width={840}>
@ -216,7 +243,7 @@ export default function Departments(){
filterTreeNode={(input: string, node: any) => String(node?.title ?? '').toLowerCase().includes(String(input).toLowerCase())}
/>
</Form.Item>
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
<Input placeholder="请输入名称" />
</Form.Item>
<Form.Item name="order_no" label="排序" initialValue={0}>

View File

@ -249,7 +249,15 @@ export default function FlowList() {
loading={loading}
dataSource={list}
columns={columns as any}
pagination={{ current: page, pageSize, total, showSizeChanger: true, onChange: (p: number, ps?: number) => fetchList(p, ps ?? pageSize, keyword) }}
// 展示分页统计:在分页栏显示总条目数
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
showTotal: (t, range) => `${range[0]}-${range[1]} 条 / 共 ${t}`,
onChange: (p: number, ps?: number) => fetchList(p, ps ?? pageSize, keyword)
}}
/>
{/* 编辑基础信息弹窗 */}

View File

@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState } from 'react'
import { Button, Form, Input, Select, Space, Table, Tag, Descriptions, DatePicker, Drawer, Typography } from 'antd'
import { Button, Form, Input, Select, Space, Table, Tag, Descriptions, DatePicker, Drawer, Typography, Popconfirm, message } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import api, { ApiResp } from '../utils/axios'
import dayjs from 'dayjs'
@ -30,9 +30,10 @@ export default function FlowRunLogs() {
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [detailOpen, setDetailOpen] = useState(false)
const [detail, setDetail] = useState<RunLogItem | null>(null)
// 选中项用于批量删除
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([])
const fetchData = async (p = page, ps = pageSize) => {
const v = form.getFieldsValue()
@ -57,6 +58,23 @@ export default function FlowRunLogs() {
const openDetail = (record: RunLogItem) => { setDetail(record); setDetailOpen(true) }
const closeDetail = () => setDetailOpen(false)
// 批量删除
const handleBatchDelete = async () => {
if (!selectedKeys.length) return
try {
const { data } = await api.delete<ApiResp<{ deleted: number }>>(`/flow_run_logs/${selectedKeys.join(',')}`)
if (data?.code === 0) {
message.success(`已删除 ${data?.data?.deleted || 0}`)
setSelectedKeys([])
fetchData(page, pageSize)
} else {
throw new Error(data?.message || '删除失败')
}
} catch (e: any) {
message.error(e.message || '删除失败')
}
}
const columns: ColumnsType<RunLogItem> = useMemo(() => [
{ title: 'ID', dataIndex: 'id', width: 90 },
{ title: '时间', dataIndex: 'started_at', width: 180, render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm:ss') },
@ -135,13 +153,32 @@ export default function FlowRunLogs() {
</Form.Item>
</Form>
{/* // 操作区:批量删除 */}
<div style={{ marginBottom: 12 }}>
<Space>
<Popconfirm title={`确认删除选中的 ${selectedKeys.length} 条运行日志?`} onConfirm={handleBatchDelete} disabled={!selectedKeys.length}>
<Button danger disabled={!selectedKeys.length}></Button>
</Popconfirm>
<Button onClick={() => fetchData(page, pageSize)}></Button>
</Space>
</div>
<Table<RunLogItem>
rowKey="id"
loading={loading}
dataSource={data}
columns={columns}
scroll={{ x: 1600 }}
pagination={{ current: page, pageSize, total, showSizeChanger: true, pageSizeOptions: [10, 20, 50, 100], onChange: (p, ps) => fetchData(p, ps) }}
rowSelection={{ selectedRowKeys: selectedKeys, onChange: (keys) => setSelectedKeys(keys) }}
// 展示分页统计:在分页栏显示总条目数
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
showTotal: (t, range) => `${range[0]}-${range[1]} 条 / 共 ${t}`,
onChange: (p, ps) => fetchData(p, ps)
}}
/>
<Drawer title="运行详情" width={820} open={detailOpen} onClose={closeDetail} destroyOnHidden placement="right">

View File

@ -30,9 +30,10 @@ export default function Logs() {
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [detailOpen, setDetailOpen] = useState(false)
const [detail, setDetail] = useState<LogInfo | null>(null)
// 选中项用于批量删除
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([])
const fetchData = async (p = page, ps = pageSize) => {
const v = form.getFieldsValue()
@ -59,16 +60,33 @@ export default function Logs() {
const openDetail = (record: LogInfo) => { setDetail(record); setDetailOpen(true) }
const closeDetail = () => setDetailOpen(false)
// 批量删除
const handleBatchDelete = async () => {
if (!selectedKeys.length) return
try {
const { data } = await api.delete<ApiResp<{ deleted: number }>>(`/logs/${selectedKeys.join(',')}`)
if (data?.code === 0) {
message.success(`已删除 ${data?.data?.deleted || 0}`)
setSelectedKeys([])
fetchData(page, pageSize)
} else {
throw new Error(data?.message || '删除失败')
}
} catch (e: any) {
message.error(e.message || '删除失败')
}
}
const columns = useMemo(() => [
{ title: 'ID', dataIndex: 'id', width: 80 },
{ title: '时间', dataIndex: 'request_time', width: 180, render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm:ss') },
{ title: 'ID', dataIndex: 'id', width: 100 },
{ title: '时间', dataIndex: 'request_time', width: 120, render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm:ss') },
{ title: '路径', dataIndex: 'path', width: 240 },
{ title: '方法', dataIndex: 'method', width: 90, render: (m: string) => <Tag>{m}</Tag> },
{ title: '用户', dataIndex: 'username', width: 140, render: (_: any, r: LogInfo) => r.username || (r.user_id ? `UID:${r.user_id}` : '-') },
{ title: '状态码', dataIndex: 'status_code', width: 100 },
{ title: '耗时(ms)', dataIndex: 'duration_ms', width: 100 },
{ title: '请求参数', dataIndex: 'request_params', width: 260, ellipsis: true },
{ title: '响应参数', dataIndex: 'response_params', width: 260, ellipsis: true },
{ title: '方法', dataIndex: 'method', width: 60, render: (m: string) => <Tag>{m}</Tag> },
{ title: '用户', dataIndex: 'username', width: 100, render: (_: any, r: LogInfo) => r.username || (r.user_id ? `UID:${r.user_id}` : '-') },
{ title: '状态码', dataIndex: 'status_code', width: 60 },
{ title: '耗时(ms)', dataIndex: 'duration_ms', width: 80 },
{ title: '请求参数', dataIndex: 'request_params', width: 280, ellipsis: true },
{ title: '响应参数', dataIndex: 'response_params', width: 280, ellipsis: true },
{ title: '操作', key: 'action', fixed: 'right' as any, width: 120, render: (_: any, r: LogInfo) => (
<Space>
<a className="action-link" onClick={() => openDetail(r)}>
@ -129,13 +147,32 @@ export default function Logs() {
</Space>
</Form.Item>
</Form>
{/* 操作区:批量删除 */}
<div style={{ marginBottom: 12 }}>
<Space>
<Popconfirm title={`确认删除选中的 ${selectedKeys.length} 条日志?`} onConfirm={handleBatchDelete} disabled={!selectedKeys.length}>
<Button danger disabled={!selectedKeys.length}></Button>
</Popconfirm>
<Button onClick={() => fetchData(page, pageSize)}></Button>
</Space>
</div>
<Table
rowKey="id"
loading={loading}
dataSource={data}
columns={columns as any}
scroll={{ x: 2000 }}
pagination={{ current: page, pageSize, total, onChange: (p, ps) => fetchData(p, ps) }}
rowSelection={{ selectedRowKeys: selectedKeys, onChange: (keys) => setSelectedKeys(keys) }}
// 展示分页统计:在分页栏显示总条目数
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
showTotal: (t, range) => `${range[0]}-${range[1]} 条 / 共 ${t}`,
onChange: (p, ps) => fetchData(p, ps)
}}
/>
<Drawer title="日志详情" width={720} open={detailOpen} onClose={closeDetail} destroyOnHidden placement="right">

View File

@ -122,6 +122,8 @@ export default function Menus(){
const [data, setData] = useState([] as MenuItem[])
const [parents, setParents] = useState([] as MenuItem[])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(10)
const [keyword, setKeyword] = useState('')
// 是否显示按钮type=3
const [showButtons, setShowButtons] = useState(true)
@ -234,16 +236,31 @@ export default function Menus(){
{ name: 'ReloadOutlined', node: <ReloadOutlined /> },
], [])
const fetchMenus = async (kw: string = keyword) => {
const fetchMenus = async (kw: string = keyword, p: number = page, ps: number = pageSize) => {
setLoading(true)
try{
const { data } = await api.get(`/menus`, { params: { keyword: kw } })
if(data?.code === 0){ setData(data.data || []); setParents((data.data || []).filter((m: MenuItem) => m.type !== 3)) }
const { data } = await api.get(`/menus`, { params: { keyword: kw, page: p, page_size: ps } })
if(data?.code === 0){
const payload = data.data
if (payload && Array.isArray(payload.items)) {
const items = payload.items as MenuItem[]
setData(items)
setParents(items.filter((m: MenuItem) => m.type !== 3))
setTotal(Number(payload.total ?? items.length ?? 0))
setPage(Number(payload.page ?? p))
setPageSize(Number(payload.page_size ?? ps))
} else {
const list = (payload || []) as MenuItem[]
setData(list)
setParents(list.filter((m: MenuItem) => m.type !== 3))
setTotal(Array.isArray(list) ? list.length : 0)
}
}
else { throw new Error(data?.message || '获取菜单失败') }
}catch(e: any){ message.error(e.message || '获取菜单失败') } finally { setLoading(false) }
}
useEffect(() => { fetchMenus(keyword) }, [])
useEffect(() => { fetchMenus(keyword, 1, pageSize) }, [])
const onCreate = () => { form.resetFields(); setCreateOpen(true) }
const handleCreate = async () => {
@ -446,7 +463,7 @@ export default function Menus(){
<div>
<PageHeader items={["系统管理","菜单管理"]} title="" />
<Form form={searchForm} layout="inline" onFinish={(vals: any)=>{ const kw = String(vals?.keyword ?? '').trim(); setKeyword(kw); fetchMenus(kw) }} style={{ marginBottom: 12 }}>
<Form form={searchForm} layout="inline" onFinish={(vals: any)=>{ const kw = String(vals?.keyword ?? '').trim(); setKeyword(kw); fetchMenus(kw, 1, pageSize) }} style={{ marginBottom: 12 }}>
<Form.Item name="keyword">
<Input allowClear placeholder="搜索菜单名称/路径/权限" style={{ width: 320 }} />
</Form.Item>
@ -466,7 +483,16 @@ export default function Menus(){
dataSource={treeDataForTable}
columns={columns}
expandable={{ expandedRowKeys, onExpandedRowsChange: (keys) => setExpandedRowKeys(keys as React.Key[]), indentSize: 18, rowExpandable: (record: MenuItem) => Array.isArray(record.children) && record.children.length > 0 }}
pagination={{ pageSize: 9999, hideOnSinglePage: true }}
// 展示分页统计:显示总条目数(与流程日志一致的受控分页)
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
showTotal: (t, range) => `${range[0]}-${range[1]} 条 / 共 ${t}`,
onChange: (p, ps) => { setPage(p); if (ps) setPageSize(ps); fetchMenus(keyword, p, ps ?? pageSize) }
}}
/>
<Modal title="新增菜单" open={createOpen} onOk={handleCreate} onCancel={() => setCreateOpen(false)} okText="创建" width={840}>

View File

@ -116,7 +116,7 @@ export default function Permissions(){
loading={loading}
dataSource={data}
columns={columns}
pagination={{ current: page, pageSize, total, showSizeChanger: true, onChange: (p: number, ps?: number) => fetchPermissions(p, ps ?? pageSize, keyword) }}
pagination={{ current: page, pageSize, total, showSizeChanger: true, pageSizeOptions: [10, 20, 50, 100], onChange: (p: number, ps?: number) => fetchPermissions(p, ps ?? pageSize, keyword) }}
/>
<Modal title="新增权限" open={createOpen} onOk={handleCreate} onCancel={() => setCreateOpen(false)} okText="创建" width={840}>

View File

@ -125,7 +125,16 @@ export default function Positions(){
loading={loading}
dataSource={data}
columns={columns}
pagination={{ current: page, pageSize, total, showSizeChanger: true, onChange: (p: number, ps?: number) => fetchPositions(p, ps ?? pageSize, keyword) }}
// 展示分页统计:在分页栏显示总条目数
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
showTotal: (t, range) => `${range[0]}-${range[1]} 条 / 共 ${t}`,
onChange: (p: number, ps?: number) => fetchPositions(p, ps ?? pageSize, keyword)
}}
/>
<Modal title="新增岗位" open={createOpen} onOk={handleCreate} onCancel={() => setCreateOpen(false)} okText="创建" width={640}>

View File

@ -239,7 +239,16 @@ export default function Roles(){
loading={loading}
dataSource={data}
columns={columns}
pagination={{ current: page, pageSize, total, showSizeChanger: true, onChange: (p: number, ps?: number) => fetchRoles(p, ps ?? pageSize, keyword) }}
// 展示分页统计:在分页栏显示总条目数
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50, 100],
showTotal: (t, range) => `${range[0]}-${range[1]} 条 / 共 ${t}`,
onChange: (p: number, ps?: number) => fetchRoles(p, ps ?? pageSize, keyword)
}}
/>
<Modal title="新增角色" open={createOpen} onOk={handleCreate} onCancel={() => setCreateOpen(false)} okText="创建" width={840}>

View File

@ -236,12 +236,15 @@ export default function ScheduleJobs() {
loading={loading}
dataSource={data}
columns={columns}
// 展示分页统计:在分页栏显示总条目数
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
onChange: (p, ps) => fetchJobs(p, ps),
pageSizeOptions: [10, 20, 50, 100],
showTotal: (t, range) => `${range[0]}-${range[1]} 条 / 共 ${t}`,
onChange: (p: number, ps?: number) => fetchJobs(p, ps ?? pageSize)
}}
/>

View File

@ -343,8 +343,16 @@ export default function Users(){
rowKey="id"
loading={loading}
dataSource={data}
columns={columns}
pagination={{ current: page, pageSize, total, showSizeChanger: true, onChange: (p: number, ps?: number) => fetchUsers(p, ps ?? pageSize, keyword) }}
columns={columns as any}
// 展示分页统计:在分页栏显示总条目数
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
showTotal: (t, range) => `${range[0]}-${range[1]} 条 / 共 ${t}`,
onChange: (p: number, ps?: number) => fetchUsers(p, ps ?? pageSize, keyword)
}}
/>
{/* <Typography.Paragraph type="secondary" style={{ marginTop: 12 }}>
提示:此页面已支持分页、创建、编辑与重置密码。