選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 
 

146 行
5.4 KiB

  1. {% extends "../layout.html" %}
  2. {% block title %}操作日志{% endblock %}
  3. {% block content %}
  4. <div id="logApp" v-cloak>
  5. <el-card shadow="never">
  6. <template #header>
  7. <div class="flex items-center justify-between">
  8. <span class="font-medium">操作日志</span>
  9. <!-- <el-button type="danger" size="small" @click="clearLogs">清理日志</el-button> -->
  10. </div>
  11. </template>
  12. <!-- 筛选栏 -->
  13. <div class="flex items-center gap-4 mb-4 flex-wrap">
  14. <el-input v-model="query.keyword" placeholder="搜索用户/描述..." style="width:180px;" clearable @keyup.enter="loadList"></el-input>
  15. <el-select v-model="query.action" placeholder="操作类型" style="width:120px;" clearable>
  16. <el-option label="登录" value="login"></el-option>
  17. <el-option label="退出" value="logout"></el-option>
  18. <el-option label="新增" value="add"></el-option>
  19. <el-option label="编辑" value="edit"></el-option>
  20. <el-option label="删除" value="delete"></el-option>
  21. <el-option label="导出" value="export"></el-option>
  22. </el-select>
  23. <el-date-picker v-model="dateRange" type="daterange" range-separator="至"
  24. start-placeholder="开始日期" end-placeholder="结束日期"
  25. value-format="YYYY-MM-DD" style="width:240px;" :teleported="false"></el-date-picker>
  26. <el-button type="primary" @click="loadList">搜索</el-button>
  27. <el-button @click="resetQuery">重置</el-button>
  28. </div>
  29. <!-- 信息栏 -->
  30. <div class="flex items-center gap-3 mb-3 text-sm text-gray-500">
  31. <span>共 <b class="text-orange-500">${ total }</b> 条记录</span>
  32. </div>
  33. <!-- 表格 -->
  34. <el-table :data="list" v-loading="loading" border>
  35. <el-table-column prop="id" label="ID" width="70"></el-table-column>
  36. <el-table-column prop="username" label="操作人" width="100"></el-table-column>
  37. <el-table-column prop="action" label="类型" width="80">
  38. <template #default="{ row }">
  39. <el-tag :type="actionMap[row.action]?.type || 'info'" size="small">${ actionMap[row.action]?.text || row.action }</el-tag>
  40. </template>
  41. </el-table-column>
  42. <el-table-column prop="module" label="模块" width="100"></el-table-column>
  43. <el-table-column prop="description" label="操作描述" min-width="260">
  44. <template #default="{ row }">
  45. <span class="text-gray-600">${ row.description }</span>
  46. </template>
  47. </el-table-column>
  48. <el-table-column prop="ip" label="IP地址" width="130"></el-table-column>
  49. <el-table-column prop="create_time" label="操作时间" width="170">
  50. <template #default="{ row }">${ row.create_time?.slice(0,19) }</template>
  51. </el-table-column>
  52. </el-table>
  53. <!-- 分页 -->
  54. <div class="flex justify-end mt-4">
  55. <el-pagination background layout="total, sizes, prev, pager, next" :total="total" v-model:page-size="pageSize" :page-sizes="[15, 30, 50, 100]" v-model:current-page="page" @current-change="loadList" @size-change="onSizeChange"></el-pagination>
  56. </div>
  57. </el-card>
  58. </div>
  59. {% endblock %}
  60. {% block js %}
  61. <script>
  62. const { createApp, ref, reactive, onMounted } = Vue;
  63. const app = createApp({
  64. delimiters: ['${', '}'],
  65. setup() {
  66. const loading = ref(false);
  67. const list = ref([]);
  68. const total = ref(0);
  69. const page = ref(1);
  70. const pageSize = ref(15);
  71. const query = reactive({ keyword: '', action: '' });
  72. const dateRange = ref(null);
  73. const actionMap = {
  74. login: { type: 'success', text: '登录' },
  75. logout: { type: 'info', text: '退出' },
  76. add: { type: 'primary', text: '新增' },
  77. edit: { type: 'warning', text: '编辑' },
  78. delete: { type: 'danger', text: '删除' },
  79. export: { type: '', text: '导出' }
  80. };
  81. async function loadList() {
  82. loading.value = true;
  83. try {
  84. const params = new URLSearchParams({
  85. page: page.value, pageSize: pageSize.value,
  86. keyword: query.keyword, action: query.action,
  87. startDate: dateRange.value ? dateRange.value[0] : '',
  88. endDate: dateRange.value ? dateRange.value[1] : ''
  89. });
  90. const res = await fetch('/admin/system/log/list?' + params).then(r => r.json());
  91. if (res.code === 0) {
  92. list.value = res.data.data || [];
  93. total.value = res.data.count || 0;
  94. }
  95. } finally { loading.value = false; }
  96. }
  97. function resetQuery() {
  98. query.keyword = '';
  99. query.action = '';
  100. dateRange.value = null;
  101. page.value = 1;
  102. loadList();
  103. }
  104. function onSizeChange() {
  105. page.value = 1;
  106. loadList();
  107. }
  108. async function clearLogs() {
  109. try {
  110. await ElementPlus.ElMessageBox.confirm('确定清理30天前的日志?此操作不可恢复。', '清理日志', { type: 'warning' });
  111. const res = await fetch('/admin/system/log/clear', {
  112. method: 'POST',
  113. headers: { 'Content-Type': 'application/json' },
  114. body: JSON.stringify({ days: 30 })
  115. }).then(r => r.json());
  116. if (res.code === 0) {
  117. ElementPlus.ElMessage.success(res.msg);
  118. loadList();
  119. }
  120. } catch {}
  121. }
  122. onMounted(() => loadList());
  123. return { loading, list, total, page, pageSize, query, dateRange, actionMap, loadList, resetQuery, onSizeChange, clearLogs };
  124. }
  125. });
  126. app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
  127. app.mount('#logApp');
  128. </script>
  129. {% endblock %}