✨ feat(im): 实现 im 的首页统计
parent
f5656c8a2f
commit
82022b86de
|
|
@ -1,9 +1,4 @@
|
||||||
// IM 数据看板 API
|
import request from '@/config/axios'
|
||||||
//
|
|
||||||
// vibe 阶段:所有方法用本地 mock(setTimeout + 随机数据)实现,不发真实请求。
|
|
||||||
// 后端就绪后把 mockXxx 调用替换为 request.get(...) 一行即可。
|
|
||||||
//
|
|
||||||
// import request from '@/config/axios'
|
|
||||||
|
|
||||||
export interface ImStatisticsOverviewVO {
|
export interface ImStatisticsOverviewVO {
|
||||||
totalUser: number
|
totalUser: number
|
||||||
|
|
@ -24,99 +19,48 @@ export interface ImStatisticsTrendVO {
|
||||||
series: Record<string, number[]>
|
series: Record<string, number[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImStatisticsDistributionVO {
|
export interface ImStatisticsMessageTypeVO {
|
||||||
messageTypeDistribution: { name: string; value: number }[]
|
type: number // 参见 ImMessageTypeEnum 枚举类,由前端按 DICT_TYPE.IM_MESSAGE_TYPE 翻译
|
||||||
groupSizeDistribution: { range: string; count: number }[]
|
value: number
|
||||||
topSenders: { userId: number; nickname: string; messageCount: number }[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== mock helpers ====================
|
export interface ImStatisticsGroupSizeVO {
|
||||||
|
range: string
|
||||||
const fakePromise = <T>(data: T, delay = 300): Promise<T> =>
|
count: number
|
||||||
new Promise((r) => setTimeout(() => r(data), delay))
|
|
||||||
|
|
||||||
const randomInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min
|
|
||||||
|
|
||||||
const buildDates = (days: number): string[] => {
|
|
||||||
const dates: string[] = []
|
|
||||||
const today = new Date()
|
|
||||||
for (let i = days - 1; i >= 0; i--) {
|
|
||||||
const d = new Date(today)
|
|
||||||
d.setDate(d.getDate() - i)
|
|
||||||
dates.push(d.toISOString().slice(0, 10))
|
|
||||||
}
|
|
||||||
return dates
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== exposed APIs ====================
|
export interface ImStatisticsTopSenderVO {
|
||||||
|
userId: number
|
||||||
|
nickname: string
|
||||||
|
messageCount: number
|
||||||
|
}
|
||||||
|
|
||||||
// 获得 KPI 概览
|
// 获得 KPI 概览
|
||||||
export const getStatisticsOverview = (): Promise<ImStatisticsOverviewVO> => {
|
export const getStatisticsOverview = (): Promise<ImStatisticsOverviewVO> => {
|
||||||
return fakePromise<ImStatisticsOverviewVO>({
|
return request.get({ url: '/im/manager/statistics/overview' })
|
||||||
totalUser: 12345,
|
|
||||||
newUserToday: 23,
|
|
||||||
totalGroup: 678,
|
|
||||||
newGroupToday: 4,
|
|
||||||
activeUserDaily: 1023,
|
|
||||||
activeUserWeekly: 4567,
|
|
||||||
activeUserMonthly: 8901,
|
|
||||||
privateMessageToday: 8765,
|
|
||||||
groupMessageToday: 3210,
|
|
||||||
privateMessageYesterday: 7890,
|
|
||||||
groupMessageYesterday: 3000
|
|
||||||
})
|
|
||||||
// 真实请求版本:return request.get({ url: '/im/manager/statistics/overview' })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获得消息趋势(私聊 + 群聊双线)
|
// 获得消息趋势(私聊 + 群聊双线)
|
||||||
export const getMessageTrend = (days: number): Promise<ImStatisticsTrendVO> => {
|
export const getMessageTrend = (days: number): Promise<ImStatisticsTrendVO> => {
|
||||||
const dates = buildDates(days)
|
return request.get({ url: '/im/manager/statistics/message-trend', params: { days } })
|
||||||
return fakePromise<ImStatisticsTrendVO>({
|
|
||||||
dates,
|
|
||||||
series: {
|
|
||||||
private: dates.map(() => randomInt(500, 2000)),
|
|
||||||
group: dates.map(() => randomInt(200, 1200))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// return request.get({ url: '/im/manager/statistics/message-trend', params: { days } })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获得用户趋势(新增注册 + 日活双线)
|
// 获得用户趋势(新增注册 + 日活双线)
|
||||||
export const getUserTrend = (days: number): Promise<ImStatisticsTrendVO> => {
|
export const getUserTrend = (days: number): Promise<ImStatisticsTrendVO> => {
|
||||||
const dates = buildDates(days)
|
return request.get({ url: '/im/manager/statistics/user-trend', params: { days } })
|
||||||
return fakePromise<ImStatisticsTrendVO>({
|
|
||||||
dates,
|
|
||||||
series: {
|
|
||||||
register: dates.map(() => randomInt(5, 80)),
|
|
||||||
active: dates.map(() => randomInt(800, 1500))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// return request.get({ url: '/im/manager/statistics/user-trend', params: { days } })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获得分布数据(消息类型 / 群规模 / TOP 发送者)
|
// 获得消息类型分布(最近 30 天)
|
||||||
export const getStatisticsDistribution = (): Promise<ImStatisticsDistributionVO> => {
|
export const getMessageTypeDistribution = (): Promise<ImStatisticsMessageTypeVO[]> => {
|
||||||
return fakePromise<ImStatisticsDistributionVO>({
|
return request.get({ url: '/im/manager/statistics/message-type-distribution' })
|
||||||
messageTypeDistribution: [
|
}
|
||||||
{ name: '文本', value: 8000 },
|
|
||||||
{ name: '图片', value: 2400 },
|
// 获得群规模分布
|
||||||
{ name: '视频', value: 320 },
|
export const getGroupSizeDistribution = (): Promise<ImStatisticsGroupSizeVO[]> => {
|
||||||
{ name: '语音', value: 980 },
|
return request.get({ url: '/im/manager/statistics/group-size-distribution' })
|
||||||
{ name: '文件', value: 540 },
|
}
|
||||||
{ name: '位置', value: 65 },
|
|
||||||
{ name: '名片', value: 32 }
|
// 获得消息 TOP 发送者(最近 30 天)
|
||||||
],
|
export const getTopSenders = (): Promise<ImStatisticsTopSenderVO[]> => {
|
||||||
groupSizeDistribution: [
|
return request.get({ url: '/im/manager/statistics/top-senders' })
|
||||||
{ range: '1-9 人', count: 320 },
|
|
||||||
{ range: '10-49 人', count: 240 },
|
|
||||||
{ range: '50-199 人', count: 95 },
|
|
||||||
{ range: '200+ 人', count: 23 }
|
|
||||||
],
|
|
||||||
topSenders: Array.from({ length: 10 }, (_, i) => ({
|
|
||||||
userId: 1000 + i,
|
|
||||||
nickname: `测试用户${i + 1}`,
|
|
||||||
messageCount: 1500 - i * 120
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
// return request.get({ url: '/im/manager/statistics/distribution' })
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,62 @@
|
||||||
<template>
|
<template>
|
||||||
<el-card shadow="never" class="chart-card">
|
<el-card shadow="never" class="chart-card">
|
||||||
<template #header>群规模分布</template>
|
<template #header>群规模分布</template>
|
||||||
<div ref="chartRef" style="width: 100%; height: 320px"></div>
|
<div ref="chartRef" v-loading="loading" style="width: 100%; height: 320px"></div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
|
import * as StatisticsApi from '@/api/im/manager/statistics'
|
||||||
|
|
||||||
defineOptions({ name: 'ImStatisticsGroupSizeChart' })
|
defineOptions({ name: 'ImStatisticsGroupSizeChart' })
|
||||||
|
|
||||||
const props = defineProps<{ data: { range: string; count: number }[] }>()
|
|
||||||
|
|
||||||
const chartRef = ref<HTMLElement>()
|
const chartRef = ref<HTMLElement>()
|
||||||
|
const loading = ref(false)
|
||||||
let chart: echarts.ECharts | null = null
|
let chart: echarts.ECharts | null = null
|
||||||
|
|
||||||
const render = () => {
|
/** 渲染柱状图 */
|
||||||
if (!chart) return
|
const render = (data: StatisticsApi.ImStatisticsGroupSizeVO[]) => {
|
||||||
chart.setOption({
|
chart?.setOption({
|
||||||
tooltip: { trigger: 'axis' },
|
tooltip: { trigger: 'axis' },
|
||||||
grid: { left: '3%', right: '4%', bottom: '3%', top: 30, containLabel: true },
|
grid: { left: '3%', right: '4%', bottom: '3%', top: 30, containLabel: true },
|
||||||
xAxis: { type: 'category', data: props.data.map((d) => d.range) },
|
xAxis: { type: 'category', data: data.map((d) => d.range) },
|
||||||
yAxis: { type: 'value', name: '群组数' },
|
yAxis: { type: 'value', name: '群组数' },
|
||||||
series: [{
|
series: [
|
||||||
type: 'bar',
|
{
|
||||||
data: props.data.map((d) => d.count),
|
type: 'bar',
|
||||||
itemStyle: { color: '#67C23A', borderRadius: [4, 4, 0, 0] },
|
data: data.map((d) => d.count),
|
||||||
barMaxWidth: 48
|
itemStyle: { color: '#67C23A', borderRadius: [4, 4, 0, 0] },
|
||||||
}]
|
barMaxWidth: 48
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 拉取并渲染数据 */
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await StatisticsApi.getGroupSizeDistribution()
|
||||||
|
render(data)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (chartRef.value) {
|
if (chartRef.value) {
|
||||||
chart = echarts.init(chartRef.value)
|
chart = echarts.init(chartRef.value)
|
||||||
render()
|
await loadData()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
watch(() => props.data, render, { deep: true })
|
|
||||||
onUnmounted(() => chart?.dispose())
|
onUnmounted(() => chart?.dispose())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart-card { border-radius: 8px; margin-bottom: 16px; }
|
.chart-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ const buildOption = (dates: string[], priv: number[], grp: number[]): echarts.EC
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: dates,
|
data: dates,
|
||||||
axisLabel: { formatter: (v: string) => v.substring(5) }
|
axisLabel: { formatter: (v: string) => v.slice(5, 10) }
|
||||||
},
|
},
|
||||||
yAxis: { type: 'value', name: '消息量' },
|
yAxis: { type: 'value', name: '消息量' },
|
||||||
series: [
|
series: [
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,67 @@
|
||||||
<template>
|
<template>
|
||||||
<el-card shadow="never" class="chart-card">
|
<el-card shadow="never" class="chart-card">
|
||||||
<template #header>消息类型分布</template>
|
<template #header>消息类型分布</template>
|
||||||
<div ref="chartRef" style="width: 100%; height: 320px"></div>
|
<div ref="chartRef" v-loading="loading" style="width: 100%; height: 320px"></div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
|
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
||||||
|
import * as StatisticsApi from '@/api/im/manager/statistics'
|
||||||
|
|
||||||
defineOptions({ name: 'ImStatisticsMessageTypeChart' })
|
defineOptions({ name: 'ImStatisticsMessageTypeChart' })
|
||||||
|
|
||||||
const props = defineProps<{ data: { name: string; value: number }[] }>()
|
|
||||||
|
|
||||||
const chartRef = ref<HTMLElement>()
|
const chartRef = ref<HTMLElement>()
|
||||||
|
const loading = ref(false)
|
||||||
let chart: echarts.ECharts | null = null
|
let chart: echarts.ECharts | null = null
|
||||||
|
|
||||||
const render = () => {
|
/** 渲染饼图:type 在前端按字典翻译为名称给 echarts */
|
||||||
if (!chart) return
|
const render = (data: StatisticsApi.ImStatisticsMessageTypeVO[]) => {
|
||||||
chart.setOption({
|
const items = data.map((d) => ({
|
||||||
|
name: getDictLabel(DICT_TYPE.IM_MESSAGE_TYPE, d.type) || `未知(${d.type})`,
|
||||||
|
value: d.value
|
||||||
|
}))
|
||||||
|
chart?.setOption({
|
||||||
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
|
||||||
legend: { orient: 'vertical', right: 8, top: 'middle' },
|
legend: { orient: 'vertical', right: 8, top: 'middle' },
|
||||||
series: [{
|
series: [
|
||||||
type: 'pie',
|
{
|
||||||
radius: ['40%', '70%'],
|
type: 'pie',
|
||||||
avoidLabelOverlap: false,
|
radius: ['40%', '70%'],
|
||||||
itemStyle: { borderRadius: 4, borderColor: '#fff', borderWidth: 2 },
|
avoidLabelOverlap: false,
|
||||||
label: { show: false },
|
itemStyle: { borderRadius: 4, borderColor: '#fff', borderWidth: 2 },
|
||||||
data: props.data
|
label: { show: false },
|
||||||
}]
|
data: items
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 拉取并渲染数据 */
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await StatisticsApi.getMessageTypeDistribution()
|
||||||
|
render(data)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (chartRef.value) {
|
if (chartRef.value) {
|
||||||
chart = echarts.init(chartRef.value)
|
chart = echarts.init(chartRef.value)
|
||||||
render()
|
await loadData()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
watch(() => props.data, render, { deep: true })
|
|
||||||
onUnmounted(() => chart?.dispose())
|
onUnmounted(() => chart?.dispose())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart-card { border-radius: 8px; margin-bottom: 16px; }
|
.chart-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,67 @@
|
||||||
<template>
|
<template>
|
||||||
<el-card shadow="never" class="chart-card">
|
<el-card shadow="never" class="chart-card">
|
||||||
<template #header>消息发送 TOP 10</template>
|
<template #header>消息发送 TOP 10</template>
|
||||||
<div ref="chartRef" style="width: 100%; height: 320px"></div>
|
<div ref="chartRef" v-loading="loading" style="width: 100%; height: 320px"></div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import * as echarts from 'echarts'
|
import * as echarts from 'echarts'
|
||||||
|
import * as StatisticsApi from '@/api/im/manager/statistics'
|
||||||
|
|
||||||
defineOptions({ name: 'ImStatisticsTopSendersChart' })
|
defineOptions({ name: 'ImStatisticsTopSendersChart' })
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
data: { userId: number; nickname: string; messageCount: number }[]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const chartRef = ref<HTMLElement>()
|
const chartRef = ref<HTMLElement>()
|
||||||
|
const loading = ref(false)
|
||||||
let chart: echarts.ECharts | null = null
|
let chart: echarts.ECharts | null = null
|
||||||
|
|
||||||
const render = () => {
|
/** 渲染横向条形图(从下到上排名) */
|
||||||
if (!chart) return
|
const render = (data: StatisticsApi.ImStatisticsTopSenderVO[]) => {
|
||||||
// 横向条形图:从下到上排名
|
const sorted = [...data].sort((a, b) => a.messageCount - b.messageCount)
|
||||||
const sorted = [...props.data].sort((a, b) => a.messageCount - b.messageCount)
|
chart?.setOption({
|
||||||
chart.setOption({
|
|
||||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||||
grid: { left: '3%', right: '4%', bottom: '3%', top: 20, containLabel: true },
|
grid: { left: '3%', right: '4%', bottom: '3%', top: 20, containLabel: true },
|
||||||
xAxis: { type: 'value', name: '消息数' },
|
xAxis: { type: 'value', name: '消息数' },
|
||||||
yAxis: {
|
yAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: sorted.map((d) => `${d.nickname}(${d.userId})`),
|
data: sorted.map((d) => `${d.nickname || d.userId}(${d.userId})`),
|
||||||
axisLabel: { width: 110, overflow: 'truncate' }
|
axisLabel: { width: 110, overflow: 'truncate' }
|
||||||
},
|
},
|
||||||
series: [{
|
series: [
|
||||||
type: 'bar',
|
{
|
||||||
data: sorted.map((d) => d.messageCount),
|
type: 'bar',
|
||||||
itemStyle: { color: '#409EFF', borderRadius: [0, 4, 4, 0] },
|
data: sorted.map((d) => d.messageCount),
|
||||||
barMaxWidth: 18
|
itemStyle: { color: '#409EFF', borderRadius: [0, 4, 4, 0] },
|
||||||
}]
|
barMaxWidth: 18
|
||||||
|
}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 拉取并渲染数据 */
|
||||||
|
const loadData = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await StatisticsApi.getTopSenders()
|
||||||
|
render(data)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
if (chartRef.value) {
|
if (chartRef.value) {
|
||||||
chart = echarts.init(chartRef.value)
|
chart = echarts.init(chartRef.value)
|
||||||
render()
|
await loadData()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
watch(() => props.data, render, { deep: true })
|
|
||||||
onUnmounted(() => chart?.dispose())
|
onUnmounted(() => chart?.dispose())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart-card { border-radius: 8px; margin-bottom: 16px; }
|
.chart-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ const buildOption = (dates: string[], reg: number[], act: number[]): echarts.ECh
|
||||||
xAxis: {
|
xAxis: {
|
||||||
type: 'category',
|
type: 'category',
|
||||||
data: dates,
|
data: dates,
|
||||||
axisLabel: { formatter: (v: string) => v.substring(5) }
|
axisLabel: { formatter: (v: string) => v.slice(5, 10) }
|
||||||
},
|
},
|
||||||
yAxis: [
|
yAxis: [
|
||||||
{ type: 'value', name: '新增注册', position: 'left' },
|
{ type: 'value', name: '新增注册', position: 'left' },
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="dashboard">
|
<div class="dashboard">
|
||||||
<!-- KPI 卡片 -->
|
<!-- 概览卡片 -->
|
||||||
<OverviewCards v-if="overview" :overview="overview" />
|
<OverviewCards v-if="overview" :overview="overview" />
|
||||||
|
|
||||||
<!-- 趋势 -->
|
<!-- 趋势 -->
|
||||||
|
|
@ -14,15 +14,15 @@
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<!-- 分布 -->
|
<!-- 分布 -->
|
||||||
<el-row v-if="distribution" :gutter="16">
|
<el-row :gutter="16">
|
||||||
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24">
|
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24">
|
||||||
<MessageTypeChart :data="distribution.messageTypeDistribution" />
|
<MessageTypeChart />
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24">
|
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24">
|
||||||
<GroupSizeChart :data="distribution.groupSizeDistribution" />
|
<GroupSizeChart />
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24">
|
<el-col :xl="8" :lg="8" :md="24" :sm="24" :xs="24">
|
||||||
<TopSendersChart :data="distribution.topSenders" />
|
<TopSendersChart />
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -39,17 +39,11 @@ import TopSendersChart from './components/TopSendersChart.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'ImStatistics' })
|
defineOptions({ name: 'ImStatistics' })
|
||||||
|
|
||||||
|
// 父页只拉概览数据;趋势 / 分布组件各自独立拉取,互不阻塞
|
||||||
const overview = ref<StatisticsApi.ImStatisticsOverviewVO>()
|
const overview = ref<StatisticsApi.ImStatisticsOverviewVO>()
|
||||||
const distribution = ref<StatisticsApi.ImStatisticsDistributionVO>()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 父页只拉 KPI + 分布;趋势组件自己内部拉,避免父组件维护 days 状态
|
overview.value = await StatisticsApi.getStatisticsOverview()
|
||||||
const [o, d] = await Promise.all([
|
|
||||||
StatisticsApi.getStatisticsOverview(),
|
|
||||||
StatisticsApi.getStatisticsDistribution()
|
|
||||||
])
|
|
||||||
overview.value = o
|
|
||||||
distribution.value = d
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue