mirror of
https://github.com/continew-org/continew-admin-ui.git
synced 2025-09-08 22:57:11 +08:00
feat: 适配访问趋势图表卡片
This commit is contained in:
9
src/apis/common/home.ts
Normal file
9
src/apis/common/home.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import http from '@/utils/http'
|
||||
import type * as Common from './type'
|
||||
|
||||
const BASE_URL = '/dashboard'
|
||||
|
||||
/** @desc 查询访问趋势 */
|
||||
export function listAccessTrend(days: number) {
|
||||
return http.get<Common.DashboardAccessTrendResp[]>(`${BASE_URL}/access/trend/${days}`)
|
||||
}
|
@@ -1,2 +1,3 @@
|
||||
export * from './common'
|
||||
export * from './captcha'
|
||||
export * from './home'
|
||||
|
@@ -3,3 +3,10 @@ export interface ImageCaptchaResp {
|
||||
uuid: string
|
||||
img: string
|
||||
}
|
||||
|
||||
/** 仪表盘访问趋势 */
|
||||
export interface DashboardAccessTrendResp {
|
||||
date: string
|
||||
pvCount: number
|
||||
ipCount: number
|
||||
}
|
||||
|
@@ -198,6 +198,55 @@
|
||||
}
|
||||
}
|
||||
|
||||
// echarts 提示
|
||||
.echarts-tooltip-diy {
|
||||
background: linear-gradient(
|
||||
304.17deg,
|
||||
rgba(253, 254, 255, 0.6) -6.04%,
|
||||
rgba(244, 247, 252, 0.6) 85.2%
|
||||
) !important;
|
||||
border: none !important;
|
||||
backdrop-filter: blur(10px) !important;
|
||||
/* Note: backdrop-filter has minimal browser support */
|
||||
|
||||
border-radius: 6px !important;
|
||||
.content-panel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 9px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
width: 164px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
box-shadow: 6px 0 20px rgba(34, 87, 188, 0.1);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.tooltip-title {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
.tooltip-title,
|
||||
.tooltip-value {
|
||||
font-size: 13px;
|
||||
line-height: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: right;
|
||||
color: #1d2129;
|
||||
font-weight: bold;
|
||||
}
|
||||
.tooltip-item-icon {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
// 通用卡片
|
||||
.general-card {
|
||||
height: 100%;
|
||||
|
@@ -1,206 +0,0 @@
|
||||
<template>
|
||||
<a-card title="访问趋势" :bordered="false" class="gi_card_title">
|
||||
<template #extra>
|
||||
<a-radio-group type="button" size="small" default-value="1">
|
||||
<a-radio value="1">近7天</a-radio>
|
||||
<a-radio value="2">近30天</a-radio>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
<VCharts :option="option" autoresize :style="{ height: '289px' }"></VCharts>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import VCharts from 'vue-echarts'
|
||||
import { graphic } from 'echarts'
|
||||
import { useChart } from '@/hooks'
|
||||
// import { ToolTipFormatterParams } from '@/types/echarts';
|
||||
|
||||
const xData = ref<string[]>([])
|
||||
const yData = ref<number[]>([])
|
||||
|
||||
function graphicFactory(side: AnyObject) {
|
||||
return {
|
||||
type: 'text',
|
||||
bottom: '8',
|
||||
...side,
|
||||
style: {
|
||||
text: '',
|
||||
textAlign: 'center',
|
||||
fill: '#4E5969',
|
||||
fontSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
const graphicElements = ref([graphicFactory({ left: '2.6%' }), graphicFactory({ right: 0 })])
|
||||
const { option } = useChart(() => {
|
||||
return {
|
||||
grid: {
|
||||
left: '40',
|
||||
right: '0',
|
||||
top: '10',
|
||||
bottom: '30'
|
||||
},
|
||||
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
|
||||
if (idx === xData.value.length - 1) return false
|
||||
return true
|
||||
},
|
||||
lineStyle: {
|
||||
color: '#E5E8EF'
|
||||
}
|
||||
},
|
||||
axisPointer: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: '#23ADFF',
|
||||
width: 2
|
||||
}
|
||||
}
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: {
|
||||
show: false
|
||||
},
|
||||
axisLabel: {
|
||||
formatter(value: any, idx: number) {
|
||||
if (idx === 0) return value
|
||||
return `${value}k`
|
||||
}
|
||||
},
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
type: 'dashed',
|
||||
color: '#E5E8EF'
|
||||
}
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter(params) {
|
||||
const [firstElement] = params as any[]
|
||||
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>
|
||||
</div>`
|
||||
},
|
||||
className: 'echarts-tooltip-diy'
|
||||
},
|
||||
graphic: {
|
||||
elements: graphicElements.value
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '浏览量(PV)',
|
||||
data: yData.value,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
// symbol: 'circle',
|
||||
symbolSize: 12,
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
itemStyle: {
|
||||
borderWidth: 2
|
||||
}
|
||||
},
|
||||
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)'
|
||||
}
|
||||
])
|
||||
},
|
||||
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)'
|
||||
}
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'IP数',
|
||||
data: yData.value,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
color: '#00B2FF',
|
||||
symbolSize: 12,
|
||||
emphasis: {
|
||||
focus: 'series',
|
||||
itemStyle: {
|
||||
borderWidth: 2,
|
||||
borderColor: '#E2F2FF'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const getChartData = () => {
|
||||
try {
|
||||
const values = [100, 200, 150, 30, 100, 50, 10, 80]
|
||||
const year = new Date().getFullYear()
|
||||
const data = values.map((i, n) => {
|
||||
const m = n + 1
|
||||
const month = m >= 10 ? m : `0${m}`
|
||||
return { y: i, x: `${year}-${month}` }
|
||||
})
|
||||
data.forEach((item: any, index: number) => {
|
||||
xData.value.push(item.x)
|
||||
yData.value.push(item.y)
|
||||
if (index === 0) {
|
||||
graphicElements.value[0].style.text = item.x
|
||||
}
|
||||
if (index === data.length - 1) {
|
||||
graphicElements.value[1].style.text = item.x
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
// you can report use errorHandler or other
|
||||
}
|
||||
}
|
||||
getChartData()
|
||||
</script>
|
219
src/views/home/components/AccessTrendCard.vue
Normal file
219
src/views/home/components/AccessTrendCard.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<a-spin :loading="loading" style="width: 100%">
|
||||
<a-card title="访问趋势" :bordered="false" class="gi_card_title">
|
||||
<template #extra>
|
||||
<a-radio-group v-model:model-value="dateRange" type="button" size="small" @change="onChange">
|
||||
<a-radio :value="7">近7天</a-radio>
|
||||
<a-radio :value="30">近30天</a-radio>
|
||||
</a-radio-group>
|
||||
</template>
|
||||
<VCharts :option="option" autoresize :style="{ height: '326px' }"></VCharts>
|
||||
</a-card>
|
||||
</a-spin>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { listAccessTrend, type DashboardAccessTrendResp } from '@/apis'
|
||||
import VCharts from 'vue-echarts'
|
||||
import { graphic } from 'echarts'
|
||||
import { useChart } from '@/hooks'
|
||||
|
||||
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 listAccessTrend(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)
|
||||
}
|
||||
|
||||
// 提示框
|
||||
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('')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getChartData(30)
|
||||
})
|
||||
</script>
|
@@ -5,7 +5,7 @@
|
||||
<a-row class="home__content">
|
||||
<a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="18" :xxl="20">
|
||||
<div class="home__item"><ProjectCard /></div>
|
||||
<div class="home__item"><AccessTrend /></div>
|
||||
<div class="home__item"><AccessTrendCard /></div>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="6" :xxl="4">
|
||||
<div class="home__item"><FastCard /></div>
|
||||
@@ -23,7 +23,7 @@
|
||||
<script setup lang="ts">
|
||||
import WorkCard from './components/WorkCard.vue'
|
||||
import ProjectCard from './components/ProjectCard.vue'
|
||||
import AccessTrend from './components/AccessTrend.vue'
|
||||
import AccessTrendCard from './components/AccessTrendCard.vue'
|
||||
import FastCard from './components/FastCard.vue'
|
||||
import MessageCard from './components/MessageCard.vue'
|
||||
import Sponsor from './components/Sponsor.vue'
|
||||
|
Reference in New Issue
Block a user