Commit 64d4aa74 by liucan

feat:状态监控页面新增错误日志统计图表和详情弹窗

parent 53eea9e0
...@@ -27,6 +27,7 @@ export const scrapydApi = { ...@@ -27,6 +27,7 @@ export const scrapydApi = {
projectLogs: "/scrapyd/projectLogs", projectLogs: "/scrapyd/projectLogs",
spiderLogs: "/scrapyd/spiderLogs", spiderLogs: "/scrapyd/spiderLogs",
jobLog: "/scrapyd/jobLog", jobLog: "/scrapyd/jobLog",
errorLogs: "/errorLogs/list",
} as const; } as const;
export const scheduleApi = { export const scheduleApi = {
......
import { request, POST } from '@/utils/request' import { request, POST } from "@/utils/request";
import type { ApiResponse, QueryParams } from '@/utils/request' import type { ApiResponse, QueryParams } from "@/utils/request";
import { scrapydApi } from './apiPaths' import { scrapydApi } from "./apiPaths";
// 获取日志列表 // 获取日志列表
export function getLogs(data: QueryParams): Promise<ApiResponse> { export function getLogs(data: QueryParams): Promise<ApiResponse> {
return request({ return request({
url: scrapydApi.logs, url: scrapydApi.logs,
method: POST, method: POST,
data data,
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>;
} }
// 获取项目日志 // 获取项目日志
...@@ -16,8 +16,8 @@ export function getProjectLogs(data: QueryParams): Promise<ApiResponse> { ...@@ -16,8 +16,8 @@ export function getProjectLogs(data: QueryParams): Promise<ApiResponse> {
return request({ return request({
url: scrapydApi.projectLogs, url: scrapydApi.projectLogs,
method: POST, method: POST,
data data,
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>;
} }
// 获取爬虫日志 // 获取爬虫日志
...@@ -25,8 +25,8 @@ export function getSpiderLogs(data: QueryParams): Promise<ApiResponse> { ...@@ -25,8 +25,8 @@ export function getSpiderLogs(data: QueryParams): Promise<ApiResponse> {
return request({ return request({
url: scrapydApi.spiderLogs, url: scrapydApi.spiderLogs,
method: POST, method: POST,
data data,
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>;
} }
// 获取任务日志 // 获取任务日志
...@@ -34,6 +34,15 @@ export function getJobLog(data: QueryParams): Promise<ApiResponse> { ...@@ -34,6 +34,15 @@ export function getJobLog(data: QueryParams): Promise<ApiResponse> {
return request({ return request({
url: scrapydApi.jobLog, url: scrapydApi.jobLog,
method: POST, method: POST,
data data,
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>;
} }
//获取错误日志列表
export function getErrorLogsList(): Promise<ApiResponse> {
return request({
url: scrapydApi.errorLogs,
method: POST,
data: null,
}) as unknown as Promise<ApiResponse>;
}
\ No newline at end of file
import { request, POST } from '@/utils/request' import { request, POST } from "@/utils/request";
import type { ApiResponse, QueryParams } from '@/utils/request' import type { ApiResponse, QueryParams } from "@/utils/request";
import { dataApi } from './apiPaths' import { dataApi } from "./apiPaths";
// 获取爬虫静态数据列表 // 获取爬虫静态数据列表
export function getStatsDataList(params: QueryParams): Promise<ApiResponse> { export function getStatsDataList(params: QueryParams): Promise<ApiResponse> {
return request({ return request({
url: dataApi.dataStatistics, url: dataApi.dataStatistics,
method: POST, method: POST,
params params,
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>;
} }
// 获取爬虫任务列表 // 获取爬虫任务列表
...@@ -16,8 +16,8 @@ export function getSpiderTaskList(params: QueryParams): Promise<ApiResponse> { ...@@ -16,8 +16,8 @@ export function getSpiderTaskList(params: QueryParams): Promise<ApiResponse> {
return request({ return request({
url: dataApi.taskStatistics, url: dataApi.taskStatistics,
method: POST, method: POST,
params params,
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>;
} }
// 获取性能统计列表 // 获取性能统计列表
...@@ -25,8 +25,8 @@ export function getPerformanceList(params: QueryParams): Promise<ApiResponse> { ...@@ -25,8 +25,8 @@ export function getPerformanceList(params: QueryParams): Promise<ApiResponse> {
return request({ return request({
url: dataApi.performanceStatistics, url: dataApi.performanceStatistics,
method: POST, method: POST,
params params,
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>;
} }
// 获取爬虫任务统计列表 // 获取爬虫任务统计列表
...@@ -34,6 +34,6 @@ export function getAllSpiderTaskStatistics(params: QueryParams): Promise<ApiResp ...@@ -34,6 +34,6 @@ export function getAllSpiderTaskStatistics(params: QueryParams): Promise<ApiResp
return request({ return request({
url: dataApi.allSpiderTaskStatistics, url: dataApi.allSpiderTaskStatistics,
method: POST, method: POST,
params params,
}) as unknown as Promise<ApiResponse> }) as unknown as Promise<ApiResponse>;
} }
\ No newline at end of file
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
v-model:current-page="currentPage" v-model:current-page="currentPage"
v-model:page-size="pageSize" v-model:page-size="pageSize"
:background="true" :background="true"
:page-sizes="[10]" :page-sizes="[5, 10]"
:pager-count="pagerCount" :pager-count="pagerCount"
:total="total" :total="total"
class="mt-4" class="mt-4"
...@@ -45,7 +45,7 @@ const props = defineProps({ ...@@ -45,7 +45,7 @@ const props = defineProps({
// 每页显示条目个数:pageSize // 每页显示条目个数:pageSize
limit: { limit: {
type: Number, type: Number,
default: 20, default: 10,
}, },
// 设置最大页码按钮数。 页码按钮的数量,当总页数超过该值时会折叠 // 设置最大页码按钮数。 页码按钮的数量,当总页数超过该值时会折叠
// 移动端页码按钮的数量端默认值 5 // 移动端页码按钮的数量端默认值 5
...@@ -86,4 +86,4 @@ const handleCurrentChange = (val: number) => { ...@@ -86,4 +86,4 @@ const handleCurrentChange = (val: number) => {
// 触发 pagination 事件,重新加载列表 // 触发 pagination 事件,重新加载列表
emit("pagination", { page: val, limit: pageSize.value }); emit("pagination", { page: val, limit: pageSize.value });
}; };
</script> </script>
\ No newline at end of file
import { markRaw, onMounted, onUnmounted, ref, type Ref } from "vue";
import * as echarts from "echarts";
//封装图表渲染逻辑
export function useChart(chartRef: Ref<HTMLElement | null>, setChartData: any, handleClickChart?: any) {
const chartInstance = ref<echarts.ECharts | null>(null);
const initChart = async () => {
if (chartRef.value) {
//这里echarts内部已经做了响应式处理 会和vue的响应式冲突 需要取消vue的响应式
chartInstance.value = markRaw(echarts.init(chartRef.value));
const options = await setChartData();
chartInstance.value.setOption(options);
//绑定图表点击事件
if (handleClickChart) {
chartInstance.value?.on("click", handleClickChart);
}
}
};
const resizeChart = () => {
chartInstance.value?.resize();
};
onMounted(() => {
initChart();
window.addEventListener("resize", resizeChart);
});
onUnmounted(() => {
window.removeEventListener("resize", resizeChart);
if (chartInstance.value) {
chartInstance.value.dispose();
}
});
}
...@@ -136,4 +136,13 @@ ...@@ -136,4 +136,13 @@
.el-input__inner{ .el-input__inner{
color: white; color: white;
}
.el-select__placeholder{
color: #ccc !important;
}
/* 修改el输入框的样式 */
.el-input__wrapper {
background-color: #1d5484 !important;
box-shadow: none !important;
} }
\ No newline at end of file
...@@ -142,7 +142,7 @@ onMounted(() => { ...@@ -142,7 +142,7 @@ onMounted(() => {
background-repeat: no-repeat; background-repeat: no-repeat;
// background: #c6ebfc; // background: #c6ebfc;
// border: 1.5px solid rgb(193, 188, 188); // border: 1.5px solid rgb(193, 188, 188);
width: 500px; width: 350px;
border-radius: 5px; border-radius: 5px;
} }
......
<template>
<div class="dataCard">
<div class="titleStyle">错误日志统计</div>
<div class="chart-container" ref="chartRef"></div>
<el-dialog draggable v-model="DialogVisible" :title="`日志详情-${curDate}`" @close="tableData = []">
<div class="table-content">
<el-table :data="curTableData" border style="width: 100%">
<el-table-column width="55" label="序号" type="index" />
<el-table-column width="150" property="spider" label="爬虫名称" />
<el-table-column property="error" label="错误信息" />
<el-table-column width="160" property="timestamp" label="发生时间" />
</el-table>
<div class="pagination w-full flex flex-row-reverse pr-4 m-t-4">
<Pagination
:total="pageObj.total"
v-model:page="pageObj.pageNo"
v-model:limit="pageObj.pageSize"
@pagination="getData"
v-model:pagerCount="pageObj.pagerCount"
/>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { computed, reactive, ref, useTemplateRef } from "vue";
import { getErrorLogsList } from "@/api/log";
import { useChart } from "@/hooks/useChart";
import { ElMessage } from "element-plus";
import Pagination from "@/components/pagination/index.vue";
const chartRef = useTemplateRef<HTMLElement | null>("chartRef");
//详情弹窗状态
const DialogVisible = ref(false);
const pageObj = ref({
total: 0,
pageNo: 1,
pageSize: 5,
pagerCount: 5,
});
//弹窗表格数据
const tableData = ref<any[]>([]);
const curTableData = ref<any[]>([]);
//数据项定义
interface LogItem {
date: string;
count: number;
}
//原始数据
const rawLogList = ref<any[]>([]);
//日志列表
const logList = ref<LogItem[]>([]);
//日志数
const logCountList = computed(() => {
return logList.value.map((item) => item.count);
});
//x轴日期列表
const dateList = ref<string[]>([]);
//Y轴最大值
const maxVal = computed(() => {
let val = logList.value[0]?.count * 1.5;
logList.value.forEach((item) => {
if (item.count > val) {
val = item.count * 1.5;
}
});
return val;
});
//当前打开的详情日期
const curDate = ref("");
const getData = () => {
curTableData.value = tableData.value.slice(
(pageObj.value.pageNo - 1) * pageObj.value.pageSize,
pageObj.value.pageNo * pageObj.value.pageSize
);
};
//设置图表数据
const setChartData = async () => {
try {
const res = await getErrorLogsList();
if (res.code === 0) {
rawLogList.value = res.data.list;
//提取日期列表
res.data.list.forEach((item: any) => {
const date = item.timestamp.split(" ")[0];
if (dateList.value.includes(date)) {
//如果该日期日志项存在,则进行+1操作
logList.value.forEach((item) => {
if (item.date === date) {
item.count++;
}
});
} else {
//如果该日期日志项不存在,则初始化到数组中
logList.value.push({ date, count: 1 });
dateList.value.push(date);
}
});
} else {
ElMessage.error(res.message);
}
} catch (error: any) {
ElMessage(error);
}
//初始化图表配置项
const chartOptions = reactive({
tooltip: {
trigger: "axis",
axisPointer: {
type: "shadow",
},
},
grid: {
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true,
},
xAxis: [
{
type: "category",
data: dateList.value,
name: "日期",
nameLocation: "middle",
nameTextStyle: {
color: "#fff",
fontSize: 12,
},
axisTick: {
alignWithLabel: true,
},
axisLabel: {
color: "#fff",
},
},
],
yAxis: [
{
type: "value",
name: "数量",
nameLocation: "middle",
max: maxVal.value,
axisLabel: {
color: "#fff",
},
nameTextStyle: {
color: "#fff",
fontSize: 12,
},
},
],
series: [
{
name: "日志数量",
type: "bar",
barWidth: "60%",
data: logCountList.value,
itemStyle: {
color: "#409EE4",
},
},
],
});
return chartOptions;
};
//处理用户点击柱状图事件
const handleClickChart = (params: any) => {
DialogVisible.value = true;
curDate.value = params.name;
rawLogList.value.forEach((item) => {
if (item.timestamp.split(" ")[0] === params.name) {
tableData.value.push(item);
}
});
pageObj.value.total = tableData.value.length;
curTableData.value = tableData.value.slice(0, 5);
};
//生成图表
useChart(chartRef, setChartData, handleClickChart);
</script>
<style scoped lang="scss">
.dataCard {
background-image: url("@/assets/picture/box2.png");
background-size: 100% 100%;
background-repeat: no-repeat;
// background: #c6ebfc;
// border: 1.5px solid rgb(193, 188, 188);
border-radius: 5px;
height: 100%;
padding: 35px;
box-sizing: border-box;
}
/* 标题区 */
.titleStyle {
color: #dff6ff;
font-size: 18px;
font-weight: 600;
text-align: center;
text-shadow: 0 0 8px rgba(100, 200, 255, 0.8);
letter-spacing: 2px;
}
.chart-container {
margin-top: 20px;
width: 100%;
height: 600px;
}
.cotnent {
padding: 10px;
display: flex;
}
.table-content {
padding: 10px;
margin-top: 30px;
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(8px);
border: 1px solid rgba(78, 237, 255, 0.2);
box-shadow: 0 0 15px rgba(78, 237, 255, 0.1);
border-radius: 8px;
padding: 15px;
}
</style>
...@@ -138,7 +138,7 @@ const goToESADataPage = () => { ...@@ -138,7 +138,7 @@ const goToESADataPage = () => {
background-repeat: no-repeat; background-repeat: no-repeat;
// background: #c6ebfc; // background: #c6ebfc;
// border: 1.5px solid rgb(193, 188, 188); // border: 1.5px solid rgb(193, 188, 188);
width: 375px; width: 263px;
height: 100%; height: 100%;
border-radius: 5px; border-radius: 5px;
display: flex; display: flex;
......
<template> <template>
<div> <div class="monitor-container">
<div class="card-container"> <div class="card-container">
<div class="monitoringCard"> <div class="monitoringCard">
<div class="data-card-wrapper"> <div class="data-card-wrapper">
...@@ -17,6 +17,9 @@ ...@@ -17,6 +17,9 @@
</div> </div>
</div> </div>
</div> </div>
<div class="monitor-chart">
<errorMessageChart />
</div>
</div> </div>
</template> </template>
...@@ -24,6 +27,7 @@ ...@@ -24,6 +27,7 @@
import dataCard from "./dataCard.vue"; import dataCard from "./dataCard.vue";
import taskCard from "@/views/os-status/components/taskCard.vue"; import taskCard from "@/views/os-status/components/taskCard.vue";
import qbCard from "@/views/os-status/components/qbDataMonitor.vue"; import qbCard from "@/views/os-status/components/qbDataMonitor.vue";
import errorMessageChart from "./errorMessageChart.vue";
import { onMounted } from "vue"; import { onMounted } from "vue";
defineProps({ defineProps({
...@@ -41,6 +45,15 @@ onMounted(() => {}); ...@@ -41,6 +45,15 @@ onMounted(() => {});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.monitor-container {
display: flex;
}
.monitor-chart {
color: white;
width: 30%;
}
.card-container { .card-container {
height: 100%; height: 100%;
display: flex; display: flex;
...@@ -49,6 +62,7 @@ onMounted(() => {}); ...@@ -49,6 +62,7 @@ onMounted(() => {});
align-items: center; align-items: center;
padding: 10px; padding: 10px;
gap: 30px; gap: 30px;
width: 70%;
} }
.monitoringCard { .monitoringCard {
......
...@@ -76,7 +76,9 @@ ...@@ -76,7 +76,9 @@
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
<span>任务执行成功统计:</span> <span>任务执行成功统计:</span>
<span class="total-num" v-if="stTaskSuccessNumber != null">{{ formatExactLargeNum(stTaskSuccessNumber) }}</span> <span class="total-num" v-if="stTaskSuccessNumber != null">{{
formatExactLargeNum(stTaskSuccessNumber)
}}</span>
<span class="loading" v-else></span> <span class="loading" v-else></span>
</div> </div>
<div class="wordStyle"> <div class="wordStyle">
...@@ -183,7 +185,7 @@ onMounted(() => { ...@@ -183,7 +185,7 @@ onMounted(() => {
background-repeat: no-repeat; background-repeat: no-repeat;
// background: #c6ebfc; // background: #c6ebfc;
// border: 1.5px solid rgb(193, 188, 188); // border: 1.5px solid rgb(193, 188, 188);
width: 500px; width: 350px;
height: 100%; height: 100%;
border-radius: 5px; border-radius: 5px;
cursor: pointer; cursor: pointer;
......
...@@ -123,7 +123,7 @@ const socket = ref<Socket | null>(null); ...@@ -123,7 +123,7 @@ const socket = ref<Socket | null>(null);
//错误消息通知 //错误消息通知
const handleErrorMsgNotify = () => { const handleErrorMsgNotify = () => {
//websocket连接 //websocket连接
const manager = new Manager("http://192.168.3.10:5001"); const manager = new Manager("/api:5001");
socket.value = manager.socket("/"); socket.value = manager.socket("/");
//收到错误通知 //收到错误通知
socket.value.on("error_message", onErrorMsg); socket.value.on("error_message", onErrorMsg);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment