feat(im): 修一批管理端统计与成员选择器细节

- 消息趋势 / 用户趋势图表加 loading 态(接口错误由全局拦截器统一提示)
- 群成员选择器 grid 模式补右上角 × 移除按钮
- 统计接口 6 个 API 补全返回值泛型
im
YunaiV 2026-05-22 20:15:15 +08:00
parent 38ecc4f40c
commit 72d8c499a4
4 changed files with 38 additions and 14 deletions

View File

@ -37,30 +37,34 @@ export interface ImStatisticsTopSenderVO {
// 获得 KPI 概览 // 获得 KPI 概览
export const getStatisticsOverview = (): Promise<ImStatisticsOverviewVO> => { export const getStatisticsOverview = (): Promise<ImStatisticsOverviewVO> => {
return request.get({ url: '/im/manager/statistics/overview' }) return request.get<ImStatisticsOverviewVO>({ url: '/im/manager/statistics/overview' })
} }
// 获得消息趋势(私聊 + 群聊双线) // 获得消息趋势(私聊 + 群聊双线)
export const getMessageTrend = (days: number): Promise<ImStatisticsTrendVO> => { export const getMessageTrend = (days: number): Promise<ImStatisticsTrendVO> => {
return request.get({ url: '/im/manager/statistics/message-trend', params: { days } }) return request.get<ImStatisticsTrendVO>({ url: '/im/manager/statistics/message-trend', params: { days } })
} }
// 获得用户趋势(新增注册 + 日活双线) // 获得用户趋势(新增注册 + 日活双线)
export const getUserTrend = (days: number): Promise<ImStatisticsTrendVO> => { export const getUserTrend = (days: number): Promise<ImStatisticsTrendVO> => {
return request.get({ url: '/im/manager/statistics/user-trend', params: { days } }) return request.get<ImStatisticsTrendVO>({ url: '/im/manager/statistics/user-trend', params: { days } })
} }
// 获得消息类型分布(最近 30 天) // 获得消息类型分布(最近 30 天)
export const getMessageTypeDistribution = (): Promise<ImStatisticsMessageTypeVO[]> => { export const getMessageTypeDistribution = (): Promise<ImStatisticsMessageTypeVO[]> => {
return request.get({ url: '/im/manager/statistics/message-type-distribution' }) return request.get<ImStatisticsMessageTypeVO[]>({
url: '/im/manager/statistics/message-type-distribution'
})
} }
// 获得群规模分布 // 获得群规模分布
export const getGroupSizeDistribution = (): Promise<ImStatisticsGroupSizeVO[]> => { export const getGroupSizeDistribution = (): Promise<ImStatisticsGroupSizeVO[]> => {
return request.get({ url: '/im/manager/statistics/group-size-distribution' }) return request.get<ImStatisticsGroupSizeVO[]>({
url: '/im/manager/statistics/group-size-distribution'
})
} }
// 获得消息 TOP 发送者(最近 30 天) // 获得消息 TOP 发送者(最近 30 天)
export const getTopSenders = (): Promise<ImStatisticsTopSenderVO[]> => { export const getTopSenders = (): Promise<ImStatisticsTopSenderVO[]> => {
return request.get({ url: '/im/manager/statistics/top-senders' }) return request.get<ImStatisticsTopSenderVO[]>({ url: '/im/manager/statistics/top-senders' })
} }

View File

@ -84,13 +84,21 @@
</div> </div>
</template> </template>
<!-- grid 形态宫格预览管理员设置等场景沿用 --> <!-- grid 形态宫格预览 locked 成员右上角叠加 × 移除locked 不渲染 -->
<div v-else class="flex flex-wrap p-2.5"> <div v-else class="flex flex-wrap p-2.5">
<GroupMemberGrid <GroupMemberGrid
v-for="member in selectedMembers" v-for="member in selectedMembers"
:key="member.userId" :key="member.userId"
:member="member" :member="member"
/> >
<Icon
v-if="!isLocked(member)"
icon="ant-design:close-circle-filled"
:size="16"
class="absolute top-0 right-0 cursor-pointer transition-colors text-[var(--el-text-color-placeholder)] hover:text-[var(--el-color-danger)]"
@click="handleToggle(member)"
/>
</GroupMemberGrid>
</div> </div>
<div <div

View File

@ -10,7 +10,7 @@
</el-select> </el-select>
</div> </div>
</template> </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>
@ -22,6 +22,7 @@ defineOptions({ name: 'ImStatisticsMessageTrendChart' })
const chartRef = ref<HTMLElement>() const chartRef = ref<HTMLElement>()
const days = ref(7) const days = ref(7)
const loading = ref(false)
let chart: echarts.ECharts | null = null let chart: echarts.ECharts | null = null
const buildOption = (dates: string[], priv: number[], grp: number[]): echarts.EChartsCoreOption => ({ const buildOption = (dates: string[], priv: number[], grp: number[]): echarts.EChartsCoreOption => ({
@ -71,8 +72,13 @@ const buildOption = (dates: string[], priv: number[], grp: number[]): echarts.EC
}) })
const loadData = async () => { const loadData = async () => {
const data = await StatisticsApi.getMessageTrend(days.value) loading.value = true
chart?.setOption(buildOption(data.dates, data.series.private || [], data.series.group || [])) try {
const data = await StatisticsApi.getMessageTrend(days.value)
chart?.setOption(buildOption(data.dates, data.series.private || [], data.series.group || []))
} finally {
loading.value = false
}
} }
onMounted(async () => { onMounted(async () => {

View File

@ -10,7 +10,7 @@
</el-select> </el-select>
</div> </div>
</template> </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>
@ -22,6 +22,7 @@ defineOptions({ name: 'ImStatisticsUserTrendChart' })
const chartRef = ref<HTMLElement>() const chartRef = ref<HTMLElement>()
const days = ref(7) const days = ref(7)
const loading = ref(false)
let chart: echarts.ECharts | null = null let chart: echarts.ECharts | null = null
const buildOption = (dates: string[], reg: number[], act: number[]): echarts.EChartsCoreOption => ({ const buildOption = (dates: string[], reg: number[], act: number[]): echarts.EChartsCoreOption => ({
@ -50,8 +51,13 @@ const buildOption = (dates: string[], reg: number[], act: number[]): echarts.ECh
}) })
const loadData = async () => { const loadData = async () => {
const data = await StatisticsApi.getUserTrend(days.value) loading.value = true
chart?.setOption(buildOption(data.dates, data.series.register || [], data.series.active || [])) try {
const data = await StatisticsApi.getUserTrend(days.value)
chart?.setOption(buildOption(data.dates, data.series.register || [], data.series.active || []))
} finally {
loading.value = false
}
} }
onMounted(async () => { onMounted(async () => {