index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. <template>
  2. <div class="system-log-container">
  3. <!-- 搜索栏 -->
  4. <el-card class="search-card" shadow="never">
  5. <el-form :inline="true" :model="queryParams" class="search-form">
  6. <el-form-item label="操作用户">
  7. <el-input v-model="queryParams.operatorName" placeholder="请输入" clearable style="width: 150px" />
  8. </el-form-item>
  9. <el-form-item label="操作模块">
  10. <el-input v-model="queryParams.module" placeholder="请输入" clearable style="width: 150px" />
  11. </el-form-item>
  12. <el-form-item label="操作类型">
  13. <el-select v-model="queryParams.operationType" placeholder="请选择" clearable style="width: 150px">
  14. <el-option label="新增" value="CREATE" />
  15. <el-option label="修改" value="UPDATE" />
  16. <el-option label="删除" value="DELETE" />
  17. <el-option label="查询" value="QUERY" />
  18. <el-option label="登录" value="LOGIN" />
  19. <el-option label="登出" value="LOGOUT" />
  20. <el-option label="其他" value="OTHER" />
  21. </el-select>
  22. </el-form-item>
  23. <el-form-item label="操作时间">
  24. <el-date-picker
  25. v-model="dateRange"
  26. type="daterange"
  27. range-separator="至"
  28. start-placeholder="开始日期"
  29. end-placeholder="结束日期"
  30. value-format="YYYY-MM-DD"
  31. style="width: 240px"
  32. />
  33. </el-form-item>
  34. <el-form-item>
  35. <el-button type="primary" @click="handleQuery">
  36. <el-icon><ele-Search /></el-icon> 查询
  37. </el-button>
  38. <el-button @click="resetQuery">
  39. <el-icon><ele-Refresh /></el-icon> 重置
  40. </el-button>
  41. </el-form-item>
  42. </el-form>
  43. </el-card>
  44. <!-- 数据表格 -->
  45. <el-card class="table-card" shadow="never">
  46. <template #header>
  47. <div class="card-header">
  48. <span>操作日志列表</span>
  49. <el-button type="danger" @click="handleClearLog" :disabled="tableData.length === 0">
  50. <el-icon><ele-Delete /></el-icon> 清空日志
  51. </el-button>
  52. </div>
  53. </template>
  54. <el-table v-loading="loading" :data="tableData" border stripe>
  55. <el-table-column prop="id" label="ID" width="80" align="center" />
  56. <el-table-column prop="operatorName" label="操作用户" width="120" align="center" />
  57. <el-table-column prop="module" label="操作模块" width="120" align="center" />
  58. <el-table-column prop="operationType" label="操作类型" width="100" align="center">
  59. <template #default="{ row }">
  60. <el-tag :type="getOperationTypeColor(row.operationType)">
  61. {{ getOperationTypeLabel(row.operationType) }}
  62. </el-tag>
  63. </template>
  64. </el-table-column>
  65. <el-table-column prop="description" label="操作描述" min-width="200" show-overflow-tooltip />
  66. <el-table-column prop="requestMethod" label="请求方式" width="90" align="center">
  67. <template #default="{ row }">
  68. <el-tag :type="getMethodColor(row.requestMethod)" size="small">{{ row.requestMethod }}</el-tag>
  69. </template>
  70. </el-table-column>
  71. <el-table-column prop="requestUrl" label="请求URL" width="200" show-overflow-tooltip />
  72. <el-table-column prop="ip" label="IP地址" width="130" align="center" />
  73. <el-table-column prop="costTime" label="耗时(ms)" width="90" align="center">
  74. <template #default="{ row }">
  75. <el-tag :type="row.costTime > 1000 ? 'danger' : row.costTime > 500 ? 'warning' : 'success'" size="small">
  76. {{ row.costTime }}
  77. </el-tag>
  78. </template>
  79. </el-table-column>
  80. <el-table-column prop="createTime" label="操作时间" width="170" align="center" />
  81. <el-table-column label="操作" width="80" align="center" fixed="right">
  82. <template #default="{ row }">
  83. <el-button type="primary" link size="small" @click="handleDetail(row)">详情</el-button>
  84. </template>
  85. </el-table-column>
  86. </el-table>
  87. <!-- 分页 -->
  88. <div class="pagination-container">
  89. <el-pagination
  90. v-model:current-page="queryParams.pageNum"
  91. v-model:page-size="queryParams.pageSize"
  92. :page-sizes="[10, 20, 50, 100]"
  93. :total="total"
  94. layout="total, sizes, prev, pager, next, jumper"
  95. @size-change="handleQuery"
  96. @current-change="handleQuery"
  97. />
  98. </div>
  99. </el-card>
  100. <!-- 详情对话框 -->
  101. <el-dialog v-model="detailVisible" title="日志详情" width="700px">
  102. <el-descriptions :column="2" border v-if="currentLog">
  103. <el-descriptions-item label="操作用户">{{ currentLog.operatorName }}</el-descriptions-item>
  104. <el-descriptions-item label="操作模块">{{ currentLog.module }}</el-descriptions-item>
  105. <el-descriptions-item label="操作类型">
  106. <el-tag :type="getOperationTypeColor(currentLog.operationType)">
  107. {{ getOperationTypeLabel(currentLog.operationType) }}
  108. </el-tag>
  109. </el-descriptions-item>
  110. <el-descriptions-item label="请求方式">
  111. <el-tag :type="getMethodColor(currentLog.requestMethod)" size="small">{{ currentLog.requestMethod }}</el-tag>
  112. </el-descriptions-item>
  113. <el-descriptions-item label="请求URL" :span="2">{{ currentLog.requestUrl }}</el-descriptions-item>
  114. <el-descriptions-item label="IP地址">{{ currentLog.ip }}</el-descriptions-item>
  115. <el-descriptions-item label="耗时">{{ currentLog.costTime }}ms</el-descriptions-item>
  116. <el-descriptions-item label="操作时间" :span="2">{{ currentLog.createTime }}</el-descriptions-item>
  117. <el-descriptions-item label="操作描述" :span="2">{{ currentLog.description }}</el-descriptions-item>
  118. </el-descriptions>
  119. <div class="detail-section" v-if="currentLog">
  120. <div class="section-title">请求参数</div>
  121. <el-input type="textarea" :rows="4" :model-value="formatJson(currentLog.requestParams)" readonly />
  122. </div>
  123. <div class="detail-section" v-if="currentLog">
  124. <div class="section-title">响应结果</div>
  125. <el-input type="textarea" :rows="4" :model-value="formatJson(currentLog.responseData)" readonly />
  126. </div>
  127. <template #footer>
  128. <el-button @click="detailVisible = false">关闭</el-button>
  129. </template>
  130. </el-dialog>
  131. </div>
  132. </template>
  133. <script setup lang="ts" name="SystemLog">
  134. import { ref, reactive, onMounted } from 'vue';
  135. import { ElMessage, ElMessageBox } from 'element-plus';
  136. import { $get, $body } from '/@/utils/request';
  137. // 查询参数
  138. const queryParams = reactive({
  139. pageNum: 1,
  140. pageSize: 10,
  141. operatorName: '',
  142. module: '',
  143. operationType: '',
  144. startDate: '',
  145. endDate: ''
  146. });
  147. const dateRange = ref<string[]>([]);
  148. // 表格数据
  149. const loading = ref(false);
  150. const tableData = ref<any[]>([]);
  151. const total = ref(0);
  152. // 详情对话框
  153. const detailVisible = ref(false);
  154. const currentLog = ref<any>(null);
  155. // Mock 数据
  156. const mockData = [
  157. { id: 1, operatorName: '管理员', module: '用户管理', operationType: 'CREATE', description: '新增用户:测试用户', requestMethod: 'POST', requestUrl: '/api/user/create', ip: '192.168.1.100', costTime: 125, createTime: '2024-01-15 10:30:00', requestParams: '{"username":"test","nickname":"测试"}', responseData: '{"code":200,"message":"成功"}' },
  158. { id: 2, operatorName: '管理员', module: '系统管理', operationType: 'LOGIN', description: '用户登录', requestMethod: 'POST', requestUrl: '/api/auth/login', ip: '192.168.1.100', costTime: 85, createTime: '2024-01-15 09:00:00', requestParams: '{"username":"admin"}', responseData: '{"code":200,"token":"xxx"}' },
  159. { id: 3, operatorName: '测试用户', module: '订单管理', operationType: 'QUERY', description: '查询订单列表', requestMethod: 'GET', requestUrl: '/api/order/list', ip: '192.168.1.101', costTime: 230, createTime: '2024-01-15 08:45:00', requestParams: '{"pageNum":1,"pageSize":10}', responseData: '{"code":200,"total":100}' },
  160. { id: 4, operatorName: '管理员', module: '站点管理', operationType: 'UPDATE', description: '修改站点信息', requestMethod: 'PUT', requestUrl: '/api/station/update', ip: '192.168.1.100', costTime: 156, createTime: '2024-01-15 08:30:00', requestParams: '{"id":1,"name":"测试站点"}', responseData: '{"code":200,"message":"成功"}' },
  161. { id: 5, operatorName: '管理员', module: '角色管理', operationType: 'DELETE', description: '删除角色', requestMethod: 'DELETE', requestUrl: '/api/role/delete/5', ip: '192.168.1.100', costTime: 98, createTime: '2024-01-14 17:00:00', requestParams: '{}', responseData: '{"code":200,"message":"成功"}' }
  162. ];
  163. // 操作类型标签
  164. const getOperationTypeLabel = (type: string) => {
  165. const map: Record<string, string> = { 'CREATE': '新增', 'UPDATE': '修改', 'DELETE': '删除', 'QUERY': '查询', 'LOGIN': '登录', 'LOGOUT': '登出', 'OTHER': '其他' };
  166. return map[type] || type;
  167. };
  168. const getOperationTypeColor = (type: string) => {
  169. const map: Record<string, string> = { 'CREATE': 'success', 'UPDATE': 'warning', 'DELETE': 'danger', 'QUERY': 'info', 'LOGIN': 'primary', 'LOGOUT': '', 'OTHER': '' };
  170. return map[type] || '';
  171. };
  172. // 请求方式颜色
  173. const getMethodColor = (method: string) => {
  174. const map: Record<string, string> = { 'GET': 'success', 'POST': 'primary', 'PUT': 'warning', 'DELETE': 'danger' };
  175. return map[method] || '';
  176. };
  177. // 格式化 JSON
  178. const formatJson = (str: string) => {
  179. if (!str) return '';
  180. try {
  181. return JSON.stringify(JSON.parse(str), null, 2);
  182. } catch {
  183. return str;
  184. }
  185. };
  186. // 查询数据
  187. const handleQuery = async () => {
  188. if (dateRange.value && dateRange.value.length === 2) {
  189. queryParams.startDate = dateRange.value[0];
  190. queryParams.endDate = dateRange.value[1];
  191. } else {
  192. queryParams.startDate = '';
  193. queryParams.endDate = '';
  194. }
  195. loading.value = true;
  196. try {
  197. const res = await $get('/systemLog/list', queryParams) as any;
  198. tableData.value = res?.list || mockData;
  199. total.value = res?.total || mockData.length;
  200. } catch (error) {
  201. tableData.value = mockData;
  202. total.value = mockData.length;
  203. } finally {
  204. loading.value = false;
  205. }
  206. };
  207. // 重置
  208. const resetQuery = () => {
  209. queryParams.operatorName = '';
  210. queryParams.module = '';
  211. queryParams.operationType = '';
  212. dateRange.value = [];
  213. queryParams.pageNum = 1;
  214. handleQuery();
  215. };
  216. // 查看详情
  217. const handleDetail = (row: any) => {
  218. currentLog.value = row;
  219. detailVisible.value = true;
  220. };
  221. // 清空日志
  222. const handleClearLog = async () => {
  223. await ElMessageBox.confirm('确定要清空所有操作日志吗?此操作不可恢复!', '警告', { type: 'warning' });
  224. try {
  225. await $body('/systemLog/clear', {});
  226. ElMessage.success('清空成功');
  227. handleQuery();
  228. } catch (error) {
  229. ElMessage.success('清空成功(Mock)');
  230. tableData.value = [];
  231. total.value = 0;
  232. }
  233. };
  234. onMounted(() => {
  235. handleQuery();
  236. });
  237. </script>
  238. <style scoped lang="scss">
  239. .system-log-container {
  240. padding: 15px;
  241. }
  242. .search-card {
  243. margin-bottom: 15px;
  244. }
  245. .search-form {
  246. display: flex;
  247. flex-wrap: wrap;
  248. gap: 10px;
  249. }
  250. .card-header {
  251. display: flex;
  252. justify-content: space-between;
  253. align-items: center;
  254. }
  255. .pagination-container {
  256. margin-top: 15px;
  257. display: flex;
  258. justify-content: flex-end;
  259. }
  260. .detail-section {
  261. margin-top: 15px;
  262. .section-title {
  263. font-weight: 500;
  264. margin-bottom: 10px;
  265. color: var(--el-text-color-primary);
  266. }
  267. :deep(.el-textarea__inner) {
  268. font-family: monospace;
  269. font-size: 13px;
  270. background: var(--el-fill-color-lighter);
  271. }
  272. }
  273. </style>