page.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  1. "use client";
  2. import {fetchApi, fetchFile} from "@/app/_modules/func";
  3. import {ClearOutlined, DeleteOutlined, ExclamationCircleFilled, EyeOutlined, ReloadOutlined,} from "@ant-design/icons";
  4. import type {ActionType, ProColumns, ProFormInstance,} from "@ant-design/pro-components";
  5. import {PageContainer, ProDescriptions, ProTable,} from "@ant-design/pro-components";
  6. import {Button, Modal, Space, Tag} from "antd";
  7. import {useRouter} from "next/navigation";
  8. import {faCheck, faDownload, faToggleOff, faToggleOn, faXmark,} from "@fortawesome/free-solid-svg-icons";
  9. import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
  10. import {useRef, useState} from "react";
  11. export default function OperLog() {
  12. const { push } = useRouter();
  13. // 添加用于控制删除确认模态框的状态
  14. const [deleteModalVisible, setDeleteModalVisible] = useState(false);
  15. // 添加用于控制清空确认模态框的状态
  16. const [clearModalVisible, setClearModalVisible] = useState(false);
  17. //表格列定义
  18. const columns: ProColumns[] = [
  19. {
  20. title: "日志编号",
  21. dataIndex: "operId",
  22. search: false,
  23. },
  24. {
  25. title: "系统模块",
  26. fieldProps: {
  27. placeholder: "请输入系统模块",
  28. },
  29. dataIndex: "title",
  30. order: 9,
  31. },
  32. {
  33. title: "操作类型",
  34. fieldProps: {
  35. placeholder: "请选择操作类型",
  36. },
  37. dataIndex: "businessType",
  38. order: 7,
  39. valueEnum: {
  40. 1: {
  41. text: "新增",
  42. status: "1",
  43. },
  44. 2: {
  45. text: "修改",
  46. status: "2",
  47. },
  48. 3: {
  49. text: "删除",
  50. status: "3",
  51. },
  52. 4: {
  53. text: "授权",
  54. status: "4",
  55. },
  56. 5: {
  57. text: "导出",
  58. status: "5",
  59. },
  60. 6: {
  61. text: "导入",
  62. status: "6",
  63. },
  64. 7: {
  65. text: "强退",
  66. status: "7",
  67. },
  68. 8: {
  69. text: "生成代码",
  70. status: "8",
  71. },
  72. 9: {
  73. text: "清空数据",
  74. status: "9",
  75. },
  76. 0: {
  77. text: "其他",
  78. status: "0",
  79. },
  80. },
  81. },
  82. {
  83. title: "操作人员",
  84. fieldProps: {
  85. placeholder: "请输入操作人员",
  86. },
  87. dataIndex: "operName",
  88. sorter: true,
  89. order: 8,
  90. },
  91. {
  92. title: "操作地址",
  93. fieldProps: {
  94. placeholder: "请输入操作地址",
  95. },
  96. dataIndex: "operIp",
  97. order: 10,
  98. },
  99. {
  100. title: "操作地点",
  101. dataIndex: "operLocation",
  102. search: false,
  103. },
  104. {
  105. title: "操作状态",
  106. fieldProps: {
  107. placeholder: "请选择操作状态",
  108. },
  109. dataIndex: "status",
  110. valueType: "select",
  111. render: (_, record) => {
  112. return (
  113. <Space>
  114. <Tag
  115. color={record.status == 0 ? "green" : "red"}
  116. icon={
  117. record.status == 0 ? (
  118. <FontAwesomeIcon icon={faCheck} />
  119. ) : (
  120. <FontAwesomeIcon icon={faXmark} />
  121. )
  122. }
  123. >
  124. {_}
  125. </Tag>
  126. </Space>
  127. );
  128. },
  129. valueEnum: {
  130. 0: {
  131. text: "成功",
  132. status: "0",
  133. },
  134. 1: {
  135. text: "失败",
  136. status: "1",
  137. },
  138. },
  139. order: 6,
  140. },
  141. {
  142. title: "操作时间",
  143. dataIndex: "operTime",
  144. valueType: "dateTime",
  145. search: false,
  146. sorter: true,
  147. },
  148. {
  149. title: "操作时间",
  150. fieldProps: {
  151. placeholder: ["开始日期", "结束日期"],
  152. },
  153. dataIndex: "operTimeRange",
  154. valueType: "dateRange",
  155. hideInTable: true,
  156. order: 5,
  157. search: {
  158. transform: (value) => {
  159. return {
  160. "params[beginTime]": `${value[0]} 00:00:00`,
  161. "params[endTime]": `${value[1]} 23:59:59`,
  162. };
  163. },
  164. },
  165. },
  166. {
  167. title: "消耗时间",
  168. dataIndex: "costTime",
  169. sorter: true,
  170. search: false,
  171. render: (_, record) => {
  172. return <span>{_}毫秒</span>;
  173. },
  174. },
  175. {
  176. title: "操作",
  177. key: "option",
  178. search: false,
  179. render: (_, record) => [
  180. <Button
  181. key={record.operId}
  182. type="link"
  183. icon={<EyeOutlined />}
  184. onClick={() => showRowModal(record)}
  185. >
  186. 详情
  187. </Button>,
  188. ],
  189. },
  190. ];
  191. //查询日志数据
  192. const getLog = async (params: any, sorter: any, filter: any) => {
  193. const searchParams = {
  194. pageNum: params.current,
  195. ...params,
  196. };
  197. delete searchParams.current;
  198. const queryParams = new URLSearchParams(searchParams);
  199. Object.keys(sorter).forEach((key) => {
  200. queryParams.append("orderByColumn", key);
  201. if (sorter[key] === "ascend") {
  202. queryParams.append("isAsc", "ascending");
  203. } else {
  204. queryParams.append("isAsc", "descending");
  205. }
  206. });
  207. const body = await fetchApi(
  208. `/api/monitor/operlog/list?${queryParams}`,
  209. push
  210. );
  211. console.log("data:", body);
  212. return body;
  213. };
  214. //删除按钮是否可用,选中行时才可用
  215. const [rowCanDelete, setRowCanDelete] = useState(false);
  216. //选中行操作
  217. const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
  218. const rowSelection = {
  219. onChange: (newSelectedRowKeys: React.Key[]) => {
  220. setSelectedRowKeys(newSelectedRowKeys);
  221. setRowCanDelete(newSelectedRowKeys && newSelectedRowKeys.length > 0);
  222. },
  223. };
  224. //点击删除按钮
  225. const onClickDeleteRow = () => {
  226. setDeleteModalVisible(true);
  227. };
  228. //点击清空按钮
  229. const onClickClear = () => {
  230. setClearModalVisible(true);
  231. };
  232. //确定删除选中的日志数据
  233. const executeDeleteRow = async () => {
  234. const body = await fetchApi(
  235. `/api/monitor/operlog/${selectedRowKeys.join(",")}`,
  236. push,
  237. {
  238. method: "DELETE",
  239. }
  240. );
  241. if (body !== undefined) {
  242. if (body.code == 200) {
  243. App.useApp().message.success("删除成功");
  244. //删除按钮变回不可点击
  245. setRowCanDelete(false);
  246. //选中的数据恢复为空
  247. setSelectedRowKeys([]);
  248. //刷新列表
  249. if (actionRef.current) {
  250. actionRef.current.reload();
  251. }
  252. } else {
  253. App.useApp().message.error(body.msg);
  254. }
  255. }
  256. setDeleteModalVisible(false);
  257. };
  258. //确定清空日志数据
  259. const executeClear = async () => {
  260. const body = await fetchApi("/api/monitor/operlog/clean", push, {
  261. method: "DELETE",
  262. });
  263. if (body !== undefined) {
  264. if (body.code == 200) {
  265. App.useApp().message.success("清空成功");
  266. //选中的数据恢复为空
  267. setSelectedRowKeys([]);
  268. //刷新列表
  269. if (actionRef.current) {
  270. actionRef.current.reload();
  271. }
  272. } else {
  273. App.useApp().message.error(body.msg);
  274. }
  275. }
  276. setClearModalVisible(false);
  277. };
  278. //控制是否展示行详情模态框
  279. const [isModalOpen, setIsModalOpen] = useState(false);
  280. //关闭行详情展示
  281. function closeRowModal() {
  282. setIsModalOpen(false);
  283. }
  284. const [selectedRow, setSelectedRow] = useState(undefined as any);
  285. //展示行详情
  286. function showRowModal(record: any) {
  287. setIsModalOpen(true);
  288. setSelectedRow(record);
  289. }
  290. //搜索栏显示状态
  291. const [showSearch, setShowSearch] = useState(true);
  292. //action对象引用
  293. const actionRef = useRef<ActionType>(null);
  294. //表单对象引用
  295. const formRef = useRef<ProFormInstance>(null!);
  296. //当前页数和每页条数
  297. const [page, setPage] = useState(1);
  298. const defaultPageSize = 10;
  299. const [pageSize, setPageSize] = useState(defaultPageSize);
  300. const pageChange = (page: number, pageSize: number) => {
  301. setPage(page);
  302. setPageSize(pageSize);
  303. };
  304. //导出日志文件
  305. const exportTable = async () => {
  306. if (formRef.current) {
  307. const formData = new FormData();
  308. const data = {
  309. pageNum: page,
  310. pageSize: pageSize,
  311. ...formRef.current.getFieldsValue(),
  312. };
  313. Object.keys(data).forEach((key) => {
  314. if (data[key] !== undefined) {
  315. formData.append(key, data[key]);
  316. }
  317. });
  318. await fetchFile(
  319. "/api/monitor/operlog/export",
  320. push,
  321. {
  322. method: "POST",
  323. body: formData,
  324. },
  325. `operlog_${new Date().getTime()}.xlsx`
  326. );
  327. }
  328. };
  329. return (
  330. <PageContainer title={false}>
  331. <ProTable
  332. formRef={formRef}
  333. rowKey="operId"
  334. rowSelection={{
  335. selectedRowKeys,
  336. ...rowSelection,
  337. }}
  338. columns={columns}
  339. request={async (params: any, sorter: any, filter: any) => {
  340. // 表单搜索项会从 params 传入,传递给后端接口。
  341. console.log(params, sorter, filter);
  342. const data = await getLog(params, sorter, filter);
  343. if (data !== undefined) {
  344. return Promise.resolve({
  345. data: data.rows,
  346. success: true,
  347. total: data.total,
  348. });
  349. }
  350. return Promise.resolve({
  351. data: [],
  352. success: true,
  353. });
  354. }}
  355. pagination={{
  356. defaultPageSize: defaultPageSize,
  357. showQuickJumper: true,
  358. showSizeChanger: true,
  359. onChange: pageChange,
  360. }}
  361. search={
  362. showSearch
  363. ? {
  364. defaultCollapsed: false,
  365. searchText: "搜索",
  366. }
  367. : false
  368. }
  369. dateFormatter="string"
  370. actionRef={actionRef}
  371. toolbar={{
  372. actions: [
  373. <Button
  374. key="danger"
  375. danger
  376. icon={<DeleteOutlined />}
  377. disabled={!rowCanDelete}
  378. onClick={onClickDeleteRow}
  379. >
  380. 删除
  381. </Button>,
  382. <Button
  383. key="clear"
  384. danger
  385. icon={<ClearOutlined />}
  386. onClick={onClickClear}
  387. >
  388. 清空
  389. </Button>,
  390. <Button
  391. key="export"
  392. type="primary"
  393. icon={<FontAwesomeIcon icon={faDownload} />}
  394. onClick={exportTable}
  395. >
  396. 导出
  397. </Button>,
  398. ],
  399. settings: [
  400. {
  401. key: "switch",
  402. icon: showSearch ? (
  403. <FontAwesomeIcon icon={faToggleOn} />
  404. ) : (
  405. <FontAwesomeIcon icon={faToggleOff} />
  406. ),
  407. tooltip: showSearch ? "隐藏搜索栏" : "显示搜索栏",
  408. onClick: (key: string | undefined) => {
  409. setShowSearch(!showSearch);
  410. },
  411. },
  412. {
  413. key: "refresh",
  414. tooltip: "刷新",
  415. icon: <ReloadOutlined />,
  416. onClick: (key: string | undefined) => {
  417. if (actionRef.current) {
  418. actionRef.current.reload();
  419. }
  420. },
  421. },
  422. ],
  423. }}
  424. />
  425. {selectedRow !== undefined && (
  426. <Modal
  427. title="操作日志详情"
  428. footer={<Button onClick={closeRowModal}>关闭</Button>}
  429. open={isModalOpen}
  430. onCancel={closeRowModal}
  431. >
  432. <ProDescriptions column={10}>
  433. <ProDescriptions.Item span={3} label="操作模块">
  434. {selectedRow.title} /
  435. </ProDescriptions.Item>
  436. <ProDescriptions.Item
  437. span={2}
  438. valueEnum={{
  439. 1: {
  440. text: "新增",
  441. status: "1",
  442. },
  443. 2: {
  444. text: "修改",
  445. status: "2",
  446. },
  447. 3: {
  448. text: "删除",
  449. status: "3",
  450. },
  451. 4: {
  452. text: "授权",
  453. status: "4",
  454. },
  455. 5: {
  456. text: "导出",
  457. status: "5",
  458. },
  459. 6: {
  460. text: "导入",
  461. status: "6",
  462. },
  463. 7: {
  464. text: "强退",
  465. status: "7",
  466. },
  467. 8: {
  468. text: "生成代码",
  469. status: "8",
  470. },
  471. 9: {
  472. text: "清空数据",
  473. status: "9",
  474. },
  475. 0: {
  476. text: "其他",
  477. status: "0",
  478. },
  479. }}
  480. >
  481. {selectedRow.businessType}
  482. </ProDescriptions.Item>
  483. <ProDescriptions.Item span={5} label="请求地址">
  484. {selectedRow.operUrl}
  485. </ProDescriptions.Item>
  486. </ProDescriptions>
  487. <ProDescriptions column={6}>
  488. <ProDescriptions.Item span={3} label="登录信息">
  489. {selectedRow.operName}/{selectedRow.operIp}/
  490. {selectedRow.operLocation}
  491. </ProDescriptions.Item>
  492. <ProDescriptions.Item span={3} label="请求方式">
  493. {selectedRow.requestMethod}
  494. </ProDescriptions.Item>
  495. <ProDescriptions.Item span={6} label="操作方法">
  496. {selectedRow.method}
  497. </ProDescriptions.Item>
  498. <ProDescriptions.Item span={6} label="请求参数">
  499. {selectedRow.operParam}
  500. </ProDescriptions.Item>
  501. <ProDescriptions.Item span={6} label="返回参数">
  502. {selectedRow.jsonResult}
  503. </ProDescriptions.Item>
  504. </ProDescriptions>
  505. <ProDescriptions column={8}>
  506. <ProDescriptions.Item
  507. span={2}
  508. label="操作状态"
  509. valueEnum={{
  510. 0: {
  511. text: "成功",
  512. status: "0",
  513. },
  514. 1: {
  515. text: "失败",
  516. status: "1",
  517. },
  518. }}
  519. >
  520. {selectedRow.status}
  521. </ProDescriptions.Item>
  522. <ProDescriptions.Item span={2} label="消耗时间">
  523. {selectedRow.costTime}
  524. </ProDescriptions.Item>
  525. <ProDescriptions.Item span={4} label="操作时间">
  526. {selectedRow.operTime}
  527. </ProDescriptions.Item>
  528. </ProDescriptions>
  529. {selectedRow.errorMsg && (
  530. <ProDescriptions>
  531. <ProDescriptions.Item label="异常信息">
  532. {selectedRow.errorMsg}
  533. </ProDescriptions.Item>
  534. </ProDescriptions>
  535. )}
  536. </Modal>
  537. )}
  538. {/* 删除确认模态框 */}
  539. <Modal
  540. title={
  541. <div style={{ display: 'flex', alignItems: 'center' }}>
  542. <ExclamationCircleFilled style={{ color: '#faad14', marginRight: 8 }} />
  543. <span>系统提示</span>
  544. </div>
  545. }
  546. open={deleteModalVisible}
  547. onOk={executeDeleteRow}
  548. onCancel={() => setDeleteModalVisible(false)}
  549. okText="确认"
  550. cancelText="取消"
  551. >
  552. <p>{`确定删除日志编号为“${selectedRowKeys.join(",")}”的数据项?`}</p>
  553. </Modal>
  554. {/* 清空确认模态框 */}
  555. <Modal
  556. title={
  557. <div style={{ display: 'flex', alignItems: 'center' }}>
  558. <ExclamationCircleFilled style={{ color: '#faad14', marginRight: 8 }} />
  559. <span>系统提示</span>
  560. </div>
  561. }
  562. open={clearModalVisible}
  563. onOk={executeClear}
  564. onCancel={() => setClearModalVisible(false)}
  565. okText="确认"
  566. cancelText="取消"
  567. >
  568. <p>是否确认清空所有操作日志数据项?</p>
  569. </Modal>
  570. </PageContainer>
  571. );
  572. }