feat: 新增分析页

This commit is contained in:
2024-10-18 00:21:19 +08:00
parent ad31d9f2ff
commit 455f2695c8
11 changed files with 766 additions and 10 deletions

View File

@@ -14,3 +14,23 @@ export function listDashboardAccessTrend(days: number) {
export function listDashboardNotice() {
return http.get<T.DashboardNoticeResp[]>(`${BASE_URL}/notice`)
}
/** @desc 查询访问时段分析 */
export function getAnalysisTimeslot() {
return http.get<T.DashboardChartCommonResp[]>(`${BASE_URL}/analysis/timeslot`)
}
/** @desc 查询模块分析 */
export function getAnalysisModule() {
return http.get<T.DashboardChartCommonResp[]>(`${BASE_URL}/analysis/module`)
}
/** @desc 查询终端分析 */
export function getAnalysisOs() {
return http.get<T.DashboardChartCommonResp[]>(`${BASE_URL}/analysis/os`)
}
/** @desc 查询浏览器分析 */
export function getAnalysisBrowser() {
return http.get<T.DashboardChartCommonResp[]>(`${BASE_URL}/analysis/browser`)
}

View File

@@ -12,6 +12,12 @@ export interface DashboardAccessTrendResp {
ipCount: number
}
/** 仪表盘图表类型 */
export interface DashboardChartCommonResp {
name: string
value: number
}
/** 仪表盘公告类型 */
export interface DashboardNoticeResp {
id: number

View File

@@ -0,0 +1,41 @@
<template>
<VCharts
v-if="renderChart"
:option="option"
:autoresize="autoResize"
:style="{ width, height }"
/>
</template>
<script lang="ts" setup>
import { nextTick, ref } from 'vue'
import VCharts from 'vue-echarts'
defineProps({
option: {
type: Object,
default() {
return {}
}
},
autoResize: {
type: Boolean,
default: true
},
width: {
type: String,
default: '100%'
},
height: {
type: String,
default: '100%'
}
})
const renderChart = ref(false)
// wait container expand
nextTick(() => {
renderChart.value = true
})
</script>
<style scoped lang="less"></style>

View File

@@ -188,6 +188,7 @@
padding: $margin;
box-sizing: border-box;
overflow-y: auto;
overflow-x: hidden;
}
// 表格页面
@@ -311,18 +312,11 @@
// 通用卡片
.general-card {
height: 100%;
overflow-y: auto;
border: none;
& > .arco-card-header {
height: auto;
padding: $padding;
border: none;
.arco-card-header-title {
color: var(--color-text-1);
font-size: 18px;
font-weight: 500;
line-height: 1.5;
}
}
& > .arco-card-body {
padding: 0 $padding $padding $padding;

View File

@@ -8,6 +8,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
Breadcrumb: typeof import('./../components/Breadcrumb/index.vue')['default']
Chart: typeof import('./../components/Chart/index.vue')['default']
CronForm: typeof import('./../components/GenCron/CronForm/index.vue')['default']
CronModel: typeof import('./../components/GenCron/CronModel/index.vue')['default']
DateRangePicker: typeof import('./../components/DateRangePicker/index.vue')['default']

View File

@@ -0,0 +1,204 @@
<template>
<a-spin :loading="loading" style="width: 100%">
<a-card title="访问时段分析" class="general-card" :header-style="{ paddingBottom: '16px' }">
<Chart style="width: 100%; height: 370px" :option="option" />
</a-card>
</a-spin>
</template>
<script lang="ts" setup>
import { graphic } from 'echarts'
import { useChart } from '@/hooks'
import { type DashboardChartCommonResp, getAnalysisTimeslot as getData } from '@/apis/common'
// 提示框
const tooltipItemsHtmlString = (items) => {
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 xAxis = ref<string[]>([])
const dataList = ref<number[]>([])
const { option } = useChart((isDark) => {
return {
grid: {
left: '40',
right: 0,
top: '20',
bottom: '100'
},
xAxis: {
type: 'category',
offset: 2,
data: xAxis.value,
boundaryGap: false,
axisLabel: {
color: '#4E5969',
formatter(value: number, idx: number) {
if (idx === 0) return ''
if (idx === xAxis.value.length - 1) return ''
return `${value}`
}
},
axisLine: {
lineStyle: {
color: isDark ? '#3f3f3f' : '#A9AEB8'
}
},
axisTick: {
show: true,
alignWithLabel: true,
lineStyle: {
color: '#86909C'
},
interval(idx: number) {
if (idx === 0) return false
if (idx === xAxis.value.length - 1) return false
return true
}
},
splitLine: {
show: true,
interval: (idx: number) => {
if (idx === 0) return false
return idx !== xAxis.value.length - 1
},
lineStyle: {
color: isDark ? '#3F3F3F' : '#E5E8EF'
}
},
axisPointer: {
show: true,
lineStyle: {
color: '#23ADFF',
width: 2
}
}
},
yAxis: {
type: 'value',
axisLabel: {
formatter(value: any, idx: number) {
if (idx === 0) return value
if (value >= 1000) {
return `${value / 1000}k`
}
return `${value}`
}
},
axisLine: {
show: false
},
splitLine: {
lineStyle: {
type: 'dashed',
color: isDark ? '#3F3F3F' : '#E5E8EF'
}
}
},
tooltip: {
show: true,
trigger: 'axis',
formatter(params) {
const [firstElement] = params
return `<div>
<p class="tooltip-title">${firstElement.axisValueLabel}</p>
${tooltipItemsHtmlString(params)}
</div>`
},
className: 'echarts-tooltip-diy'
},
series: [
{
name: '浏览量(PV)',
data: dataList.value,
type: 'line',
smooth: true,
showSymbol: false,
color: '#246EFF',
emphasis: {
focus: 'series',
itemStyle: {
borderWidth: 2,
borderColor: '#E0E3FF'
}
},
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)'
}
])
}
}
],
dataZoom: [
{
bottom: 40,
type: 'slider',
left: 40,
right: 14,
height: 14,
borderColor: 'transparent',
handleIcon:
'image://http://p3-armor.byteimg.com/tos-cn-i-49unhts6dw/1ee5a8c6142b2bcf47d2a9f084096447.svg~tplv-49unhts6dw-image.image',
handleSize: '20',
handleStyle: {
shadowColor: 'rgba(0, 0, 0, 0.2)',
shadowBlur: 4
},
brushSelect: false,
backgroundColor: isDark ? '#313132' : '#F2F3F5'
},
{
type: 'inside',
start: 0,
end: 100,
zoomOnMouseWheel: false
}
]
}
})
const loading = ref(false)
// 查询图表数据
const getChartData = async () => {
try {
loading.value = true
const { data } = await getData()
data.forEach((item: DashboardChartCommonResp) => {
xAxis.value.push(item.name)
dataList.value.push(item.value)
})
} finally {
loading.value = false
}
}
onMounted(() => {
getChartData()
})
</script>
<style lang="scss" scoped>
:deep(.arco-card-body) {
padding-bottom: 0;
}
</style>

View File

@@ -0,0 +1,218 @@
<template>
<a-spin :loading="loading" style="width: 100%">
<a-card title="访问趋势" class="general-card">
<template #extra>
<a-radio-group v-model:model-value="dateRange" type="button" size="small" @change="onChange as any">
<a-radio :value="7">近7天</a-radio>
<a-radio :value="30">近30天</a-radio>
</a-radio-group>
</template>
<Chart :option="option" :style="{ height: '326px' }" />
</a-card>
</a-spin>
</template>
<script lang="ts" setup>
import { graphic } from 'echarts'
import { type DashboardAccessTrendResp, listDashboardAccessTrend } from '@/apis'
import { useChart } from '@/hooks'
// 提示框
const tooltipItemsHtmlString = (items) => {
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 xData = ref<string[]>([])
const pvStatisticsData = ref<number[]>([])
const ipStatisticsData = ref<number[]>([])
const { option } = useChart((isDark) => {
return {
grid: {
left: '38',
right: '0',
top: '10',
bottom: '50'
},
legend: {
bottom: -3,
icon: 'circle',
textStyle: {
color: '#4E5969'
}
},
xAxis: {
type: 'category',
offset: 2,
data: xData.value,
boundaryGap: false,
axisLabel: {
color: '#4E5969',
formatter(value: number, idx: number) {
if (idx === 0) return ''
if (idx === xData.value.length - 1) return ''
return `${value}`
}
},
axisLine: {
show: false
},
axisTick: {
show: false
},
splitLine: {
show: true,
interval: (idx: number) => {
if (idx === 0) return false
return idx !== xData.value.length - 1
},
lineStyle: {
color: isDark ? '#3F3F3F' : '#E5E8EF'
}
},
axisPointer: {
show: true,
lineStyle: {
color: '#23ADFF',
width: 2
}
}
},
yAxis: {
type: 'value',
axisLabel: {
formatter(value: any, idx: number) {
if (idx === 0) return value
if (value >= 1000) {
return `${value / 1000}k`
}
return `${value}`
}
},
axisLine: {
show: false
},
splitLine: {
lineStyle: {
type: 'dashed',
color: isDark ? '#3F3F3F' : '#E5E8EF'
}
}
},
tooltip: {
show: true,
trigger: 'axis',
formatter(params) {
const [firstElement] = params
return `<div>
<p class="tooltip-title">${firstElement.axisValueLabel}</p>
${tooltipItemsHtmlString(params)}
</div>`
},
className: 'echarts-tooltip-diy'
},
series: [
{
name: '浏览量(PV)',
data: pvStatisticsData.value,
type: 'line',
smooth: true,
showSymbol: false,
color: '#246EFF',
symbol: 'circle',
symbolSize: 10,
emphasis: {
focus: 'series',
itemStyle: {
borderWidth: 2,
borderColor: '#E0E3FF'
}
},
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)'
}
])
}
},
{
name: 'IP数',
data: ipStatisticsData.value,
type: 'line',
smooth: true,
showSymbol: false,
color: '#00B2FF',
symbol: 'circle',
symbolSize: 10,
emphasis: {
focus: 'series',
itemStyle: {
borderWidth: 2,
borderColor: '#E2F2FF'
}
},
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)'
}
])
}
}
]
}
})
const loading = ref(false)
const dateRange = ref(30)
// 查询图表数据
const getChartData = async (days: number) => {
try {
loading.value = true
xData.value = []
pvStatisticsData.value = []
ipStatisticsData.value = []
const { data: chartData } = await listDashboardAccessTrend(days)
chartData.forEach((el: DashboardAccessTrendResp) => {
xData.value.unshift(el.date)
pvStatisticsData.value.unshift(el.pvCount)
ipStatisticsData.value.unshift(el.ipCount)
})
} finally {
loading.value = false
}
}
// 切换
const onChange = (days: number) => {
getChartData(days)
}
onMounted(() => {
getChartData(30)
})
</script>

View File

@@ -0,0 +1,82 @@
<template>
<a-spin :loading="loading" style="width: 100%">
<a-card class="general-card" title="浏览器分析" :header-style="{ paddingBottom: '12px' }">
<div class="chart">
<Chart v-if="!loading" style="height: 210px" :option="option" />
</div>
</a-card>
</a-spin>
</template>
<script lang="ts" setup>
import { useChart } from '@/hooks'
import { type DashboardChartCommonResp, getAnalysisBrowser as getData } from '@/apis/common'
const xAxis = ref<string[]>([])
const dataList = ref([])
const { option } = useChart((isDark) => {
return {
legend: {
bottom: 'center',
data: xAxis.value,
bottom: 0,
icon: 'circle',
itemWidth: 8,
textStyle: {
color: isDark ? 'rgba(255,255,255,0.7)' : '#4E5969'
},
itemStyle: {
borderWidth: 0
}
},
tooltip: {
show: true,
trigger: 'item'
},
series: [
{
type: 'pie',
radius: ['50%', '70%'],
center: ['50%', '45%'],
label: {
formatter: '{d}% ',
color: isDark ? 'rgba(255, 255, 255, 0.7)' : '#4E5969'
},
itemStyle: {
borderColor: isDark ? '#000' : '#fff',
borderWidth: 1
},
data: dataList.value
}
]
}
})
const loading = ref(false)
const colors = ['#249EFF', '#846BCE', '#21CCFF', '#0E42D2', '#86DF6C']
// 查询图表数据
const getChartData = async () => {
try {
loading.value = true
const { data } = await getData()
data.forEach((item: DashboardChartCommonResp, index) => {
xAxis.value.push(item.name)
dataList.value.push({
...item,
itemStyle: {
color: data.length > 1 && index === data.length - 1 ? colors[colors.length - 1] : colors[index]
}
})
})
} finally {
loading.value = false
}
}
onMounted(() => {
getChartData()
})
</script>
<style scoped lang="less">
</style>

View File

@@ -0,0 +1,82 @@
<template>
<a-spin :loading="loading" style="width: 100%">
<a-card class="general-card" title="模块分析" :header-style="{ paddingBottom: '12px' }">
<div class="chart">
<Chart v-if="!loading" style="height: 210px" :option="option" />
</div>
</a-card>
</a-spin>
</template>
<script lang="ts" setup>
import { useChart } from '@/hooks'
import { type DashboardChartCommonResp, getAnalysisModule as getData } from '@/apis/common'
const xAxis = ref<string[]>([])
const dataList = ref([])
const { option } = useChart((isDark) => {
return {
legend: {
bottom: 'center',
data: xAxis.value,
bottom: 0,
icon: 'circle',
itemWidth: 8,
textStyle: {
color: isDark ? 'rgba(255,255,255,0.7)' : '#4E5969'
},
itemStyle: {
borderWidth: 0
}
},
tooltip: {
show: true,
trigger: 'item'
},
series: [
{
type: 'pie',
radius: ['50%', '70%'],
center: ['50%', '45%'],
label: {
formatter: '{d}% ',
color: isDark ? 'rgba(255, 255, 255, 0.7)' : '#4E5969'
},
itemStyle: {
borderColor: isDark ? '#000' : '#fff',
borderWidth: 1
},
data: dataList.value
}
]
}
})
const loading = ref(false)
const colors = ['#249EFF', '#846BCE', '#21CCFF', '#0E42D2', '#86DF6C']
// 查询图表数据
const getChartData = async () => {
try {
loading.value = true
const { data } = await getData()
data.forEach((item: DashboardChartCommonResp, index) => {
xAxis.value.push(item.name)
dataList.value.push({
...item,
itemStyle: {
color: data.length > 1 && index === data.length - 1 ? colors[colors.length - 1] : colors[index]
}
})
})
} finally {
loading.value = false
}
}
onMounted(() => {
getChartData()
})
</script>
<style scoped lang="less">
</style>

View File

@@ -0,0 +1,82 @@
<template>
<a-spin :loading="loading" style="width: 100%">
<a-card class="general-card" title="终端分析" :header-style="{ paddingBottom: '12px' }">
<div class="chart">
<Chart v-if="!loading" style="height: 210px" :option="option" />
</div>
</a-card>
</a-spin>
</template>
<script lang="ts" setup>
import { useChart } from '@/hooks'
import { type DashboardChartCommonResp, getAnalysisOs as getData } from '@/apis/common'
const xAxis = ref<string[]>([])
const dataList = ref([])
const { option } = useChart((isDark) => {
return {
legend: {
bottom: 'center',
data: xAxis.value,
bottom: 0,
icon: 'circle',
itemWidth: 8,
textStyle: {
color: isDark ? 'rgba(255,255,255,0.7)' : '#4E5969'
},
itemStyle: {
borderWidth: 0
}
},
tooltip: {
show: true,
trigger: 'item'
},
series: [
{
type: 'pie',
radius: ['50%', '70%'],
center: ['50%', '45%'],
label: {
formatter: '{d}% ',
color: isDark ? 'rgba(255, 255, 255, 0.7)' : '#4E5969'
},
itemStyle: {
borderColor: isDark ? '#000' : '#fff',
borderWidth: 1
},
data: dataList.value
}
]
}
})
const loading = ref(false)
const colors = ['#249EFF', '#846BCE', '#21CCFF', '#0E42D2', '#86DF6C']
// 查询图表数据
const getChartData = async () => {
try {
loading.value = true
const { data } = await getData()
data.forEach((item: DashboardChartCommonResp, index) => {
xAxis.value.push(item.name)
dataList.value.push({
...item,
itemStyle: {
color: data.length > 1 && index === data.length - 1 ? colors[colors.length - 1] : colors[index]
}
})
})
} finally {
loading.value = false
}
}
onMounted(() => {
getChartData()
})
</script>
<style scoped lang="less">
</style>

View File

@@ -1,10 +1,36 @@
<template>
<div id="home" class="gi_page home">
分析页面开发中...
<div class="gi_page container">
<a-space direction="vertical" :size="16" fill>
<div>
<AccessTrend />
</div>
<div>
<a-grid :cols="24" :col-gap="16" :row-gap="16">
<a-grid-item :span="{ xs: 24, sm: 24, md: 24, lg: 24, xl: 8, xxl: 8 }">
<ModuleItem />
</a-grid-item>
<a-grid-item :span="{ xs: 24, sm: 24, md: 24, lg: 24, xl: 8, xxl: 8 }">
<OsItem />
</a-grid-item>
<a-grid-item :span="{ xs: 24, sm: 24, md: 24, lg: 24, xl: 8, xxl: 8 }">
<BrowserItem />
</a-grid-item>
</a-grid>
</div>
<div>
<AccessTimeslot />
</div>
</a-space>
</div>
</template>
<script setup lang="ts">
import AccessTrend from './components/AccessTrend.vue'
import ModuleItem from './components/ModuleItem.vue'
import OsItem from './components/OsItem.vue'
import BrowserItem from './components/BrowserItem.vue'
import AccessTimeslot from './components/AccessTimeslot.vue'
defineOptions({ name: 'Analysis' })
</script>