feat: 增强管理员工作台聊天统计看板
- 工作台接入聊天消息总数、会话总数、活跃智能体数、趋势与 Top5 排行 - dashboard 接口新增 chatStatus 与聊天统计字段,分析库不可用时明确降级 - today 维度按小时聚合聊天趋势,并补充后端查询与测试覆盖
This commit is contained in:
@@ -8,30 +8,45 @@ export interface DashboardOverviewQuery {
|
||||
|
||||
export interface DashboardSummary {
|
||||
activeUserTotal: number;
|
||||
activeAssistantTotal: number;
|
||||
botTotal: number;
|
||||
chatMessageTotal: number;
|
||||
chatSessionTotal: number;
|
||||
knowledgeBaseTotal: number;
|
||||
userTotal: number;
|
||||
workflowTotal: number;
|
||||
}
|
||||
|
||||
export interface DashboardChatStatus {
|
||||
available: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface DashboardTrendItem {
|
||||
activeUserTotal: number;
|
||||
chatMessageTotal: number;
|
||||
chatSessionTotal: number;
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface DashboardDistributionItem {
|
||||
activeUserTotal: number;
|
||||
assistantId?: number | string;
|
||||
avgMessagePerSession?: number;
|
||||
botTotal: number;
|
||||
key: string;
|
||||
knowledgeBaseTotal: number;
|
||||
label: string;
|
||||
messageTotal?: number;
|
||||
sessionTotal?: number;
|
||||
userTotal: number;
|
||||
value: number;
|
||||
workflowTotal: number;
|
||||
}
|
||||
|
||||
export interface DashboardOverviewResponse {
|
||||
chatStatus: DashboardChatStatus;
|
||||
distribution: DashboardDistributionItem[];
|
||||
query: DashboardOverviewQuery;
|
||||
summary: DashboardSummary;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import type { EchartsUIType } from '@easyflow/plugins/echarts';
|
||||
|
||||
import type {
|
||||
DashboardDistributionItem,
|
||||
DashboardOverviewQuery,
|
||||
DashboardOverviewResponse,
|
||||
DashboardRange,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
import { AnalysisChartCard } from '@easyflow/common-ui';
|
||||
import { EchartsUI, useEcharts } from '@easyflow/plugins/echarts';
|
||||
import { useUserStore } from '@easyflow/stores';
|
||||
import { convertToRgb } from '@easyflow/utils';
|
||||
|
||||
import { RefreshRight } from '@element-plus/icons-vue';
|
||||
import { ElButton, ElEmpty, ElRadioButton, ElRadioGroup } from 'element-plus';
|
||||
@@ -28,6 +30,7 @@ import { ElButton, ElEmpty, ElRadioButton, ElRadioGroup } from 'element-plus';
|
||||
import { getDashboardOverview } from '#/api/dashboard';
|
||||
|
||||
interface SummaryCardItem {
|
||||
available?: boolean;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
@@ -54,27 +57,48 @@ const rangeOptions: Array<{ label: string; value: DashboardRange }> = [
|
||||
];
|
||||
|
||||
const emptySummary: DashboardSummary = {
|
||||
activeAssistantTotal: 0,
|
||||
activeUserTotal: 0,
|
||||
botTotal: 0,
|
||||
chatMessageTotal: 0,
|
||||
chatSessionTotal: 0,
|
||||
knowledgeBaseTotal: 0,
|
||||
userTotal: 0,
|
||||
workflowTotal: 0,
|
||||
};
|
||||
|
||||
const summary = computed(() => overview.value?.summary ?? emptySummary);
|
||||
const trends = computed<DashboardTrendItem[]>(
|
||||
() => overview.value?.trends ?? [],
|
||||
const trends = computed<DashboardTrendItem[]>(() => overview.value?.trends ?? []);
|
||||
const distribution = computed<DashboardDistributionItem[]>(
|
||||
() => overview.value?.distribution ?? [],
|
||||
);
|
||||
const chatAvailable = computed(() => overview.value?.chatStatus?.available !== false);
|
||||
const chatStatusMessage = computed(
|
||||
() => overview.value?.chatStatus?.message || '聊天数据不可用',
|
||||
);
|
||||
|
||||
const summaryCards = computed<SummaryCardItem[]>(() => [
|
||||
{ label: '用户总量', value: formatCount(summary.value.userTotal) },
|
||||
{ label: '活跃用户', value: formatCount(summary.value.activeUserTotal) },
|
||||
{ label: '助手数量', value: formatCount(summary.value.botTotal) },
|
||||
{ label: '工作流数量', value: formatCount(summary.value.workflowTotal) },
|
||||
{
|
||||
label: '知识库数量',
|
||||
value: formatCount(summary.value.knowledgeBaseTotal),
|
||||
},
|
||||
{
|
||||
available: chatAvailable.value,
|
||||
label: '聊天消息总数',
|
||||
value: formatOptionalCount(summary.value.chatMessageTotal, chatAvailable.value),
|
||||
},
|
||||
{
|
||||
available: chatAvailable.value,
|
||||
label: '聊天会话总数',
|
||||
value: formatOptionalCount(summary.value.chatSessionTotal, chatAvailable.value),
|
||||
},
|
||||
{
|
||||
available: chatAvailable.value,
|
||||
label: '活跃智能体数',
|
||||
value: formatOptionalCount(summary.value.activeAssistantTotal, chatAvailable.value),
|
||||
},
|
||||
]);
|
||||
|
||||
const updatedAtText = computed(() => {
|
||||
@@ -131,15 +155,24 @@ async function loadOverview() {
|
||||
|
||||
async function renderCharts() {
|
||||
await nextTick();
|
||||
if (!chatAvailable.value) {
|
||||
return;
|
||||
}
|
||||
renderTrendChart();
|
||||
}
|
||||
|
||||
function renderTrendChart() {
|
||||
const xAxisData = trends.value.map((item) => item.label);
|
||||
const activeUserData = trends.value.map((item) => item.activeUserTotal);
|
||||
const messageData = trends.value.map((item) => item.chatMessageTotal);
|
||||
const sessionData = trends.value.map((item) => item.chatSessionTotal);
|
||||
const primaryColor = getChartTokenColor('--primary');
|
||||
const successColor = getChartTokenColor('--success');
|
||||
const axisColor = getChartTokenColor('--border');
|
||||
const tooltipLineColor = getChartTokenColor('--accent');
|
||||
const textColor = getChartTokenColor('--foreground');
|
||||
|
||||
renderTrendEcharts({
|
||||
color: ['hsl(var(--primary))'],
|
||||
color: [primaryColor, successColor],
|
||||
grid: {
|
||||
bottom: 18,
|
||||
containLabel: true,
|
||||
@@ -148,21 +181,38 @@ function renderTrendChart() {
|
||||
top: 24,
|
||||
},
|
||||
legend: {
|
||||
itemGap: 18,
|
||||
top: 0,
|
||||
icon: 'circle',
|
||||
itemGap: 24,
|
||||
itemHeight: 10,
|
||||
itemWidth: 10,
|
||||
padding: [4, 12, 8, 12],
|
||||
textStyle: {
|
||||
color: textColor,
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
},
|
||||
top: 4,
|
||||
},
|
||||
tooltip: {
|
||||
axisPointer: {
|
||||
lineStyle: {
|
||||
color: tooltipLineColor,
|
||||
width: 1,
|
||||
},
|
||||
type: 'line',
|
||||
},
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'hsl(var(--border))',
|
||||
color: axisColor,
|
||||
},
|
||||
},
|
||||
axisTick: {
|
||||
show: false,
|
||||
},
|
||||
boundaryGap: false,
|
||||
data: xAxisData,
|
||||
type: 'category',
|
||||
},
|
||||
@@ -175,7 +225,7 @@ function renderTrendChart() {
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'hsl(var(--border))',
|
||||
color: axisColor,
|
||||
type: 'dashed',
|
||||
},
|
||||
},
|
||||
@@ -183,10 +233,57 @@ function renderTrendChart() {
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: activeUserData,
|
||||
name: '活跃用户',
|
||||
data: messageData,
|
||||
emphasis: {
|
||||
focus: 'none',
|
||||
itemStyle: {
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 3,
|
||||
color: primaryColor,
|
||||
},
|
||||
scale: true,
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: primaryColor,
|
||||
borderWidth: 2,
|
||||
color: '#ffffff',
|
||||
},
|
||||
lineStyle: {
|
||||
color: primaryColor,
|
||||
width: 3,
|
||||
},
|
||||
name: '消息数',
|
||||
smooth: true,
|
||||
symbolSize: 8,
|
||||
showSymbol: false,
|
||||
symbol: 'circle',
|
||||
symbolSize: 9,
|
||||
type: 'line',
|
||||
},
|
||||
{
|
||||
data: sessionData,
|
||||
emphasis: {
|
||||
focus: 'none',
|
||||
itemStyle: {
|
||||
borderColor: '#ffffff',
|
||||
borderWidth: 3,
|
||||
color: successColor,
|
||||
},
|
||||
scale: true,
|
||||
},
|
||||
itemStyle: {
|
||||
borderColor: successColor,
|
||||
borderWidth: 2,
|
||||
color: '#ffffff',
|
||||
},
|
||||
lineStyle: {
|
||||
color: successColor,
|
||||
width: 3,
|
||||
},
|
||||
name: '会话数',
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
symbol: 'circle',
|
||||
symbolSize: 9,
|
||||
type: 'line',
|
||||
},
|
||||
],
|
||||
@@ -205,6 +302,28 @@ function formatCount(value?: number) {
|
||||
return Number(value || 0).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function formatOptionalCount(value: number | undefined, available: boolean) {
|
||||
return available ? formatCount(value) : '--';
|
||||
}
|
||||
|
||||
function getChartTokenColor(variableName: string) {
|
||||
if (typeof window === 'undefined') {
|
||||
return '#3b82f6';
|
||||
}
|
||||
const value = getComputedStyle(document.documentElement)
|
||||
.getPropertyValue(variableName)
|
||||
.trim();
|
||||
return value ? convertToRgb(`hsl(${value})`) : '#3b82f6';
|
||||
}
|
||||
|
||||
function formatAvg(value?: number) {
|
||||
const safeValue = Number(value || 0);
|
||||
return safeValue.toLocaleString('zh-CN', {
|
||||
maximumFractionDigits: 1,
|
||||
minimumFractionDigits: safeValue > 0 && safeValue < 10 ? 1 : 0,
|
||||
});
|
||||
}
|
||||
|
||||
function formatDateTime(value: string) {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
@@ -270,7 +389,7 @@ onBeforeUnmount(() => {
|
||||
|
||||
<section
|
||||
v-if="isLoading && !overview"
|
||||
class="grid gap-4 md:grid-cols-2 xl:grid-cols-4"
|
||||
class="grid gap-4 md:grid-cols-2 xl:grid-cols-3"
|
||||
>
|
||||
<div
|
||||
v-for="item in 8"
|
||||
@@ -294,7 +413,7 @@ onBeforeUnmount(() => {
|
||||
</section>
|
||||
|
||||
<template v-else>
|
||||
<section class="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
|
||||
<section class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||
<div
|
||||
v-for="item in summaryCards"
|
||||
:key="item.label"
|
||||
@@ -308,16 +427,72 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
{{ item.value }}
|
||||
</div>
|
||||
<div
|
||||
v-if="item.available === false"
|
||||
class="text-muted-foreground mt-2 text-xs"
|
||||
>
|
||||
{{ chatStatusMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<AnalysisChartCard title="趋势变化">
|
||||
<div class="space-y-2">
|
||||
<p class="text-muted-foreground text-sm">
|
||||
观察活跃用户在所选时间范围内的变化趋势。
|
||||
</p>
|
||||
<EchartsUI ref="trendChartRef" height="360px" />
|
||||
<section class="grid gap-4 xl:grid-cols-[minmax(0,2fr)_minmax(320px,1fr)]">
|
||||
<AnalysisChartCard title="聊天趋势">
|
||||
<template v-if="chatAvailable">
|
||||
<div class="space-y-2">
|
||||
<p class="text-muted-foreground text-sm">
|
||||
观察所选时间范围内消息数与会话数的趋势变化。
|
||||
</p>
|
||||
<EchartsUI ref="trendChartRef" height="360px" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="flex min-h-[360px] items-center justify-center">
|
||||
<ElEmpty :description="chatStatusMessage" />
|
||||
</div>
|
||||
</AnalysisChartCard>
|
||||
|
||||
<AnalysisChartCard title="智能体排行">
|
||||
<template v-if="chatAvailable">
|
||||
<div v-if="distribution.length" class="space-y-3">
|
||||
<div
|
||||
v-for="(item, index) in distribution"
|
||||
:key="item.key || item.label"
|
||||
class="border-border/60 bg-muted/20 flex items-start justify-between rounded-2xl border px-4 py-4"
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="bg-primary/10 text-primary flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-sm font-semibold"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-sm font-semibold">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
消息 {{ formatCount(item.messageTotal) }} · 会话
|
||||
{{ formatCount(item.sessionTotal) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="text-foreground text-lg font-semibold">
|
||||
{{ formatAvg(item.avgMessagePerSession) }}
|
||||
</div>
|
||||
<div class="text-muted-foreground mt-1 text-xs">
|
||||
平均每会话消息数
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex min-h-[360px] items-center justify-center">
|
||||
<ElEmpty description="暂无聊天排行数据" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="flex min-h-[360px] items-center justify-center">
|
||||
<ElEmpty :description="chatStatusMessage" />
|
||||
</div>
|
||||
</AnalysisChartCard>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user