Skip to content

Commit 2ea903b

Browse files
committed
feat: system homepage
1 parent bdc4db3 commit 2ea903b

5 files changed

Lines changed: 211 additions & 10 deletions

File tree

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { get, getExportFile } from '../core/request'
2+
import type { ParamsPage, ResponsePage } from '../core/types'
3+
import type {
4+
Dict,
5+
HomeApplicationAggregation,
6+
HomeKnowledgeAggregation,
7+
HomeToolAggregation,
8+
HomeModelAggregation,
9+
HomeDateRange,
10+
HomeMonitoringDay,
11+
HomeRankingKind,
12+
HomeRankingRecord,
13+
} from '@/api/types'
14+
15+
const prefix = `/system/homepage`
16+
17+
/** 合并系统首页的工作空间筛选条件。 */
18+
const withWorkspaceId = (workspaceId: string, query: Dict<unknown> = {}) => ({
19+
...query,
20+
workspace_id: workspaceId,
21+
})
22+
/** 获取智能体数量与发布状态。 */
23+
const getApplicationAggregation = (workspaceId: string) =>
24+
get<HomeApplicationAggregation>(`${prefix}/application/aggregation`, withWorkspaceId(workspaceId))
25+
/** 获取知识库与文档数量。 */
26+
const getKnowledgeAggregation = (workspaceId: string) =>
27+
get<HomeKnowledgeAggregation>(`${prefix}/knowledge/aggregation`, withWorkspaceId(workspaceId))
28+
/** 获取工具数量与类型分布。 */
29+
const getToolAggregation = (workspaceId: string) => get<HomeToolAggregation>(`${prefix}/tool/aggregation`, withWorkspaceId(workspaceId))
30+
/** 获取模型数量与类型分布。 */
31+
const getModelAggregation = (workspaceId: string) => get<HomeModelAggregation>(`${prefix}/model/aggregation`, withWorkspaceId(workspaceId))
32+
/** 获取指定日期及智能体范围内的每日使用趋势。 */
33+
const getMonitoring = (workspaceId: string, range: HomeDateRange, applicationId?: string) =>
34+
get<HomeMonitoringDay[]>(
35+
`${prefix}/monitoring/aggregation`,
36+
withWorkspaceId(workspaceId, {
37+
...range,
38+
...(applicationId ? { application_id: applicationId } : {}),
39+
}),
40+
)
41+
/** 获取工作空间日期范围内的 Tokens 总量。 */
42+
const getTokensAggregation = (workspaceId: string, range: HomeDateRange) =>
43+
get<number>(`${prefix}/tokens/aggregation`, withWorkspaceId(workspaceId, { ...range }))
44+
/** 获取工作空间日期范围内的对话轮次。 */
45+
const getChatRecordAggregation = (workspaceId: string, range: HomeDateRange) =>
46+
get<number>(`${prefix}/chat_record/aggregation`, withWorkspaceId(workspaceId, { ...range }))
47+
48+
const rankingPaths = { tokens: 'tokens_ranking', questions: 'question_ranking', userTokens: 'user_tokens_ranking' }
49+
50+
/** 分页查询智能体或用户使用排行。 */
51+
const getRanking = (workspaceId: string, kind: HomeRankingKind, page: ParamsPage, range: HomeDateRange, name?: string) =>
52+
get<ResponsePage<HomeRankingRecord>>(
53+
`${prefix}/application/${rankingPaths[kind]}/${page.currentPage}/${page.pageSize}`,
54+
withWorkspaceId(workspaceId, { ...range, ...(name ? { name } : {}) }),
55+
)
56+
/** 按当前日期与名称筛选导出完整排行。 */
57+
const exportRanking = (workspaceId: string, kind: HomeRankingKind, range: HomeDateRange, name?: string) =>
58+
getExportFile(
59+
`${rankingPaths[kind]}.xlsx`,
60+
`${prefix}/${rankingPaths[kind]}/export`,
61+
withWorkspaceId(workspaceId, { ...range, ...(name ? { name } : {}) }),
62+
)
63+
64+
export default {
65+
getApplicationAggregation,
66+
getKnowledgeAggregation,
67+
getToolAggregation,
68+
getModelAggregation,
69+
getMonitoring,
70+
getTokensAggregation,
71+
getChatRecordAggregation,
72+
getRanking,
73+
exportRanking,
74+
}

‎ui/src/api/admin/workspace/application/application.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import type { ApplicationDetail, ApplicationFormPayload, Dict, PromptGeneratePay
44
import { getWorkspaceId } from '@/utils/resource-context'
55
import { ADMIN_API_BASE_PATH } from '@/api/constants'
66

7-
const getPrefix = () => {
8-
const workspaceId = getWorkspaceId()
7+
const getPrefix = (workspaceId = getWorkspaceId()) => {
98
return `/workspace/${workspaceId}/application`
109
}
1110

@@ -17,8 +16,8 @@ const getAllApplication = (query?: Dict<unknown>) => {
1716
}
1817

1918
/** 获取工作空间智能体列表。 */
20-
const getApplicationPage = (page: ParamsPage, query?: Dict<unknown>) => {
21-
return get<ResponsePage<ApplicationDetail>>(`${getPrefix()}/${page.currentPage}/${page.pageSize}`, query)
19+
const getApplicationPage = (page: ParamsPage, query?: Dict<unknown>, workspaceId?: string) => {
20+
return get<ResponsePage<ApplicationDetail>>(`${getPrefix(workspaceId)}/${page.currentPage}/${page.pageSize}`, query)
2221
}
2322

2423
/** 获取工作空间智能体详情。 */

‎ui/src/router/admin/system/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const systemRoutes: RouteRecordRaw = {
1212
{
1313
path: 'home',
1414
name: 'system-home',
15-
component: () => import('@/views/system/index.vue'),
15+
component: () => import('@/views/system/home/index.vue'),
1616
meta: {
1717
title: '首页',
1818
activeIcon: 'icon_screen_filled',

‎ui/src/views/home/components/ranking/HomeRankings.vue‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@ onMounted(loadRankings)
101101
@detail="handleOpenRanking('tokens')"
102102
>
103103
<template #description="{ record }">
104-
<span>对话 {{ numberFormat(record.chat_record_count) }} 次 </span>
104+
<span>对话 {{ numberFormat(record.chat_record_count) }} 次</span>
105105
<el-divider direction="vertical" />
106-
<span> 均 {{ formatTokenNumber(getRankingAverage('tokens', record)) }} Tokens </span>
106+
<span>均 {{ formatTokenNumber(getRankingAverage('tokens', record)) }} Tokens</span>
107107
</template>
108108
</RankingCard>
109109
<RankingCard
@@ -115,9 +115,9 @@ onMounted(loadRankings)
115115
@detail="handleOpenRanking('questions')"
116116
>
117117
<template #description="{ record }">
118-
<span>活跃用户 {{ numberFormat(record.chat_user_count) }} </span>
118+
<span>活跃用户 {{ numberFormat(record.chat_user_count) }}</span>
119119
<el-divider direction="vertical" />
120-
<span> 均 {{ numberFormat(getRankingAverage('questions', record)) }} 轮/人</span>
120+
<span>均 {{ numberFormat(getRankingAverage('questions', record)) }} 轮/人</span>
121121
</template>
122122
</RankingCard>
123123
<RankingCard
@@ -128,7 +128,7 @@ onMounted(loadRankings)
128128
:total="tokenTotal"
129129
@detail="handleOpenRanking('userTokens')"
130130
>
131-
<template #description="{ record }"> 对话 {{ numberFormat(record.chat_record_count) }} 次 </template>
131+
<template #description="{ record }">对话 {{ numberFormat(record.chat_record_count) }} 次</template>
132132
</RankingCard>
133133
</div>
134134
<RankingDrawer :api="api" v-if="selectedKind" :workspace-id="workspaceId" :initial-kind="selectedKind" :initial-range="range" />

‎ui/src/views/system/home/index.vue‎

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<script setup lang="ts">
2+
import { onMounted, ref, watch } from 'vue'
3+
import SystemHomepageApi from '@/api/admin/system/homepage'
4+
import WorkspaceApi from '@/api/admin/system/workspace'
5+
import ApplicationApi from '@/api/admin/workspace/application/application'
6+
import type { ApplicationDetail, WorkspaceItem } from '@/api/types'
7+
import WorkspaceDropdown from '@/components/business/workspace-dropdown/index.vue'
8+
import { useStore } from '@/stores'
9+
import HomeResourceOverview from '@/views/home/components/HomeResourceOverview.vue'
10+
import HomeStatistics from '@/views/home/components/HomeStatistics.vue'
11+
import HomeRankings from '@/views/home/components/ranking/HomeRankings.vue'
12+
13+
defineOptions({ name: 'SystemHomeView' })
14+
15+
const { auth } = useStore()
16+
17+
/* 工作空间筛选 */
18+
const selectedWorkspaceId = ref('default')
19+
const workspaceOptions = ref<WorkspaceItem[]>([])
20+
function loadWorkspaceOptions() {
21+
return WorkspaceApi.getSystemWorkspaceList().then((workspaces) => {
22+
workspaceOptions.value = workspaces
23+
if (!workspaces.some(({ id }) => id === selectedWorkspaceId.value)) {
24+
selectedWorkspaceId.value = workspaces[0]?.id ?? 'default'
25+
}
26+
})
27+
}
28+
function handleWorkspaceSelect(workspace: WorkspaceItem) {
29+
selectedWorkspaceId.value = workspace.id ?? 'default'
30+
}
31+
32+
/* 监控智能体筛选 */
33+
const applicationId = ref('all')
34+
const applications = ref<ApplicationDetail[]>([])
35+
const applicationsLoading = ref(false)
36+
const selectedApplication = ref<ApplicationDetail>()
37+
function loadApplications(query = '') {
38+
applicationsLoading.value = true
39+
return ApplicationApi.getApplicationPage({ currentPage: 1, pageSize: 200 }, { name: query }, selectedWorkspaceId.value)
40+
.then((result) => {
41+
applications.value = result.records
42+
})
43+
.finally(() => {
44+
applicationsLoading.value = false
45+
})
46+
}
47+
function handleApplicationChange(value: string) {
48+
selectedApplication.value = applications.value.find((application) => application.id === value)
49+
}
50+
51+
watch(
52+
selectedWorkspaceId,
53+
() => {
54+
applicationId.value = 'all'
55+
applications.value = []
56+
selectedApplication.value = undefined
57+
loadApplications()
58+
},
59+
{ immediate: true },
60+
)
61+
62+
onMounted(() => {
63+
if (auth.isEE) {
64+
loadWorkspaceOptions()
65+
}
66+
})
67+
</script>
68+
<template>
69+
<el-scrollbar class="h-full">
70+
<!-- 切换工作空间 -->
71+
<div class="px-6 py-4">
72+
<WorkspaceDropdown v-model="selectedWorkspaceId" :options="workspaceOptions" @select="handleWorkspaceSelect" />
73+
</div>
74+
<el-divider />
75+
<!-- 资源 -->
76+
<div class="px-6 py-4">
77+
<h4 class="mb-4 mt-4">资源</h4>
78+
<HomeResourceOverview :key="selectedWorkspaceId" :api="SystemHomepageApi" :workspace-id="selectedWorkspaceId" />
79+
<!-- 监控 -->
80+
<HomeStatistics
81+
:key="selectedWorkspaceId"
82+
class="mt-4"
83+
:workspace-id="selectedWorkspaceId"
84+
:application-id="applicationId"
85+
:api="SystemHomepageApi"
86+
>
87+
<template #application="{ loading }">
88+
<el-select
89+
v-model="applicationId"
90+
filterable
91+
remote
92+
fit-input-width
93+
class="w-55!"
94+
:remote-method="loadApplications"
95+
:loading="applicationsLoading"
96+
:disabled="loading"
97+
@change="handleApplicationChange"
98+
>
99+
<el-option label="全部智能体" value="all">
100+
<div class="flex-align-center gap-2">
101+
<MkIcon name="icon_card_outlined" :size="20" class="shrink-0 text-N600" />
102+
<span>全部智能体</span>
103+
</div>
104+
</el-option>
105+
<!-- TODO 全部智能体 -->
106+
<template v-for="application in applications" :key="application.id">
107+
<el-option :label="application.name" :value="application.id">
108+
<div class="flex-align-center gap-2">
109+
<ApplicationIcon :icon="application.icon" :size="20" class="shrink-0" />
110+
<span class="truncate" :title="application.name">{{ application.name }}</span>
111+
</div>
112+
</el-option>
113+
</template>
114+
<template #label="{ label, value }">
115+
<div class="flex-align-center gap-2">
116+
<MkIcon v-if="value === 'all'" name="icon_card_outlined" :size="18" class="shrink-0" />
117+
<ApplicationIcon v-else :icon="selectedApplication?.icon" :size="18" class="shrink-0" />
118+
<span class="truncate" :title="label">{{ label }}</span>
119+
</div>
120+
</template>
121+
</el-select>
122+
</template>
123+
</HomeStatistics>
124+
<!-- 排行榜 TOP5 -->
125+
<HomeRankings :key="selectedWorkspaceId" class="mt-4" :workspace-id="selectedWorkspaceId" :api="SystemHomepageApi" />
126+
</div>
127+
</el-scrollbar>
128+
</template>

0 commit comments

Comments
 (0)