mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-09-10 20:57:10 +08:00
feat: 仪表盘分析页新增地理位置访问分析图表
This commit is contained in:
@@ -20,6 +20,11 @@ export function getDashboardOverviewIp() {
|
||||
return http.get<T.DashboardOverviewCommonResp>(`${BASE_URL}/analysis/overview/ip`)
|
||||
}
|
||||
|
||||
/** @desc 查询地域分析 */
|
||||
export function getAnalysisGeo() {
|
||||
return http.get<T.DashboardChartCommonResp[]>(`${BASE_URL}/analysis/geo`)
|
||||
}
|
||||
|
||||
/** @desc 查询访问趋势 */
|
||||
export function getDashboardAccessTrend(days: number) {
|
||||
return http.get<T.DashboardAccessTrendResp[]>(`${BASE_URL}/access/trend/${days}`)
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<VCharts v-if="renderChart" :option="option" :autoresize="autoResize" :style="{ width, height }" ref="chart" />
|
||||
<VCharts ref="chart" :option="option" :autoresize="autoResize" :style="{ width, height }" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { registerMap } from 'echarts/core';
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { registerMap } from 'echarts/core'
|
||||
import { ref } from 'vue'
|
||||
import VCharts from 'vue-echarts'
|
||||
import worldMap from './world.json'
|
||||
import chinaMap from './china.json'
|
||||
|
||||
defineProps({
|
||||
option: {
|
||||
type: Object,
|
||||
@@ -28,16 +29,13 @@ defineProps({
|
||||
default: '100%',
|
||||
},
|
||||
})
|
||||
|
||||
registerMap('world', worldMap)
|
||||
registerMap('china', chinaMap)
|
||||
const renderChart = ref(false)
|
||||
|
||||
const chart = ref(null)
|
||||
defineExpose({
|
||||
chart
|
||||
})
|
||||
// wait container expand
|
||||
nextTick(() => {
|
||||
renderChart.value = true
|
||||
chart,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,218 +0,0 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-card class="general-card" title="访问趋势">
|
||||
<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="chartOption" style="height: 460px" />
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type EChartsOption, graphic } from 'echarts'
|
||||
import { type DashboardAccessTrendResp, getDashboardAccessTrend as getData } 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 xAxis = ref<string[]>([])
|
||||
const pvChartData = ref<number[]>([])
|
||||
const ipChartData = ref<number[]>([])
|
||||
const { chartOption } = useChart((isDark: EChartsOption) => {
|
||||
return {
|
||||
grid: {
|
||||
left: '38',
|
||||
right: '5',
|
||||
top: '10',
|
||||
bottom: '50',
|
||||
},
|
||||
legend: {
|
||||
bottom: -3,
|
||||
icon: 'circle',
|
||||
textStyle: {
|
||||
color: '#4E5969',
|
||||
},
|
||||
},
|
||||
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: {
|
||||
show: false,
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
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: '访问次数',
|
||||
data: pvChartData.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: ipChartData.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
|
||||
xAxis.value = []
|
||||
pvChartData.value = []
|
||||
ipChartData.value = []
|
||||
const { data: chartData } = await getData(days)
|
||||
chartData.forEach((item: DashboardAccessTrendResp) => {
|
||||
xAxis.value.push(item.date)
|
||||
pvChartData.value.push(item.pvCount)
|
||||
ipChartData.value.push(item.ipCount)
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换
|
||||
const onChange = (days: number) => {
|
||||
getChartData(days)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getChartData(30)
|
||||
})
|
||||
</script>
|
@@ -1,175 +0,0 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-card class="general-card" title="访问趋势">
|
||||
<template #extra>
|
||||
<a-radio-group v-model:model-value="dateRange" type="button" size="small" @change="onChange as any">
|
||||
<a-radio value="world">世界</a-radio>
|
||||
<a-radio value="china">中国</a-radio>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
<div class="content">
|
||||
<div class="mapChart">
|
||||
<Chart :option="chartOption" style="height: 600px" ref="chartRef" />
|
||||
</div>
|
||||
<div class="dataShow">
|
||||
<div class="dataItem" v-for="item in data" :key="item.name">
|
||||
<div class="title">
|
||||
<span>{{ item.name }}</span>
|
||||
<span>{{ item.value }}</span>
|
||||
</div>
|
||||
<a-progress color="#246EFF" size="medium" :percent="(item.value - 0) / 100" :show-text="false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type EChartsOption, graphic } from 'echarts'
|
||||
import { type DashboardAccessTrendResp, getDashboardAccessTrend as getData } from '@/apis'
|
||||
import { useChart } from '@/hooks'
|
||||
|
||||
const chartRef = ref(null)
|
||||
const xAxis = ref<string[]>([])
|
||||
const pvChartData = ref<number[]>([])
|
||||
const ipChartData = ref<number[]>([])
|
||||
const data = [
|
||||
{ name: "河北省", value: 9 },
|
||||
{ name: "山西省", value: 9 },
|
||||
{ name: "辽宁省", value: 9 },
|
||||
{ name: "吉林省", value: 9 },
|
||||
{ name: "黑龙江省", value: 9 },
|
||||
{ name: "江苏省", value: 9 },
|
||||
{ name: "浙江省", value: 2000 },
|
||||
{ name: "安徽省", value: 9 },
|
||||
{ name: "福建省", value: 9 },
|
||||
{ name: "江西省", value: 9 },
|
||||
{ name: "山东省", value: 9 },
|
||||
{ name: "河南省", value: 9 },
|
||||
{ name: "湖北省", value: 9 },
|
||||
{ name: "湖南省", value: 9 },
|
||||
{ name: "广东省", value: 9 },
|
||||
{ name: "海南省", value: 9 },
|
||||
{ name: "四川省", value: 9 },
|
||||
{ name: "贵州省", value: 9 },
|
||||
{ name: "云南省", value: 9 },
|
||||
{ name: "陕西省", value: 9 },
|
||||
{ name: "甘肃省", value: 9 },
|
||||
|
||||
];
|
||||
const { chartOption } = useChart((isDark: EChartsOption) => {
|
||||
return {
|
||||
visualMap: {
|
||||
left: 'left',
|
||||
min: 1,
|
||||
max: 20000,
|
||||
inRange: {
|
||||
color: [
|
||||
'#81E2FF',
|
||||
'#246EFE',
|
||||
]
|
||||
},
|
||||
calculable: false
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}<br/>访问量:{c}'
|
||||
},
|
||||
|
||||
series: [
|
||||
{
|
||||
type: 'map',
|
||||
map: 'china',
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#F7F8FA'
|
||||
},
|
||||
data: data
|
||||
}
|
||||
|
||||
]
|
||||
};
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const dateRange = ref('china')
|
||||
// 查询图表数据
|
||||
const getChartData = async (days: number) => {
|
||||
try {
|
||||
loading.value = true
|
||||
xAxis.value = []
|
||||
pvChartData.value = []
|
||||
ipChartData.value = []
|
||||
const { data: chartData } = await getData(days)
|
||||
chartData.forEach((item: DashboardAccessTrendResp) => {
|
||||
xAxis.value.push(item.date)
|
||||
pvChartData.value.push(item.pvCount)
|
||||
ipChartData.value.push(item.ipCount)
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换
|
||||
const onChange = (type: string) => {
|
||||
chartRef.value?.chart.setOption({
|
||||
series: [{
|
||||
map: type,
|
||||
label: {
|
||||
show: false
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#F7F8FA'
|
||||
},
|
||||
data: data
|
||||
}]
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getChartData(30)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
display: flex;
|
||||
|
||||
.mapChart {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
.dataShow {
|
||||
height: 100%;
|
||||
max-height: 500px;
|
||||
flex: 1;
|
||||
padding: 16px 24px;
|
||||
border-radius: 5px;
|
||||
overflow: auto;
|
||||
background-color: #333333;
|
||||
|
||||
.dataItem {
|
||||
padding-top: 10px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.dataItem:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
/* 隐藏滚动条 */
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
138
src/views/dashboard/analysis/components/Geo.vue
Normal file
138
src/views/dashboard/analysis/components/Geo.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-card class="general-card" title="地理位置">
|
||||
<template #extra>
|
||||
<a-radio-group type="button" size="small">
|
||||
<a-radio value="china">中国</a-radio>
|
||||
<a-radio value="world" disabled title="暂未开放">世界</a-radio>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
<div class="content">
|
||||
<div class="mapChart">
|
||||
<Chart ref="chartRef" :option="chartOption" style="height: 468px" />
|
||||
</div>
|
||||
<div class="dataShow">
|
||||
<div v-for="item in topData" :key="item.name" class="dataItem">
|
||||
<div class="title">
|
||||
<span>{{ item.name }}</span>
|
||||
<span>{{ item.value }}</span>
|
||||
</div>
|
||||
<a-progress color="#165dff" size="medium" :percent="item.value / totalValue" :show-text="false" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import { getAnalysisGeo as getData } from '@/apis/common/dashboard'
|
||||
import { useChart } from '@/hooks'
|
||||
|
||||
const chartRef = useTemplateRef('chartRef')
|
||||
const chartData = ref([])
|
||||
const totalValue = ref(0)
|
||||
const topData = ref([])
|
||||
|
||||
const { chartOption } = useChart((isDark: EChartsOption) => {
|
||||
return {
|
||||
visualMap: {
|
||||
left: 'left',
|
||||
min: 0,
|
||||
max: 20000,
|
||||
inRange: {
|
||||
color: ['#EAF4FF', '#3C7EFF'],
|
||||
},
|
||||
orient: 'horizontal',
|
||||
calculable: false,
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter(data: any) {
|
||||
return `${data.name}<br/>访问次数: ${data.value || 0}`
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'map',
|
||||
map: 'china',
|
||||
zoom: 1.1,
|
||||
label: {
|
||||
show: false,
|
||||
},
|
||||
itemStyle: {
|
||||
normal: {
|
||||
areaColor: isDark ? '#313132' : '#F6F6F6',
|
||||
},
|
||||
},
|
||||
data: chartData.value,
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
// 查询图表数据
|
||||
const getChartData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { data } = await getData()
|
||||
chartData.value = data.filter((item) => item.value !== 0)
|
||||
totalValue.value = data.reduce((sum, item) => sum + item.value, 0)
|
||||
topData.value = data.sort((a, b) => b.value - a.value).slice(0, 7)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getChartData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.mapChart {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
.dataShow {
|
||||
height: 100%;
|
||||
max-height: 500px;
|
||||
flex: 1;
|
||||
padding: 16px 24px;
|
||||
border-radius: 4px;
|
||||
overflow: auto;
|
||||
background-color: var(--color-bg-4);
|
||||
|
||||
.dataItem {
|
||||
padding-top: 16px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.dataItem:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
/* 隐藏滚动条 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
.dataShow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -7,7 +7,7 @@
|
||||
<div>
|
||||
<a-grid :cols="24" :col-gap="14" :row-gap="14">
|
||||
<a-grid-item :span="{ xs: 24, sm: 24, md: 24, lg: 24, xl: 18, xxl: 18 }">
|
||||
<AccessTrendNew />
|
||||
<Geo />
|
||||
</a-grid-item>
|
||||
<a-grid-item :span="{ xs: 24, sm: 24, md: 24, lg: 24, xl: 6, xxl: 6 }">
|
||||
<Os style="margin-bottom: 16px" />
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import DataOverview from './components/DataOverview/index.vue'
|
||||
import AccessTrendNew from './components/AccessTrendNew.vue'
|
||||
import Geo from './components/Geo.vue'
|
||||
import Os from './components/Os.vue'
|
||||
import Browser from './components/Browser.vue'
|
||||
import Module from './components/Module.vue'
|
||||
|
Reference in New Issue
Block a user