提交 a1c20afb 编写于 作者: 查尔斯-BUG万象集's avatar 查尔斯-BUG万象集

feat: 完善仪表盘访问趋势区块内容

上级 dc1691f0
......@@ -19,8 +19,11 @@ package top.charles7c.cnadmin.monitor.mapper;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Param;
import top.charles7c.cnadmin.common.base.BaseMapper;
import top.charles7c.cnadmin.monitor.model.entity.LogDO;
import top.charles7c.cnadmin.monitor.model.vo.DashboardAccessTrendVO;
import top.charles7c.cnadmin.monitor.model.vo.DashboardPopularModuleVO;
import top.charles7c.cnadmin.monitor.model.vo.DashboardTotalVO;
......@@ -39,9 +42,19 @@ public interface LogMapper extends BaseMapper<LogDO> {
*/
DashboardTotalVO selectDashboardTotal();
/**
* 查询仪表盘访问趋势信息
*
* @param days
* 日期数
*
* @return 仪表盘访问趋势信息
*/
List<DashboardAccessTrendVO> selectListDashboardAccessTrend(@Param("days") Integer days);
/**
* 查询仪表盘热门模块列表
*
*
* @return 仪表盘热门模块列表
*/
List<DashboardPopularModuleVO> selectListDashboardPopularModule();
......
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.charles7c.cnadmin.monitor.model.vo;
import java.io.Serializable;
import lombok.Data;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* 仪表盘-访问趋势信息
*
* @author Charles7c
* @since 2023/9/9 20:20
*/
@Data
@Schema(description = "仪表盘-访问趋势信息")
public class DashboardAccessTrendVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 日期
*/
@Schema(description = "日期", example = "2023-08-08")
private String date;
/**
* 浏览量(PV)
*/
@Schema(description = "浏览量(PV)", example = "1000")
private Long pvCount;
/**
* IP 数
*/
@Schema(description = "IP 数", example = "500")
private Long ipCount;
}
......@@ -18,6 +18,7 @@ package top.charles7c.cnadmin.monitor.service;
import java.util.List;
import top.charles7c.cnadmin.monitor.model.vo.DashboardAccessTrendVO;
import top.charles7c.cnadmin.monitor.model.vo.DashboardGeoDistributionVO;
import top.charles7c.cnadmin.monitor.model.vo.DashboardPopularModuleVO;
import top.charles7c.cnadmin.monitor.model.vo.DashboardTotalVO;
......@@ -38,16 +39,25 @@ public interface DashboardService {
*/
DashboardTotalVO getTotal();
/**
* 查询访问趋势信息
*
* @param days
* 日期数
* @return 访问趋势信息
*/
List<DashboardAccessTrendVO> listAccessTrend(Integer days);
/**
* 查询热门模块列表
*
*
* @return 热门模块列表
*/
List<DashboardPopularModuleVO> listPopularModule();
/**
* 查询访客地域分布信息
*
*
* @return 访客地域分布信息
*/
DashboardGeoDistributionVO getGeoDistribution();
......
......@@ -83,9 +83,16 @@ public interface LogService {
*/
DashboardTotalVO getDashboardTotal();
/**
* 查询仪表盘访问趋势信息
*
* @return 仪表盘访问趋势信息
*/
List<DashboardAccessTrendVO> listDashboardAccessTrend(Integer days);
/**
* 查询仪表盘热门模块列表
*
*
* @return 仪表盘热门模块列表
*/
List<DashboardPopularModuleVO> listDashboardPopularModule();
......
......@@ -29,6 +29,7 @@ import org.springframework.stereotype.Service;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.NumberUtil;
import top.charles7c.cnadmin.monitor.model.vo.DashboardAccessTrendVO;
import top.charles7c.cnadmin.monitor.model.vo.DashboardGeoDistributionVO;
import top.charles7c.cnadmin.monitor.model.vo.DashboardPopularModuleVO;
import top.charles7c.cnadmin.monitor.model.vo.DashboardTotalVO;
......@@ -63,6 +64,11 @@ public class DashboardServiceImpl implements DashboardService {
return totalVO;
}
@Override
public List<DashboardAccessTrendVO> listAccessTrend(Integer days) {
return logService.listDashboardAccessTrend(days);
}
@Override
public List<DashboardPopularModuleVO> listPopularModule() {
List<DashboardPopularModuleVO> popularModuleList = logService.listDashboardPopularModule();
......
......@@ -151,6 +151,11 @@ public class LogServiceImpl implements LogService {
return logMapper.selectDashboardTotal();
}
@Override
public List<DashboardAccessTrendVO> listDashboardAccessTrend(Integer days) {
return logMapper.selectListDashboardAccessTrend(days);
}
@Override
public List<DashboardPopularModuleVO> listDashboardPopularModule() {
return logMapper.selectListDashboardPopularModule();
......
......@@ -9,6 +9,18 @@
(SELECT COUNT(*) FROM `sys_log` WHERE DATE(`create_time`) = DATE_SUB(CURDATE(), INTERVAL 1 DAY)) AS yesterdayPvCount
</select>
<select id="selectListDashboardAccessTrend"
resultType="top.charles7c.cnadmin.monitor.model.vo.DashboardAccessTrendVO">
SELECT
DATE(`create_time`) AS date,
COUNT(*) AS pvCount,
COUNT(DISTINCT `client_ip`) AS ipCount
FROM `sys_log`
GROUP BY DATE(`create_time`)
ORDER BY DATE(`create_time`) DESC
LIMIT #{days}
</select>
<select id="selectListDashboardPopularModule"
resultType="top.charles7c.cnadmin.monitor.model.vo.DashboardPopularModuleVO">
SELECT
......
import axios from 'axios';
import type { TableData } from '@arco-design/web-vue/es/table/interface';
const BASE_URL = '/dashboard';
......@@ -10,6 +9,12 @@ export interface DashboardTotalRecord {
newPvFromYesterday: number;
}
export interface DashboardAccessTrendRecord {
date: string;
pvCount: number;
ipCount: number;
}
export interface DashboardPopularModuleRecord {
module: string;
pvCount: number;
......@@ -31,6 +36,12 @@ export function getTotal() {
return axios.get<DashboardTotalRecord>(`${BASE_URL}/total`);
}
export function listAccessTrend(days: number) {
return axios.get<DashboardAccessTrendRecord[]>(
`${BASE_URL}/access/trend/${days}`
);
}
export function listPopularModule() {
return axios.get<DashboardPopularModuleRecord[]>(
`${BASE_URL}/popular/module`
......@@ -46,12 +57,3 @@ export function getGeoDistribution() {
export function listAnnouncement() {
return axios.get<DashboardAnnouncementRecord[]>(`${BASE_URL}/announcement`);
}
export interface ContentDataRecord {
x: string;
y: number;
}
export function queryContentData() {
return axios.get<ContentDataRecord[]>('/api/content-data');
}
\ No newline at end of file
......@@ -6,10 +6,21 @@
:body-style="{
paddingTop: '20px',
}"
:title="$t('workplace.contentData')"
:title="$t('workplace.accessTrend')"
>
<template #extra>
<a-link>{{ $t('workplace.viewMore') }}</a-link>
<a-radio-group
v-model:model-value="dateRange"
type="button"
@change="handleDateRangeChange as any"
>
<a-radio :value="7">
{{ $t('workplace.accessTrend.dateRange7') }}
</a-radio>
<a-radio :value="30">
{{ $t('workplace.accessTrend.dateRange30') }}
</a-radio>
</a-radio-group>
</template>
<Chart height="289px" :option="chartOption" />
</a-card>
......@@ -18,40 +29,48 @@
<script lang="ts" setup>
import { ref } from 'vue';
import { graphic } from 'echarts';
import useLoading from '@/hooks/loading';
import { queryContentData, ContentDataRecord } from '@/api/common/dashboard';
import {
DashboardAccessTrendRecord,
listAccessTrend,
} from '@/api/common/dashboard';
import useChartOption from '@/hooks/chart-option';
import { ToolTipFormatterParams } from '@/types/echarts';
import { AnyObject } from '@/types/global';
function graphicFactory(side: AnyObject) {
return {
type: 'text',
bottom: '8',
...side,
style: {
text: '',
textAlign: 'center',
fill: '#4E5969',
fontSize: 12,
},
};
}
const tooltipItemsHtmlString = (items: ToolTipFormatterParams[]) => {
return items
.map(
(el) => `<div class="content-panel">
<p>
<span style="background-color: ${el.color}" class="tooltip-item-icon"></span>
<span>${el.seriesName}</span>
</p>
<span class="tooltip-value">
${el.value}
</span>
</div>`
)
.join('');
};
const { loading, setLoading } = useLoading(true);
const dateRange = ref(30);
const xAxis = ref<string[]>([]);
const chartsData = ref<number[]>([]);
const graphicElements = ref([
graphicFactory({ left: '2.6%' }),
graphicFactory({ right: 0 }),
]);
const { chartOption } = useChartOption(() => {
const pvStatisticsData = ref<number[]>([]);
const ipStatisticsData = ref<number[]>([]);
const { chartOption } = useChartOption((isDark) => {
return {
grid: {
left: '2.6%',
left: '30',
right: '0',
top: '10',
bottom: '30',
bottom: '50',
},
legend: {
bottom: -3,
icon: 'circle',
textStyle: {
color: '#4E5969',
},
},
xAxis: {
type: 'category',
......@@ -76,11 +95,10 @@
show: true,
interval: (idx: number) => {
if (idx === 0) return false;
if (idx === xAxis.value.length - 1) return false;
return true;
return idx !== xAxis.value.length - 1;
},
lineStyle: {
color: '#E5E8EF',
color: isDark ? '#3F3F3F' : '#E5E8EF',
},
},
axisPointer: {
......@@ -93,100 +111,89 @@
},
yAxis: {
type: 'value',
axisLine: {
show: false,
},
axisLabel: {
formatter(value: any, idx: number) {
if (idx === 0) return value;
return `${value}k`;
return `${value}`;
},
},
axisLine: {
show: false,
},
splitLine: {
show: true,
lineStyle: {
type: 'dashed',
color: '#E5E8EF',
color: isDark ? '#3F3F3F' : '#E5E8EF',
},
},
},
tooltip: {
show: true,
trigger: 'axis',
formatter(params) {
const [firstElement] = params as ToolTipFormatterParams[];
return `<div>
<p class="tooltip-title">${firstElement.axisValueLabel}</p>
<div class="content-panel"><span>总内容量</span><span class="tooltip-value">${(
Number(firstElement.value) * 10000
).toLocaleString()}</span></div>
${tooltipItemsHtmlString(params as ToolTipFormatterParams[])}
</div>`;
},
className: 'echarts-tooltip-diy',
},
graphic: {
elements: graphicElements.value,
},
series: [
{
data: chartsData.value,
name: '浏览量(PV)',
data: pvStatisticsData.value,
type: 'line',
smooth: true,
// symbol: 'circle',
symbolSize: 12,
showSymbol: false,
color: isDark ? '#3D72F6' : '#246EFF',
symbol: 'circle',
symbolSize: 10,
emphasis: {
focus: 'series',
itemStyle: {
borderWidth: 2,
borderColor: '#E0E3FF',
},
},
lineStyle: {
width: 3,
color: new graphic.LinearGradient(0, 0, 1, 0, [
{
offset: 0,
color: 'rgba(30, 231, 255, 1)',
},
{
offset: 0.5,
color: 'rgba(36, 154, 255, 1)',
},
{
offset: 1,
color: 'rgba(111, 66, 251, 1)',
},
]),
},
},
{
name: 'IP数',
data: ipStatisticsData.value,
type: 'line',
smooth: true,
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(17, 126, 255, 0.16)',
},
{
offset: 1,
color: 'rgba(17, 128, 255, 0)',
},
]),
color: isDark ? '#A079DC' : '#00B2FF',
symbol: 'circle',
symbolSize: 10,
emphasis: {
focus: 'series',
itemStyle: {
borderWidth: 2,
borderColor: '#E2F2FF',
},
},
},
],
};
});
const fetchData = async () => {
/**
* 查询趋势图信息
*
* @param days 日期数
*/
const getList = async (days: number) => {
setLoading(true);
try {
const { data: chartData } = await queryContentData();
chartData.forEach((el: ContentDataRecord, idx: number) => {
xAxis.value.push(el.x);
chartsData.value.push(el.y);
if (idx === 0) {
graphicElements.value[0].style.text = el.x;
}
if (idx === chartData.length - 1) {
graphicElements.value[1].style.text = el.x;
}
xAxis.value = [];
pvStatisticsData.value = [];
ipStatisticsData.value = [];
const { data: chartData } = await listAccessTrend(days);
chartData.forEach((el: DashboardAccessTrendRecord) => {
xAxis.value.unshift(el.date);
pvStatisticsData.value.unshift(el.pvCount);
ipStatisticsData.value.unshift(el.ipCount);
});
} catch (err) {
// you can report use errorHandler or other
......@@ -194,7 +201,16 @@
setLoading(false);
}
};
fetchData();
/**
* 切换日期范围
*
* @param days 日期数
*/
const handleDateRangeChange = (days: number) => {
getList(days);
};
getList(30);
</script>
<style scoped lang="less"></style>
......@@ -4,7 +4,7 @@
:title="$t('workplace.docs')"
:header-style="{ paddingBottom: 0 }"
:body-style="{ paddingTop: '10px', paddingBottom: '10px' }"
style="height: 198px"
style="height: 200px"
>
<template #extra>
<a-link href="https://doc.charles7c.top" target="_blank" rel="noopener">{{
......@@ -70,10 +70,11 @@
</a-card>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped>
.arco-card-body .arco-link {
margin: 10px 0;
color: rgb(var(--gray-8));
}
</style>
<script setup lang="ts"></script>
\ No newline at end of file
......@@ -4,7 +4,7 @@
class="general-card"
:header-style="{ paddingBottom: '0' }"
:body-style="{
padding: '0 20px',
padding: '0 20px 15px 20px',
}"
>
<template #title>
......@@ -58,6 +58,9 @@
itemStyle: {
borderWidth: 0,
},
textStyle: {
color: '#4E5969',
},
},
tooltip: {
show: true,
......@@ -66,15 +69,15 @@
series: [
{
type: 'pie',
radius: '70%',
radius: '65%',
label: {
formatter: '{d}%',
fontSize: 14,
color: isDark ? 'rgba(255, 255, 255, 0.7)' : '#4E5969',
},
itemStyle: {
borderColor: isDark ? '#232324' : '#fff',
borderWidth: 1,
borderColor: '#D9F6FF',
},
data: statisticsData.value.locationIpStatistics,
},
......@@ -85,6 +88,6 @@
<style scoped lang="less">
.general-card {
min-height: 566px;
min-height: 568px;
}
</style>
......@@ -44,7 +44,7 @@
<script lang="ts" setup>
import Banner from './components/banner.vue';
import DataPanel from './components/data-panel.vue';
import ContentChart from './components/content-chart.vue';
import ContentChart from './components/access-trend.vue';
import PopularModule from './components/popular-module.vue';
import CategoriesPercent from './components/geo-distribution.vue';
import RecentlyVisited from './components/recently-visited.vue';
......
......@@ -24,7 +24,9 @@ export default {
'workplace.allProject': 'All',
'workplace.loadMore': 'More',
'workplace.viewMore': 'More',
'workplace.contentData': 'Content Data',
'workplace.accessTrend': 'Access Trend',
'workplace.accessTrend.dateRange7': 'Last 7 Days',
'workplace.accessTrend.dateRange30': 'Last 30 Days',
'workplace.popularModule': 'Popular Module(Top10)',
'workplace.geoDistribution': 'Geo Distribution(Top10)',
'workplace.unit.pecs': 'pecs',
......
......@@ -24,7 +24,9 @@ export default {
'workplace.allProject': '所有项目',
'workplace.loadMore': '加载更多',
'workplace.viewMore': '查看更多',
'workplace.contentData': '内容数据',
'workplace.accessTrend': '访问趋势',
'workplace.accessTrend.dateRange7': '近7天',
'workplace.accessTrend.dateRange30': '近30天',
'workplace.popularModule': '热门模块(Top10)',
'workplace.geoDistribution': '访客地域分布(Top10)',
'workplace.unit.pecs': '',
......
......@@ -21,15 +21,20 @@ import java.util.List;
import lombok.RequiredArgsConstructor;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.charles7c.cnadmin.common.model.vo.R;
import top.charles7c.cnadmin.common.util.validate.ValidationUtils;
import top.charles7c.cnadmin.monitor.annotation.Log;
import top.charles7c.cnadmin.monitor.model.vo.DashboardAccessTrendVO;
import top.charles7c.cnadmin.monitor.model.vo.DashboardGeoDistributionVO;
import top.charles7c.cnadmin.monitor.model.vo.DashboardPopularModuleVO;
import top.charles7c.cnadmin.monitor.model.vo.DashboardTotalVO;
......@@ -58,6 +63,14 @@ public class DashboardController {
return R.ok(dashboardService.getTotal());
}
@Operation(summary = "查询访问趋势信息", description = "查询访问趋势信息")
@Parameter(name = "days", description = "日期数", example = "30", in = ParameterIn.PATH)
@GetMapping("/access/trend/{days}")
public R<List<DashboardAccessTrendVO>> listAccessTrend(@PathVariable Integer days) {
ValidationUtils.throwIf(7 != days && 30 != days, "仅支持查询近 7/30 天访问趋势信息");
return R.ok(dashboardService.listAccessTrend(days));
}
@Operation(summary = "查询热门模块列表", description = "查询热门模块列表")
@GetMapping("/popular/module")
public R<List<DashboardPopularModuleVO>> listPopularModule() {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册