PostManagement.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. import React, { useState, useEffect, useRef } from 'react';
  2. import { Search, Plus, RefreshCw, Edit2, Trash2, Download } from 'lucide-react';
  3. import { postApi, PostVO, PostStatus, PageParam } from '../api/Post';
  4. import { toast } from 'sonner';
  5. import { formatDateTimeFull } from '../utils/formatTime';
  6. import { Modal, Button, Input, Space } from 'antd';
  7. import { ExclamationCircleOutlined } from '@ant-design/icons';
  8. import { Button as UIButton } from './ui/button';
  9. import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './ui/table';
  10. import PostForm, { PostFormRef } from './PostForm';
  11. export default function PostManagement() {
  12. const [loading, setLoading] = useState(true);
  13. const [list, setList] = useState<PostVO[]>([]);
  14. const [total, setTotal] = useState(0);
  15. const [queryParams, setQueryParams] = useState<PageParam>({
  16. pageNo: 1,
  17. pageSize: 10,
  18. name: undefined,
  19. code: undefined,
  20. userId: undefined, // 添加用户ID参数
  21. });
  22. const [exportLoading, setExportLoading] = useState(false);
  23. const formRef = useRef<PostFormRef>(null);
  24. // 从 sessionStorage 读取用户ID(如果存在)
  25. useEffect(() => {
  26. const userId = sessionStorage.getItem('selectedUserId');
  27. if (userId) {
  28. setQueryParams(prev => ({ ...prev, userId: Number(userId) }));
  29. // 读取后清除,避免下次进入时还使用旧的用户ID
  30. sessionStorage.removeItem('selectedUserId');
  31. }
  32. }, []);
  33. // 获取岗位列表
  34. const getList = async (params?: PageParam) => {
  35. const currentParams = params || queryParams;
  36. setLoading(true);
  37. try {
  38. console.log('PostManagement: 开始获取岗位列表', currentParams);
  39. const response = await postApi.getPostPage(currentParams);
  40. console.log('PostManagement: API 响应', response);
  41. // 处理响应数据
  42. let data;
  43. if (response && typeof response === 'object') {
  44. // 如果响应有 data 属性,使用 data;否则直接使用 response
  45. data = (response as any).data !== undefined ? (response as any).data : response;
  46. } else {
  47. data = response;
  48. }
  49. // 确保 data 是对象
  50. if (!data || typeof data !== 'object') {
  51. console.warn('PostManagement: 响应数据格式异常', data);
  52. setList([]);
  53. setTotal(0);
  54. return;
  55. }
  56. setList(data.list || []);
  57. setTotal(data.total || 0);
  58. console.log('PostManagement: 设置列表数据', data.list, '总数', data.total);
  59. } catch (error: any) {
  60. console.error('PostManagement: 获取岗位列表失败', error);
  61. // 即使请求失败,也设置空列表,避免一直显示加载中
  62. setList([]);
  63. setTotal(0);
  64. // 显示错误信息
  65. const errorMessage = error?.response?.data?.message ||
  66. error?.message ||
  67. '获取岗位列表失败,请检查网络连接或联系管理员';
  68. toast.error(errorMessage);
  69. } finally {
  70. setLoading(false);
  71. }
  72. };
  73. // 组件挂载和分页参数变化时获取数据
  74. useEffect(() => {
  75. console.log('PostManagement: useEffect 触发,queryParams =', queryParams);
  76. getList();
  77. // eslint-disable-next-line react-hooks/exhaustive-deps
  78. }, [queryParams.pageNo, queryParams.pageSize]);
  79. // 搜索
  80. const handleQuery = () => {
  81. const newParams = { ...queryParams, pageNo: 1 };
  82. setQueryParams(newParams);
  83. getList(newParams);
  84. };
  85. // 重置搜索
  86. const resetQuery = () => {
  87. const resetParams: PageParam = {
  88. pageNo: 1,
  89. pageSize: 10,
  90. name: undefined,
  91. code: undefined,
  92. };
  93. setQueryParams(resetParams);
  94. getList(resetParams);
  95. };
  96. // 打开表单
  97. const openForm = (type: string, id?: number) => {
  98. console.log('PostManagement: 打开表单', type, id, 'formRef.current =', formRef.current);
  99. if (formRef.current) {
  100. formRef.current.open(type, id);
  101. } else {
  102. console.error('PostManagement: formRef.current 为 null');
  103. toast.error('表单组件未初始化,请刷新页面重试');
  104. }
  105. };
  106. // 删除岗位
  107. const handleDelete = async (id: number, name: string) => {
  108. Modal.confirm({
  109. title: '确认删除',
  110. icon: <ExclamationCircleOutlined />,
  111. content: (
  112. <div>
  113. <p>确定要删除岗位 <strong>"{name}"</strong> 吗?</p>
  114. <p style={{ color: '#ff4d4f', marginTop: '8px' }}>删除后无法恢复,请谨慎操作!</p>
  115. </div>
  116. ),
  117. okText: '确定删除',
  118. okType: 'danger',
  119. cancelText: '取消',
  120. onOk: async () => {
  121. try {
  122. await postApi.deletePost(id);
  123. toast.success('删除成功');
  124. await getList();
  125. } catch (error: any) {
  126. toast.error(error.message || '删除失败');
  127. }
  128. },
  129. });
  130. };
  131. // 导出岗位
  132. const handleExport = async () => {
  133. Modal.confirm({
  134. title: '确认导出',
  135. icon: <ExclamationCircleOutlined />,
  136. content: '确定要导出岗位数据吗?',
  137. okText: '确定导出',
  138. cancelText: '取消',
  139. onOk: async () => {
  140. setExportLoading(true);
  141. try {
  142. const response = await postApi.exportPost(queryParams);
  143. const blob = (response as any)?.data || response;
  144. const url = window.URL.createObjectURL(blob);
  145. const link = document.createElement('a');
  146. link.href = url;
  147. link.download = '岗位列表.xls';
  148. document.body.appendChild(link);
  149. link.click();
  150. document.body.removeChild(link);
  151. window.URL.revokeObjectURL(url);
  152. toast.success('导出成功');
  153. } catch (error: any) {
  154. toast.error(error.message || '导出失败');
  155. } finally {
  156. setExportLoading(false);
  157. }
  158. },
  159. });
  160. };
  161. // 获取状态标签
  162. const getStatusLabel = (status: number) => {
  163. return status === PostStatus.ENABLE ? '启用' : '禁用';
  164. };
  165. // 获取状态样式
  166. const getStatusStyle = (status: number) => {
  167. return status === PostStatus.ENABLE
  168. ? 'bg-green-100 text-green-700'
  169. : 'bg-gray-100 text-gray-700';
  170. };
  171. return (
  172. <div className="space-y-4">
  173. {/* 搜索栏 */}
  174. <div className="bg-white rounded-xl border border-gray-200/50 shadow-sm p-5">
  175. <div className="flex items-center justify-between gap-4 flex-wrap">
  176. {/* 搜索输入框 */}
  177. <div className="flex items-center gap-3 flex-wrap flex-1">
  178. <div className="flex items-center gap-3">
  179. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">岗位名称:</label>
  180. <Input
  181. value={queryParams.name || ''}
  182. onChange={(e) => setQueryParams(prev => ({ ...prev, name: e.target.value }))}
  183. onPressEnter={handleQuery}
  184. placeholder="请输入岗位名称"
  185. style={{ width: 192 }}
  186. allowClear
  187. />
  188. </div>
  189. <div className="flex items-center gap-3">
  190. <label className="text-sm font-medium text-gray-700 whitespace-nowrap">岗位编码:</label>
  191. <Input
  192. value={queryParams.code || ''}
  193. onChange={(e) => setQueryParams(prev => ({ ...prev, code: e.target.value }))}
  194. onPressEnter={handleQuery}
  195. placeholder="请输入岗位编码"
  196. style={{ width: 192 }}
  197. allowClear
  198. />
  199. </div>
  200. </div>
  201. {/* 操作按钮组 */}
  202. <Space>
  203. <Button
  204. type="primary"
  205. icon={<Search className="w-4 h-4" />}
  206. onClick={handleQuery}
  207. >
  208. 搜索
  209. </Button>
  210. <Button
  211. icon={<RefreshCw className="w-4 h-4" />}
  212. onClick={resetQuery}
  213. >
  214. 重置
  215. </Button>
  216. <Button
  217. type="primary"
  218. icon={<Plus className="w-4 h-4" />}
  219. onClick={() => openForm('create')}
  220. >
  221. 新增
  222. </Button>
  223. <Button
  224. icon={<Download className="w-4 h-4" />}
  225. onClick={handleExport}
  226. loading={exportLoading}
  227. >
  228. 导出
  229. </Button>
  230. </Space>
  231. </div>
  232. </div>
  233. {/* 表格 */}
  234. <div className="bg-white rounded-lg border border-gray-200 overflow-hidden">
  235. <Table>
  236. <TableHeader>
  237. <TableRow>
  238. <TableHead className="w-[80px] text-center">岗位编号</TableHead>
  239. <TableHead className="w-[150px]">岗位名称</TableHead>
  240. <TableHead className="w-[150px]">岗位编码</TableHead>
  241. <TableHead className="w-[100px] text-center">岗位顺序</TableHead>
  242. <TableHead>岗位备注</TableHead>
  243. <TableHead className="w-[100px] text-center">状态</TableHead>
  244. <TableHead className="w-[180px] text-center">创建时间</TableHead>
  245. <TableHead className="w-[160px] text-center">操作</TableHead>
  246. </TableRow>
  247. </TableHeader>
  248. <TableBody>
  249. {loading ? (
  250. <TableRow>
  251. <TableCell colSpan={8} className="text-center py-8 text-gray-500">
  252. 加载中...
  253. </TableCell>
  254. </TableRow>
  255. ) : list.length === 0 ? (
  256. <TableRow>
  257. <TableCell colSpan={8} className="text-center py-8 text-gray-500">
  258. 暂无数据
  259. </TableCell>
  260. </TableRow>
  261. ) : (
  262. list.map((row, index) => (
  263. <TableRow key={row.id} className="hover:bg-gray-50">
  264. <TableCell className="text-center">{row.id}</TableCell>
  265. <TableCell className="font-medium">{row.name}</TableCell>
  266. <TableCell>{row.code}</TableCell>
  267. <TableCell className="text-center">{row.sort}</TableCell>
  268. <TableCell className="text-sm text-gray-600">{row.remark || '-'}</TableCell>
  269. <TableCell className="text-center">
  270. <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusStyle(row.status)}`}>
  271. {getStatusLabel(row.status)}
  272. </span>
  273. </TableCell>
  274. <TableCell className="text-center text-sm text-gray-600">
  275. {formatDateTimeFull(row.createTime)}
  276. </TableCell>
  277. <TableCell>
  278. <div className="flex items-center gap-2 justify-center">
  279. <UIButton
  280. variant="ghost"
  281. size="sm"
  282. onClick={() => openForm('update', row.id)}
  283. className="h-8 px-2"
  284. >
  285. <Edit2 className="w-4 h-4" />
  286. </UIButton>
  287. <UIButton
  288. variant="ghost"
  289. size="sm"
  290. onClick={() => handleDelete(row.id!, row.name)}
  291. className="h-8 px-2 text-red-600 hover:text-red-700"
  292. >
  293. <Trash2 className="w-4 h-4" />
  294. </UIButton>
  295. </div>
  296. </TableCell>
  297. </TableRow>
  298. ))
  299. )}
  300. </TableBody>
  301. </Table>
  302. </div>
  303. {/* 分页 */}
  304. {!loading && list.length > 0 && (
  305. <div className="bg-white rounded-lg border border-gray-200 px-6 py-4">
  306. <div className="flex items-center justify-between">
  307. <div className="text-sm text-gray-600">
  308. 共 <span className="text-blue-600 font-medium">{total}</span> 条记录
  309. </div>
  310. <div className="flex gap-2">
  311. <Button
  312. onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! - 1 })}
  313. disabled={queryParams.pageNo! <= 1}
  314. >
  315. 上一页
  316. </Button>
  317. <span className="px-4 py-2 text-sm text-gray-600 flex items-center">
  318. {queryParams.pageNo} / {Math.ceil(total / queryParams.pageSize!) || 1}
  319. </span>
  320. <Button
  321. onClick={() => setQueryParams({ ...queryParams, pageNo: queryParams.pageNo! + 1 })}
  322. disabled={queryParams.pageNo! >= Math.ceil(total / queryParams.pageSize!)}
  323. >
  324. 下一页
  325. </Button>
  326. </div>
  327. </div>
  328. </div>
  329. )}
  330. {/* 表单弹窗 */}
  331. <PostForm ref={formRef} onSuccess={getList} />
  332. </div>
  333. );
  334. }