feat: 增强工作台趋势概览与聊天排行

- 支持用户活跃与智能体活跃趋势统计及自定义时间范围

- 增加用户活跃榜与智能体趋势数据结构及查询实现

- 同步补齐工作台页面展示与定向测试
This commit is contained in:
2026-05-06 19:22:09 +08:00
parent 5827ecde42
commit 31b0e21d3d
20 changed files with 2087 additions and 146 deletions

View File

@@ -1,15 +1,18 @@
import { requestClient } from '#/api/request';
export type DashboardRange = '7d' | '30d' | 'today';
export type DashboardRange = '7d' | '30d' | 'custom' | 'today';
export interface DashboardOverviewQuery {
endDate?: string;
range?: DashboardRange;
startDate?: string;
}
export interface DashboardSummary {
activeUserTotal: number;
activeAssistantTotal: number;
botTotal: number;
chatActiveUserTotal: number;
chatMessageTotal: number;
chatSessionTotal: number;
knowledgeBaseTotal: number;
@@ -30,10 +33,24 @@ export interface DashboardTrendItem {
label: string;
}
export interface DashboardAssistantTrendPoint {
key: string;
label: string;
sessionTotal: number;
}
export interface DashboardAssistantTrendSeries {
assistantId?: number | string;
label: string;
points: DashboardAssistantTrendPoint[];
totalSessionCount: number;
}
export interface DashboardDistributionItem {
activeUserTotal: number;
assistantId?: number | string;
avgMessagePerSession?: number;
avgSessionPerUser?: number;
botTotal: number;
key: string;
knowledgeBaseTotal: number;
@@ -45,13 +62,23 @@ export interface DashboardDistributionItem {
workflowTotal: number;
}
export interface DashboardUserRankItem {
assistantTotal: number;
label: string;
messageTotal: number;
sessionTotal: number;
userId?: number | string;
}
export interface DashboardOverviewResponse {
assistantTrends: DashboardAssistantTrendSeries[];
chatStatus: DashboardChatStatus;
distribution: DashboardDistributionItem[];
query: DashboardOverviewQuery;
summary: DashboardSummary;
trends: DashboardTrendItem[];
updatedAt: string;
userRanks: DashboardUserRankItem[];
}
export async function getDashboardOverview(params: DashboardOverviewQuery) {

View File

@@ -2,12 +2,14 @@
import type { EchartsUIType } from '@easyflow/plugins/echarts';
import type {
DashboardAssistantTrendSeries,
DashboardDistributionItem,
DashboardOverviewQuery,
DashboardOverviewResponse,
DashboardRange,
DashboardSummary,
DashboardTrendItem,
DashboardUserRankItem,
} from '#/api/dashboard';
import {
@@ -17,6 +19,7 @@ import {
onMounted,
reactive,
ref,
watch,
} from 'vue';
import { AnalysisChartCard } from '@easyflow/common-ui';
@@ -25,10 +28,26 @@ 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';
import {
ElButton,
ElDatePicker,
ElEmpty,
ElPopover,
ElRadioButton,
ElRadioGroup,
} from 'element-plus';
import { getDashboardOverview } from '#/api/dashboard';
type DashboardTrendMode = 'assistantActive' | 'usage' | 'userActive';
interface AssistantTrendSelectionItem {
assistantKey: string;
color: string;
isSelected: boolean;
series: DashboardAssistantTrendSeries;
}
interface SummaryCardItem {
available?: boolean;
label: string;
@@ -39,16 +58,23 @@ let greetingTimer: null | ReturnType<typeof setInterval> = null;
const userStore = useUserStore();
const now = ref(new Date());
const filters = reactive<Required<DashboardOverviewQuery>>({
const filters = reactive<DashboardOverviewQuery>({
range: '7d',
});
const customDateRange = ref<string[]>([]);
const overview = ref<DashboardOverviewResponse | null>(null);
const isLoading = ref(false);
const errorMessage = ref('');
const trendMode = ref<DashboardTrendMode>('usage');
const selectedAssistantTrendKeys = ref<string[]>([]);
const trendChartRef = ref<EchartsUIType>();
const { renderEcharts: renderTrendEcharts } = useEcharts(trendChartRef);
const {
getChartInstance: getTrendChartInstance,
renderEcharts: renderTrendEcharts,
resize: resizeTrendChart,
} = useEcharts(trendChartRef);
const rangeOptions: Array<{ label: string; value: DashboardRange }> = [
{ label: '今日', value: 'today' },
@@ -56,10 +82,17 @@ const rangeOptions: Array<{ label: string; value: DashboardRange }> = [
{ label: '近 30 天', value: '30d' },
];
const trendModeOptions: Array<{ label: string; value: DashboardTrendMode }> = [
{ label: '使用量趋势', value: 'usage' },
{ label: '用户活跃', value: 'userActive' },
{ label: '智能体活跃', value: 'assistantActive' },
];
const emptySummary: DashboardSummary = {
activeAssistantTotal: 0,
activeUserTotal: 0,
botTotal: 0,
chatActiveUserTotal: 0,
chatMessageTotal: 0,
chatSessionTotal: 0,
knowledgeBaseTotal: 0,
@@ -68,36 +101,67 @@ const emptySummary: DashboardSummary = {
};
const summary = computed(() => overview.value?.summary ?? emptySummary);
const trends = computed<DashboardTrendItem[]>(() => overview.value?.trends ?? []);
const trends = computed<DashboardTrendItem[]>(
() => overview.value?.trends ?? [],
);
const assistantTrends = computed<DashboardAssistantTrendSeries[]>(
() => overview.value?.assistantTrends ?? [],
);
const distribution = computed<DashboardDistributionItem[]>(
() => overview.value?.distribution ?? [],
);
const chatAvailable = computed(() => overview.value?.chatStatus?.available !== false);
const userRanks = computed<DashboardUserRankItem[]>(
() => overview.value?.userRanks ?? [],
);
const chatAvailable = computed(
() => overview.value?.chatStatus?.available !== false,
);
const chatStatusMessage = computed(
() => overview.value?.chatStatus?.message || '聊天数据不可用',
);
const isUsageTrend = computed(() => trendMode.value === 'usage');
const isUserActiveTrend = computed(() => trendMode.value === 'userActive');
const isAssistantActiveTrend = computed(
() => trendMode.value === 'assistantActive',
);
const summaryCards = computed<SummaryCardItem[]>(() => [
{ label: '助手数量', value: formatCount(summary.value.botTotal) },
{ label: '工作流数量', value: formatCount(summary.value.workflowTotal) },
{
label: '知识库数量',
value: formatCount(summary.value.knowledgeBaseTotal),
available: chatAvailable.value,
label: 'AI活跃用户',
value: formatOptionalCount(
summary.value.chatActiveUserTotal,
chatAvailable.value,
),
},
{
available: chatAvailable.value,
label: '聊天消息总数',
value: formatOptionalCount(summary.value.chatMessageTotal, chatAvailable.value),
label: '活跃智能体',
value: formatOptionalCount(
summary.value.activeAssistantTotal,
chatAvailable.value,
),
},
{
available: chatAvailable.value,
label: '聊天会话总数',
value: formatOptionalCount(summary.value.chatSessionTotal, chatAvailable.value),
value: formatOptionalCount(
summary.value.chatSessionTotal,
chatAvailable.value,
),
},
{
available: chatAvailable.value,
label: '活跃智能体数',
value: formatOptionalCount(summary.value.activeAssistantTotal, chatAvailable.value),
label: '聊天消息总数',
value: formatOptionalCount(
summary.value.chatMessageTotal,
chatAvailable.value,
),
},
{ label: '智能体总数', value: formatCount(summary.value.botTotal) },
{
label: '知识库总数',
value: formatCount(summary.value.knowledgeBaseTotal),
},
]);
@@ -134,15 +198,99 @@ const greetingTitle = computed(
() => `${greetingText.value}${displayName.value}`,
);
const trendDescription = computed(() => {
if (isUsageTrend.value) {
return '消息与会话趋势';
}
if (isUserActiveTrend.value) {
return '用户活跃趋势';
}
return '智能体活跃趋势';
});
const selectedAssistantTrendSet = computed(
() => new Set(selectedAssistantTrendKeys.value),
);
const assistantTrendColors = computed(() =>
buildAssistantTrendPalette(assistantTrends.value.length),
);
const assistantTrendSelectionItems = computed<AssistantTrendSelectionItem[]>(
() =>
assistantTrends.value.map((series, index) => {
const assistantKey = getAssistantTrendKey(
series.assistantId,
series.label,
);
return {
assistantKey,
color:
assistantTrendColors.value[index] || getChartTokenColor('--primary'),
isSelected: selectedAssistantTrendSet.value.has(assistantKey),
series,
};
}),
);
const selectedAssistantTrends = computed<AssistantTrendSelectionItem[]>(() =>
assistantTrendSelectionItems.value.filter((item) => item.isSelected),
);
const selectedAssistantTrendCount = computed(
() => selectedAssistantTrends.value.length,
);
const selectedAssistantTrendPreviewItems = computed(() =>
selectedAssistantTrends.value.slice(0, 3),
);
const assistantTrendSelectorLabel = computed(() => {
const total = assistantTrendSelectionItems.value.length;
if (total === 0) {
return '无可选智能体';
}
return `${selectedAssistantTrendCount.value}/${total} 已选`;
});
const assistantTrendSelectorSummary = computed(() => {
const count = selectedAssistantTrendCount.value;
if (count === 0) {
return '未选择智能体';
}
if (count === 1) {
return selectedAssistantTrends.value[0]?.series.label || '已选择 1 个';
}
return `${selectedAssistantTrends.value[0]?.series.label || '已选择'}${count}`;
});
const showAssistantTrendNoData = computed(
() =>
isAssistantActiveTrend.value &&
assistantTrendSelectionItems.value.length === 0,
);
const showAssistantTrendEmptySelection = computed(
() =>
isAssistantActiveTrend.value &&
assistantTrendSelectionItems.value.length > 0 &&
selectedAssistantTrends.value.length === 0,
);
const showTrendChart = computed(() => chatAvailable.value);
async function loadOverview() {
isLoading.value = true;
errorMessage.value = '';
try {
const data = await getDashboardOverview({
endDate: filters.range === 'custom' ? filters.endDate : undefined,
range: filters.range,
startDate: filters.range === 'custom' ? filters.startDate : undefined,
});
overview.value = data;
resetAssistantTrendSelection();
await renderCharts();
} catch (error) {
overview.value = null;
@@ -155,24 +303,197 @@ async function loadOverview() {
async function renderCharts() {
await nextTick();
if (!chatAvailable.value) {
if (!showTrendChart.value) {
return;
}
renderTrendChart();
}
function renderTrendChart() {
const xAxisData = trends.value.map((item) => item.label);
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 warningColor = getChartTokenColor('--warning');
const destructiveColor = getChartTokenColor('--destructive');
const axisColor = getChartTokenColor('--border');
const tooltipLineColor = getChartTokenColor('--accent');
const textColor = getChartTokenColor('--foreground');
if (isAssistantActiveTrend.value) {
const xAxisData =
assistantTrendSelectionItems.value[0]?.series.points.map(
(point) => point.label,
) ?? [];
const assistantSeries = assistantTrendSelectionItems.value.map((item) => ({
data: item.series.points.map((point) => point.sessionTotal),
emphasis: {
focus: 'series' as const,
},
itemStyle: {
borderColor: item.color,
borderWidth: 2,
color: '#ffffff',
},
lineStyle: {
color: item.color,
width: 3,
},
name: item.series.label,
smooth: true,
showSymbol: false,
symbol: 'circle',
symbolSize: 8,
type: 'line' as const,
}));
renderTrendEcharts({
color: assistantTrendSelectionItems.value.map((item) => item.color),
grid: {
bottom: 18,
containLabel: true,
left: 12,
right: 12,
top: 12,
},
legend: {
data: assistantTrendSelectionItems.value.map(
(item) => item.series.label,
),
selected: buildAssistantTrendSelectedMap(),
show: false,
},
tooltip: {
axisPointer: {
lineStyle: {
color: tooltipLineColor,
width: 1,
},
type: 'line',
},
trigger: 'axis',
},
xAxis: {
axisLine: {
lineStyle: {
color: axisColor,
},
},
axisTick: {
show: false,
},
boundaryGap: false,
data: xAxisData,
type: 'category',
},
yAxis: {
axisLine: {
show: false,
},
axisTick: {
show: false,
},
splitLine: {
lineStyle: {
color: axisColor,
type: 'dashed',
},
},
type: 'value',
},
series: assistantSeries,
});
return;
}
const xAxisData = trends.value.map((item) => item.label);
const messageData = trends.value.map((item) => item.chatMessageTotal);
const sessionData = trends.value.map((item) => item.chatSessionTotal);
const activeUserData = trends.value.map((item) => item.activeUserTotal);
const usageSeries = [
{
data: messageData,
emphasis: {
focus: 'none' as const,
itemStyle: {
borderColor: '#ffffff',
borderWidth: 3,
color: primaryColor,
},
scale: true,
},
itemStyle: {
borderColor: primaryColor,
borderWidth: 2,
color: '#ffffff',
},
lineStyle: {
color: primaryColor,
width: 3,
},
name: '消息数',
smooth: true,
showSymbol: false,
symbol: 'circle',
symbolSize: 9,
type: 'line' as const,
},
{
data: sessionData,
emphasis: {
focus: 'none' as const,
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' as const,
},
];
const activeSeries = [
{
data: activeUserData,
emphasis: {
focus: 'none' as const,
},
itemStyle: {
borderColor: primaryColor,
borderWidth: 2,
color: '#ffffff',
},
lineStyle: {
color: primaryColor,
width: 3,
},
name: '活跃用户数',
smooth: true,
showSymbol: false,
symbol: 'circle',
symbolSize: 9,
type: 'line' as const,
},
];
renderTrendEcharts({
color: [primaryColor, successColor],
color: isUsageTrend.value
? [primaryColor, successColor]
: [primaryColor, warningColor, destructiveColor],
grid: {
bottom: 18,
containLabel: true,
@@ -231,62 +552,7 @@ function renderTrendChart() {
},
type: 'value',
},
series: [
{
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,
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',
},
],
series: isUsageTrend.value ? usageSeries : activeSeries,
});
}
@@ -295,9 +561,130 @@ function handleRangeChange(value: boolean | number | string | undefined) {
return;
}
filters.range = value;
customDateRange.value = [];
filters.startDate = undefined;
filters.endDate = undefined;
void loadOverview();
}
function handleCustomRangeChange(value: string[] | undefined) {
if (!value || value.length !== 2 || !value[0] || !value[1]) {
customDateRange.value = [];
filters.range = '7d';
filters.startDate = undefined;
filters.endDate = undefined;
void loadOverview();
return;
}
filters.range = 'custom';
filters.startDate = value[0];
filters.endDate = value[1];
void loadOverview();
}
function handleTrendModeChange(value: boolean | number | string | undefined) {
if (
value !== 'usage' &&
value !== 'userActive' &&
value !== 'assistantActive'
) {
return;
}
trendMode.value = value;
}
function resetAssistantTrendSelection() {
selectedAssistantTrendKeys.value = assistantTrends.value.map((series) =>
getAssistantTrendKey(series.assistantId, series.label),
);
}
function toggleAssistantTrend(assistantKey: string) {
const currentItem = assistantTrendSelectionItems.value.find(
(item) => item.assistantKey === assistantKey,
);
if (!currentItem) {
return;
}
const selectedKeys = new Set(selectedAssistantTrendKeys.value);
let actionType: 'legendSelect' | 'legendUnSelect' = 'legendSelect';
if (selectedKeys.has(assistantKey)) {
selectedKeys.delete(assistantKey);
actionType = 'legendUnSelect';
} else {
selectedKeys.add(assistantKey);
}
selectedAssistantTrendKeys.value = [...selectedKeys];
if (!isAssistantActiveTrend.value) {
return;
}
void nextTick(() => {
const chartInstance = getTrendChartInstance();
if (!chartInstance) {
return;
}
chartInstance.dispatchAction({
name: currentItem.series.label,
type: actionType,
});
});
}
function selectAllAssistantTrends() {
selectedAssistantTrendKeys.value = assistantTrendSelectionItems.value.map(
(item) => item.assistantKey,
);
if (!isAssistantActiveTrend.value) {
return;
}
void nextTick(() => {
renderTrendChart();
resizeTrendChart();
});
}
function clearAssistantTrendSelection() {
const previousItems = [...selectedAssistantTrends.value];
selectedAssistantTrendKeys.value = [];
if (!isAssistantActiveTrend.value) {
return;
}
void nextTick(() => {
const chartInstance = getTrendChartInstance();
if (!chartInstance) {
return;
}
for (const item of previousItems) {
chartInstance.dispatchAction({
name: item.series.label,
type: 'legendUnSelect',
});
}
});
}
function buildAssistantTrendSelectedMap() {
return Object.fromEntries(
assistantTrendSelectionItems.value.map((item) => [
item.series.label,
item.isSelected,
]),
);
}
watch(
[trendMode, chatAvailable, trends, assistantTrends],
() => {
if (!showTrendChart.value) {
return;
}
void renderCharts();
},
{
flush: 'post',
},
);
function formatCount(value?: number) {
return Number(value || 0).toLocaleString('zh-CN');
}
@@ -307,13 +694,54 @@ function formatOptionalCount(value: number | undefined, available: boolean) {
}
function getChartTokenColor(variableName: string) {
return convertToRgb(`hsl(${getChartTokenHsl(variableName)})`);
}
function getChartTokenHsl(variableName: string) {
if (typeof window === 'undefined') {
return '#3b82f6';
return '211 100% 50%';
}
const value = getComputedStyle(document.documentElement)
.getPropertyValue(variableName)
.trim();
return value ? convertToRgb(`hsl(${value})`) : '#3b82f6';
return value || '211 100% 50%';
}
function buildAssistantTrendPalette(total: number) {
const palette = [
getChartTokenColor('--primary'),
getChartTokenColor('--success'),
getChartTokenColor('--warning'),
getChartTokenColor('--destructive'),
];
if (total <= palette.length) {
return palette.slice(0, total);
}
const primaryHue = extractHue(getChartTokenHsl('--primary')) ?? 211;
const derivedHues = [46, 92, 138, 184, 230, 276].map(
(offset) => `hsl(${normalizeHue(primaryHue + offset)} 68% 52%)`,
);
return [...palette, ...derivedHues].slice(0, total);
}
function getAssistantTrendKey(
assistantId: number | string | undefined,
label: string,
) {
return assistantId === undefined
? `label:${label}`
: `assistant:${assistantId}`;
}
function extractHue(hslValue: string) {
const [hue] = hslValue.split(/\s+/);
const parsed = Number.parseFloat(hue || '');
return Number.isFinite(parsed) ? parsed : null;
}
function normalizeHue(value: number) {
const normalized = value % 360;
return normalized < 0 ? normalized + 360 : normalized;
}
function formatAvg(value?: number) {
@@ -363,6 +791,16 @@ onBeforeUnmount(() => {
<div
class="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center sm:justify-end"
>
<ElDatePicker
v-model="customDateRange"
class="w-full sm:w-[280px]"
type="daterange"
unlink-panels
value-format="YYYY-MM-DD"
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleCustomRangeChange"
/>
<ElRadioGroup
:model-value="filters.range"
size="default"
@@ -392,7 +830,7 @@ onBeforeUnmount(() => {
class="grid gap-4 md:grid-cols-2 xl:grid-cols-3"
>
<div
v-for="item in 8"
v-for="item in 6"
:key="item"
class="border-border bg-muted/50 h-28 animate-pulse rounded-3xl border"
></div>
@@ -436,14 +874,134 @@ onBeforeUnmount(() => {
</div>
</section>
<section class="grid gap-4 xl:grid-cols-[minmax(0,2fr)_minmax(320px,1fr)]">
<AnalysisChartCard title="聊天趋势">
<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 class="space-y-4">
<div
class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<div class="text-muted-foreground text-sm">
{{ trendDescription }}
</div>
<div class="flex flex-wrap items-center justify-end gap-2">
<ElPopover
v-if="
isAssistantActiveTrend &&
assistantTrendSelectionItems.length > 0
"
placement="bottom-end"
trigger="click"
:width="340"
>
<template #reference>
<button
type="button"
class="border-border/60 bg-background/88 hover:border-border hover:bg-muted/20 text-foreground inline-flex h-9 max-w-[260px] items-center gap-2 rounded-2xl border px-3.5 text-sm transition-[border-color,background-color,box-shadow] duration-200"
>
<span
v-if="selectedAssistantTrendPreviewItems.length > 0"
class="flex shrink-0 items-center -space-x-1.5"
>
<span
v-for="item in selectedAssistantTrendPreviewItems"
:key="item.assistantKey"
class="border-background inline-block h-2.5 w-2.5 rounded-full border"
:style="{ backgroundColor: item.color }"
></span>
</span>
<span class="min-w-0 flex-1 truncate font-medium">
{{ assistantTrendSelectorSummary }}
</span>
<span
class="bg-muted text-muted-foreground shrink-0 rounded-full px-2 py-0.5 text-[11px] font-medium"
>
{{ assistantTrendSelectorLabel }}
</span>
</button>
</template>
<div class="space-y-3">
<div class="flex items-center justify-between gap-3">
<div class="text-sm font-semibold">智能体</div>
<div class="flex items-center gap-2">
<button
type="button"
class="text-primary text-xs font-medium"
@click="selectAllAssistantTrends"
>
全选
</button>
<button
type="button"
class="text-muted-foreground text-xs font-medium"
@click="clearAssistantTrendSelection"
>
清空
</button>
</div>
</div>
<div class="grid max-h-64 gap-2 overflow-y-auto">
<button
v-for="item in assistantTrendSelectionItems"
:key="item.assistantKey"
type="button"
class="border-border/60 bg-background hover:border-border flex items-center gap-3 rounded-2xl border px-3 py-2 text-left transition-colors"
:class="
item.isSelected
? 'border-primary/40 bg-primary/5'
: 'text-muted-foreground'
"
@click="toggleAssistantTrend(item.assistantKey)"
>
<span
class="h-2.5 w-2.5 shrink-0 rounded-full"
:style="{ backgroundColor: item.color }"
></span>
<span
class="min-w-0 flex-1 truncate text-sm font-medium"
>
{{ item.series.label }}
</span>
<span class="text-muted-foreground shrink-0 text-xs">
{{ formatCount(item.series.totalSessionCount) }}
</span>
</button>
</div>
</div>
</ElPopover>
<ElRadioGroup
:model-value="trendMode"
size="small"
@update:model-value="handleTrendModeChange"
>
<ElRadioButton
v-for="item in trendModeOptions"
:key="item.value"
:value="item.value"
:label="item.label"
>
{{ item.label }}
</ElRadioButton>
</ElRadioGroup>
</div>
</div>
<div class="relative min-h-[360px]">
<div
v-if="showAssistantTrendNoData"
class="bg-background/92 absolute inset-0 z-10 flex items-center justify-center"
>
<ElEmpty description="暂无智能体活跃数据" />
</div>
<div
v-else-if="showAssistantTrendEmptySelection"
class="bg-background/92 absolute inset-0 z-10 flex items-center justify-center"
>
<ElEmpty description="请选择智能体" />
</div>
<EchartsUI ref="trendChartRef" height="360px" />
</div>
</div>
</template>
<div v-else class="flex min-h-[360px] items-center justify-center">
@@ -451,9 +1009,9 @@ onBeforeUnmount(() => {
</div>
</AnalysisChartCard>
<AnalysisChartCard title="智能体排行">
<AnalysisChartCard title="智能体使用榜">
<template v-if="chatAvailable">
<div v-if="distribution.length" class="space-y-3">
<div v-if="distribution.length > 0" class="space-y-3">
<div
v-for="(item, index) in distribution"
:key="item.key || item.label"
@@ -471,24 +1029,24 @@ onBeforeUnmount(() => {
{{ item.label }}
</div>
<div class="text-muted-foreground mt-1 text-xs">
消息 {{ formatCount(item.messageTotal) }} · 会话
{{ formatCount(item.sessionTotal) }}
用户 {{ formatCount(item.userTotal) }} · 消息
{{ formatCount(item.messageTotal) }}
</div>
</div>
</div>
</div>
<div class="text-right">
<div class="text-foreground text-lg font-semibold">
{{ formatAvg(item.avgMessagePerSession) }}
{{ formatCount(item.sessionTotal) }}
</div>
<div class="text-muted-foreground mt-1 text-xs">
平均每会话消息数
会话数 · 人均 {{ formatAvg(item.avgSessionPerUser) }}
</div>
</div>
</div>
</div>
<div v-else class="flex min-h-[360px] items-center justify-center">
<ElEmpty description="暂无聊天排行数据" />
<ElEmpty description="暂无智能体使用数据" />
</div>
</template>
<div v-else class="flex min-h-[360px] items-center justify-center">
@@ -496,6 +1054,66 @@ onBeforeUnmount(() => {
</div>
</AnalysisChartCard>
</section>
<section>
<AnalysisChartCard title="用户活跃榜">
<template v-if="chatAvailable">
<div v-if="userRanks.length > 0" class="space-y-3">
<div
v-for="(item, index) in userRanks"
:key="item.userId || item.label"
class="border-border/60 bg-card flex flex-col gap-4 rounded-2xl border px-4 py-4 md:flex-row md:items-center md:justify-between"
>
<div class="flex min-w-0 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) }}
</div>
</div>
</div>
<div class="grid grid-cols-2 gap-3 md:grid-cols-3">
<div class="bg-muted/30 rounded-2xl px-4 py-3 text-right">
<div class="text-foreground text-base font-semibold">
{{ formatCount(item.sessionTotal) }}
</div>
<div class="text-muted-foreground mt-1 text-xs">会话数</div>
</div>
<div class="bg-muted/30 rounded-2xl px-4 py-3 text-right">
<div class="text-foreground text-base font-semibold">
{{ formatCount(item.assistantTotal) }}
</div>
<div class="text-muted-foreground mt-1 text-xs">
使用智能体数
</div>
</div>
<div
class="bg-muted/30 col-span-2 rounded-2xl px-4 py-3 text-right md:col-span-1"
>
<div class="text-foreground text-base font-semibold">
{{ formatCount(item.messageTotal) }}
</div>
<div class="text-muted-foreground mt-1 text-xs">消息数</div>
</div>
</div>
</div>
</div>
<div v-else class="flex min-h-[220px] items-center justify-center">
<ElEmpty description="暂无用户活跃数据" />
</div>
</template>
<div v-else class="flex min-h-[220px] items-center justify-center">
<ElEmpty :description="chatStatusMessage" />
</div>
</AnalysisChartCard>
</section>
</template>
</div>
</template>