完成系统日志的请求

This commit is contained in:
wzclm 2025-02-20 23:14:35 +08:00
parent fb5ad57dc1
commit d5667573fa
8 changed files with 397 additions and 89 deletions

37
src/api/logs/index.js Normal file
View File

@ -0,0 +1,37 @@
import request from '@/utils/request'
/**
* 获取系统日志列表
* @param {Object} params - 查询参数
* @param {Array} [params.dateRange] - 时间范围
* @param {string} [params.type] - 日志类型
* @param {string} [params.user] - 操作人
* @param {string} [params.status] - 状态
* @returns {Promise} 返回日志列表数据
*/
export function getLogList(params) {
return request.get('/api/logs')
}
/**
* 导出日志
* @returns {Promise} 返回二进制文件流
*/
export function exportLogs() {
return request.get('/api/logs/export', {
responseType: 'arraybuffer',
headers: {
'Accept': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
}
})
}
/**
* 清理过期日志
* @param {Object} params - 清理参数
* @param {number} [params.days=30] - 保留最近多少天的日志默认30天
* @returns {Promise} 返回清理结果
*/
export function cleanupLogs(params = { days: 30 }) {
return request.post('/api/logs/cleanup', params)
}

0
src/api/roles/index.js Normal file
View File

View File

@ -36,30 +36,35 @@ service.interceptors.request.use(
// 响应拦截器
service.interceptors.response.use(
(response) => {
const res = response.data
// 如果是二进制数据,直接返回
if (response.config.responseType === 'arraybuffer' || response.config.responseType === 'blob') {
return response.data;
}
const res = response.data;
// 如果响应成功
if (res.success) {
return res
return res;
}
// 处理特定错误码
switch (res.code) {
case 401:
handleUnauthorized()
break
handleUnauthorized();
break;
case 403:
ElMessage.error('没有权限访问该资源')
break
ElMessage.error('没有权限访问该资源');
break;
case 500:
console.error('服务器错误详情:', res)
ElMessage.error(res.message || '服务器错误,请稍后重试')
break
console.error('服务器错误详情:', res);
ElMessage.error(res.message || '服务器错误,请稍后重试');
break;
default:
ElMessage.error(res.message || '请求失败')
ElMessage.error(res.message || '请求失败');
}
return Promise.reject(new Error(res.message || '请求失败'))
return Promise.reject(new Error(res.message || '请求失败'));
},
(error) => {
console.error('响应错误详情:', {
@ -73,36 +78,36 @@ service.interceptors.response.use(
headers: error.config?.headers,
params: error.config?.params
}
})
});
// 处理网络错误
if (!error.response) {
ElMessage.error('网络错误,请检查您的网络连接')
return Promise.reject(error)
ElMessage.error('网络错误,请检查您的网络连接');
return Promise.reject(error);
}
// 处理HTTP状态码错误
const status = error.response.status
const status = error.response.status;
switch (status) {
case 401:
handleUnauthorized()
break
handleUnauthorized();
break;
case 403:
ElMessage.error('没有权限访问该资源')
break
ElMessage.error('没有权限访问该资源');
break;
case 404:
ElMessage.error('请求的资源不存在')
break
ElMessage.error('请求的资源不存在');
break;
case 500:
ElMessage.error('服务器错误,请稍后重试')
break
ElMessage.error('服务器错误,请稍后重试');
break;
default:
ElMessage.error(`请求失败:${error.message}`)
ElMessage.error(`请求失败:${error.message}`);
}
return Promise.reject(error)
return Promise.reject(error);
}
)
);
// 处理未授权情况
const handleUnauthorized = () => {

40
src/utils/sort.js Normal file
View File

@ -0,0 +1,40 @@
/**
* 对数组按照指定字段进行排序
* @param {Array} array - 要排序的数组
* @param {string} field - 排序字段
* @param {string} [order='asc'] - 排序方式'asc' 升序'desc' 降序
* @returns {Array} 排序后的新数组
*/
export function sortArrayByField(array, field, order = 'asc') {
if (!Array.isArray(array) || array.length === 0) {
return array;
}
const sortedArray = [...array].sort((a, b) => {
let valueA = a[field];
let valueB = b[field];
// 处理日期类型
if (field.includes('time') || field.includes('date') || field.includes('at')) {
valueA = new Date(valueA).getTime();
valueB = new Date(valueB).getTime();
}
// 处理数字类型
else if (typeof valueA === 'number' && typeof valueB === 'number') {
return order === 'asc' ? valueA - valueB : valueB - valueA;
}
// 处理字符串类型
else {
valueA = String(valueA).toLowerCase();
valueB = String(valueB).toLowerCase();
}
if (order === 'asc') {
return valueA > valueB ? 1 : -1;
} else {
return valueA < valueB ? 1 : -1;
}
});
return sortedArray;
}

View File

@ -237,7 +237,7 @@ const icons = {
</template>
<style lang="scss" scoped>
@use "./styles/variables" as v;
@use "../../../styles/variables.scss" as *;
.data-container {
.mb-20 {

View File

@ -599,7 +599,7 @@ onUnmounted(() => {
</template>
<style lang="scss" scoped>
@use "./styles/variables" as v;
@use "../../../styles/variables.scss" as *;
.device-container {
.group-card {

View File

@ -1,63 +1,170 @@
<script setup>
import { ref, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { ref, onMounted, computed, watch } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { useSystemLogStore } from '../../../stores/systemLog';
import { markRaw } from 'vue';
import { Download, Delete } from "@element-plus/icons-vue";
import { getLogList, exportLogs, cleanupLogs } from '@/api/logs';
import { formatDateTime } from "@/utils/format";
import { sortArrayByField } from "@/utils/sort";
const systemLogStore = useSystemLogStore();
//
const logTypes = [
{ label: "用户操作", value: "用户操作" },
{ label: "系统配置", value: "系统配置" },
{ label: "数据操作", value: "数据操作" },
{ label: "异常警告", value: "异常警告" },
{ label: "GET", value: "GET" },
{ label: "POST", value: "POST" },
{ label: "PUT", value: "PUT" },
{ label: "DELETE", value: "DELETE" }
];
//
const statusOptions = [
{ label: "成功", value: "成功" },
{ label: "失败", value: "失败" },
{ label: "警告", value: "警告" },
{ label: "成功", value: 1 },
{ label: "失败", value: 0 }
];
//
const getStatusType = (status) => {
switch (status) {
case "成功":
return "success";
case "失败":
return "danger";
case "警告":
return "warning";
default:
return "info";
}
};
//
const handleExport = () => {
console.log("导出日志:", searchForm.value);
};
//
const handleClear = () => {
systemLogStore.clearLogs();
ElMessage.success('日志已清空');
};
//
const searchForm = ref({
dateRange: [],
type: "",
user: "",
status: "",
status: ""
});
//
const tableData = ref([]);
const allData = ref([]); //
const loading = ref(false);
//
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);
//
const getList = async () => {
loading.value = true;
try {
const res = await getLogList();
if (res.success) {
// 使
allData.value = res.data?.list || [];
updateTableData();
} else {
ElMessage.error(res.message || '获取日志列表失败');
}
} catch (error) {
console.error('获取日志列表错误:', error);
ElMessage.error('获取日志列表失败');
} finally {
loading.value = false;
}
};
//
const updateTableData = () => {
tableData.value = paginatedData.value;
total.value = filteredData.value.length;
};
//
const handleExport = async () => {
try {
loading.value = true;
const res = await exportLogs();
// JSON
if (res instanceof ArrayBuffer && res.byteLength < 1000) { //
const text = new TextDecoder().decode(res);
try {
const errorData = JSON.parse(text);
throw new Error(errorData.message || '导出失败');
} catch (e) {
if (e instanceof SyntaxError) {
// JSON
} else {
throw e;
}
}
}
//
const blob = new Blob([res], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
// blob
if (blob.size === 0) {
throw new Error('导出的文件为空');
}
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.style.display = 'none';
link.href = url;
link.download = `智慧湿地管理平台系统日志_${formatDateTime(new Date(), 'YYYY年MM月DD日')}.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
ElMessage.success('导出成功');
} catch (error) {
console.error('导出日志错误:', error);
ElMessage.error(error.message || '导出失败,请稍后重试');
} finally {
loading.value = false;
}
};
//
const handleClearLogs = () => {
ElMessageBox.confirm(
`<p>此操作将清理30天前的所有日志记录请确认</p>
<ul style="margin: 10px 0; padding-left: 20px;">
<li>系统将保留最近30天的日志记录</li>
<li>被清理的日志无法恢复</li>
<li>建议在清理前先导出日志备份</li>
</ul>`,
'清理确认',
{
confirmButtonText: '确定清理',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: true
}
).then(async () => {
try {
loading.value = true;
await cleanupLogs({ days: 30 });
ElMessage.success('日志清理成功');
getList(); //
} catch (error) {
console.error('清理日志错误:', error);
const errorMsg = error.response?.data?.message || '清理日志失败,请稍后重试';
ElMessage.error(errorMsg);
} finally {
loading.value = false;
}
}).catch(() => {
//
ElMessage.info('已取消清理操作');
});
};
//
const handleSearchClear = (field) => {
searchForm.value[field] = field === 'dateRange' ? [] : '';
handleSearch(); //
};
//
watch([searchForm, currentPage, pageSize], () => {
updateTableData();
}, { deep: true });
//
const resetSearch = () => {
@ -65,14 +172,81 @@ const resetSearch = () => {
dateRange: [],
type: "",
user: "",
status: "",
status: ""
};
currentPage.value = 1; //
updateTableData();
};
//
const handleSearch = () => {
currentPage.value = 1; //
updateTableData();
};
//
const handleSizeChange = (val) => {
pageSize.value = val;
updateTableData();
};
const handleCurrentChange = (val) => {
currentPage.value = val;
updateTableData();
};
//
onMounted(() => {
getList();
});
const icons = {
Download: markRaw(Download),
Delete: markRaw(Delete)
};
//
const filteredData = computed(() => {
let result = [...allData.value];
//
if (searchForm.value.dateRange?.length === 2) {
const startTime = new Date(searchForm.value.dateRange[0]).getTime();
const endTime = new Date(searchForm.value.dateRange[1]).getTime();
result = result.filter(item => {
const itemTime = new Date(item.created_at).getTime();
return itemTime >= startTime && itemTime <= endTime;
});
}
//
if (searchForm.value.type) {
result = result.filter(item => item.request_method === searchForm.value.type);
}
//
if (searchForm.value.user) {
const keyword = searchForm.value.user.toLowerCase();
result = result.filter(item =>
item.user_id?.toString().toLowerCase().includes(keyword)
);
}
//
if (searchForm.value.status !== '') {
result = result.filter(item => item.status === searchForm.value.status);
}
//
return sortArrayByField(result, 'created_at', 'asc');
});
//
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize.value;
const end = start + pageSize.value;
return filteredData.value.slice(start, end);
});
</script>
<template>
@ -86,7 +260,7 @@ const icons = {
<el-icon><component :is="icons.Download" /></el-icon>
导出日志
</el-button>
<el-button type="danger" @click="handleClear">
<el-button type="danger" @click="handleClearLogs">
<el-icon><component :is="icons.Delete" /></el-icon>
清空日志
</el-button>
@ -104,10 +278,16 @@ const icons = {
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
@clear="handleSearchClear('dateRange')"
/>
</el-form-item>
<el-form-item label="日志类型">
<el-select v-model="searchForm.type" placeholder="请选择" clearable>
<el-select
v-model="searchForm.type"
placeholder="请选择"
clearable
@clear="handleSearchClear('type')"
>
<el-option
v-for="item in logTypes"
:key="item.value"
@ -117,10 +297,20 @@ const icons = {
</el-select>
</el-form-item>
<el-form-item label="操作人">
<el-input v-model="searchForm.user" placeholder="请输入" clearable />
<el-input
v-model="searchForm.user"
placeholder="请输入"
clearable
@clear="handleSearchClear('user')"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择" clearable>
<el-select
v-model="searchForm.status"
placeholder="请选择"
clearable
@clear="handleSearchClear('status')"
>
<el-option
v-for="item in statusOptions"
:key="item.value"
@ -130,32 +320,54 @@ const icons = {
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary">搜索</el-button>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button>
</el-form-item>
</el-form>
<!-- 日志表格 -->
<el-table :data="systemLogStore.getLogs" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="type" label="日志类型" width="120" />
<el-table-column prop="user" label="操作人" width="120" />
<el-table-column prop="action" label="操作" width="120" />
<el-table-column prop="ip" label="IP地址" width="140" />
<el-table-column prop="status" label="状态" width="100">
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
>
<el-table-column prop="id" label="ID" width="80" align="center"/>
<el-table-column prop="request_method" label="请求方法" width="100" align="center">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">
{{ row.status }}
<el-tag
:type="row.request_method === 'GET' ? 'success' :
row.request_method === 'POST' ? 'primary' :
row.request_method === 'PUT' ? 'warning' :
row.request_method === 'DELETE' ? 'danger' : 'info'"
size="small"
>
{{ row.request_method }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="detail"
label="详细信息"
min-width="200"
show-overflow-tooltip
/>
<el-table-column prop="createTime" label="操作时间" width="180" />
<el-table-column prop="action" label="操作行为" width="200" align="center"/>
<el-table-column prop="resource_type" label="资源类型" width="120" align="center"/>
<el-table-column prop="request_url" label="请求URL" min-width="180" show-overflow-tooltip align="center"/>
<el-table-column prop="ip_address" label="IP地址" width="140" align="center"/>
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">
{{ row.status === 1 ? '成功' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="error_message" label="错误信息" min-width="200" show-overflow-tooltip align="center">
<template #default="{ row }">
<span v-if="row.status === 0" class="error-message" style="color: #f56c6c">
{{ row.error_message || '未知错误' }}
</span>
</template>
</el-table-column>
<el-table-column prop="created_at" label="操作时间" width="180" align="center">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
</el-table-column>
</el-table>
<!-- 分页器 -->
@ -163,9 +375,12 @@ const icons = {
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
:total="systemLogStore.getLogs.length"
:background="true"
layout="total, sizes, prev, pager, next"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
@ -173,7 +388,7 @@ const icons = {
</template>
<style lang="scss" scoped>
@use "./styles/variables" as v;
@use "../../../styles/variables.scss" as *;
.logs-container {
.card-header {
@ -198,12 +413,23 @@ const icons = {
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid $border-color;
:deep(.el-input),
:deep(.el-select) {
width: 240px;
}
}
.pagination-container {
margin-top: 20px;
display: flex;
justify-content: flex-end;
:deep(.el-pagination) {
padding: 0;
margin: 0;
font-weight: normal;
}
}
:deep(.el-card) {

View File

@ -236,7 +236,7 @@ const handleDelete = (row) => {
</template>
<style lang="scss" scoped>
@use "./styles/variables" as v;
@use "../../../styles/variables.scss" as *;
.permission-container {
.el-card {