Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions ui/src/api/admin/system/homepage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { get, getExportFile } from '../core/request'
import type { ParamsPage, ResponsePage } from '../core/types'
import type {
Dict,
HomeApplicationAggregation,
HomeKnowledgeAggregation,
HomeToolAggregation,
HomeModelAggregation,
HomeDateRange,
HomeMonitoringDay,
HomeRankingKind,
HomeRankingRecord,
} from '@/api/types'

const prefix = `/system/homepage`

/** 合并系统首页的工作空间筛选条件。 */
const withWorkspaceId = (workspaceId: string, query: Dict<unknown> = {}) => ({
...query,
workspace_id: workspaceId,
})
/** 获取智能体数量与发布状态。 */
const getApplicationAggregation = (workspaceId: string) =>
get<HomeApplicationAggregation>(`${prefix}/application/aggregation`, withWorkspaceId(workspaceId))
/** 获取知识库与文档数量。 */
const getKnowledgeAggregation = (workspaceId: string) =>
get<HomeKnowledgeAggregation>(`${prefix}/knowledge/aggregation`, withWorkspaceId(workspaceId))
/** 获取工具数量与类型分布。 */
const getToolAggregation = (workspaceId: string) => get<HomeToolAggregation>(`${prefix}/tool/aggregation`, withWorkspaceId(workspaceId))
/** 获取模型数量与类型分布。 */
const getModelAggregation = (workspaceId: string) => get<HomeModelAggregation>(`${prefix}/model/aggregation`, withWorkspaceId(workspaceId))
/** 获取指定日期及智能体范围内的每日使用趋势。 */
const getMonitoring = (workspaceId: string, range: HomeDateRange, applicationId?: string) =>
get<HomeMonitoringDay[]>(
`${prefix}/monitoring/aggregation`,
withWorkspaceId(workspaceId, {
...range,
...(applicationId ? { application_id: applicationId } : {}),
}),
)
/** 获取工作空间日期范围内的 Tokens 总量。 */
const getTokensAggregation = (workspaceId: string, range: HomeDateRange) =>
get<number>(`${prefix}/tokens/aggregation`, withWorkspaceId(workspaceId, { ...range }))
/** 获取工作空间日期范围内的对话轮次。 */
const getChatRecordAggregation = (workspaceId: string, range: HomeDateRange) =>
get<number>(`${prefix}/chat_record/aggregation`, withWorkspaceId(workspaceId, { ...range }))

const rankingPaths = { tokens: 'tokens_ranking', questions: 'question_ranking', userTokens: 'user_tokens_ranking' }

/** 分页查询智能体或用户使用排行。 */
const getRanking = (workspaceId: string, kind: HomeRankingKind, page: ParamsPage, range: HomeDateRange, name?: string) =>
get<ResponsePage<HomeRankingRecord>>(
`${prefix}/application/${rankingPaths[kind]}/${page.currentPage}/${page.pageSize}`,
withWorkspaceId(workspaceId, { ...range, ...(name ? { name } : {}) }),
)
/** 按当前日期与名称筛选导出完整排行。 */
const exportRanking = (workspaceId: string, kind: HomeRankingKind, range: HomeDateRange, name?: string) =>
getExportFile(
`${rankingPaths[kind]}.xlsx`,
`${prefix}/${rankingPaths[kind]}/export`,
withWorkspaceId(workspaceId, { ...range, ...(name ? { name } : {}) }),
)

export default {
getApplicationAggregation,
getKnowledgeAggregation,
getToolAggregation,
getModelAggregation,
getMonitoring,
getTokensAggregation,
getChatRecordAggregation,
getRanking,
exportRanking,
}
7 changes: 3 additions & 4 deletions ui/src/api/admin/workspace/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import type { ApplicationDetail, ApplicationFormPayload, Dict, PromptGeneratePay
import { getWorkspaceId } from '@/utils/resource-context'
import { ADMIN_API_BASE_PATH } from '@/api/constants'

const getPrefix = () => {
const workspaceId = getWorkspaceId()
const getPrefix = (workspaceId = getWorkspaceId()) => {
return `/workspace/${workspaceId}/application`
}

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

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

/** 获取工作空间智能体详情。 */
Expand Down
2 changes: 1 addition & 1 deletion ui/src/router/admin/system/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const systemRoutes: RouteRecordRaw = {
{
path: 'home',
name: 'system-home',
component: () => import('@/views/system/index.vue'),
component: () => import('@/views/system/home/index.vue'),
meta: {
title: '首页',
activeIcon: 'icon_screen_filled',
Expand Down
10 changes: 5 additions & 5 deletions ui/src/views/home/components/ranking/HomeRankings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@ onMounted(loadRankings)
@detail="handleOpenRanking('tokens')"
>
<template #description="{ record }">
<span>对话 {{ numberFormat(record.chat_record_count) }} 次 </span>
<span>对话 {{ numberFormat(record.chat_record_count) }} 次</span>
<el-divider direction="vertical" />
<span> 均 {{ formatTokenNumber(getRankingAverage('tokens', record)) }} Tokens </span>
<span>均 {{ formatTokenNumber(getRankingAverage('tokens', record)) }} Tokens</span>
</template>
</RankingCard>
<RankingCard
Expand All @@ -115,9 +115,9 @@ onMounted(loadRankings)
@detail="handleOpenRanking('questions')"
>
<template #description="{ record }">
<span>活跃用户 {{ numberFormat(record.chat_user_count) }} </span>
<span>活跃用户 {{ numberFormat(record.chat_user_count) }}</span>
<el-divider direction="vertical" />
<span> 均 {{ numberFormat(getRankingAverage('questions', record)) }} 轮/人</span>
<span>均 {{ numberFormat(getRankingAverage('questions', record)) }} 轮/人</span>
</template>
</RankingCard>
<RankingCard
Expand All @@ -128,7 +128,7 @@ onMounted(loadRankings)
:total="tokenTotal"
@detail="handleOpenRanking('userTokens')"
>
<template #description="{ record }"> 对话 {{ numberFormat(record.chat_record_count) }} 次 </template>
<template #description="{ record }">对话 {{ numberFormat(record.chat_record_count) }} 次</template>
</RankingCard>
</div>
<RankingDrawer :api="api" v-if="selectedKind" :workspace-id="workspaceId" :initial-kind="selectedKind" :initial-range="range" />
Expand Down
128 changes: 128 additions & 0 deletions ui/src/views/system/home/index.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue'
import SystemHomepageApi from '@/api/admin/system/homepage'
import WorkspaceApi from '@/api/admin/system/workspace'
import ApplicationApi from '@/api/admin/workspace/application/application'
import type { ApplicationDetail, WorkspaceItem } from '@/api/types'
import WorkspaceDropdown from '@/components/business/workspace-dropdown/index.vue'
import { useStore } from '@/stores'
import HomeResourceOverview from '@/views/home/components/HomeResourceOverview.vue'
import HomeStatistics from '@/views/home/components/HomeStatistics.vue'
import HomeRankings from '@/views/home/components/ranking/HomeRankings.vue'

defineOptions({ name: 'SystemHomeView' })

const { auth } = useStore()

/* 工作空间筛选 */
const selectedWorkspaceId = ref('default')
const workspaceOptions = ref<WorkspaceItem[]>([])
function loadWorkspaceOptions() {
return WorkspaceApi.getSystemWorkspaceList().then((workspaces) => {
workspaceOptions.value = workspaces
if (!workspaces.some(({ id }) => id === selectedWorkspaceId.value)) {
selectedWorkspaceId.value = workspaces[0]?.id ?? 'default'
}
})
}
function handleWorkspaceSelect(workspace: WorkspaceItem) {
selectedWorkspaceId.value = workspace.id ?? 'default'
}

/* 监控智能体筛选 */
const applicationId = ref('all')
const applications = ref<ApplicationDetail[]>([])
const applicationsLoading = ref(false)
const selectedApplication = ref<ApplicationDetail>()
function loadApplications(query = '') {
applicationsLoading.value = true
return ApplicationApi.getApplicationPage({ currentPage: 1, pageSize: 200 }, { name: query }, selectedWorkspaceId.value)
.then((result) => {
applications.value = result.records
})
.finally(() => {
applicationsLoading.value = false
})
}
function handleApplicationChange(value: string) {
selectedApplication.value = applications.value.find((application) => application.id === value)
}

watch(
selectedWorkspaceId,
() => {
applicationId.value = 'all'
applications.value = []
selectedApplication.value = undefined
loadApplications()
},
{ immediate: true },
)

onMounted(() => {
if (auth.isEE) {
loadWorkspaceOptions()
}
})
</script>
<template>
<el-scrollbar class="h-full">
<!-- 切换工作空间 -->
<div class="px-6 py-4">
<WorkspaceDropdown v-model="selectedWorkspaceId" :options="workspaceOptions" @select="handleWorkspaceSelect" />
</div>
<el-divider />
<!-- 资源 -->
<div class="px-6 py-4">
<h4 class="mb-4 mt-4">资源</h4>
<HomeResourceOverview :key="selectedWorkspaceId" :api="SystemHomepageApi" :workspace-id="selectedWorkspaceId" />
<!-- 监控 -->
<HomeStatistics
:key="selectedWorkspaceId"
class="mt-4"
:workspace-id="selectedWorkspaceId"
:application-id="applicationId"
:api="SystemHomepageApi"
>
<template #application="{ loading }">
<el-select
v-model="applicationId"
filterable
remote
fit-input-width
class="w-55!"
:remote-method="loadApplications"
:loading="applicationsLoading"
:disabled="loading"
@change="handleApplicationChange"
>
<el-option label="全部智能体" value="all">
<div class="flex-align-center gap-2">
<MkIcon name="icon_card_outlined" :size="20" class="shrink-0 text-N600" />
<span>全部智能体</span>
</div>
</el-option>
<!-- TODO 全部智能体 -->
<template v-for="application in applications" :key="application.id">
<el-option :label="application.name" :value="application.id">
<div class="flex-align-center gap-2">
<ApplicationIcon :icon="application.icon" :size="20" class="shrink-0" />
<span class="truncate" :title="application.name">{{ application.name }}</span>
</div>
</el-option>
</template>
<template #label="{ label, value }">
<div class="flex-align-center gap-2">
<MkIcon v-if="value === 'all'" name="icon_card_outlined" :size="18" class="shrink-0" />
<ApplicationIcon v-else :icon="selectedApplication?.icon" :size="18" class="shrink-0" />
<span class="truncate" :title="label">{{ label }}</span>
</div>
</template>
</el-select>
</template>
</HomeStatistics>
<!-- 排行榜 TOP5 -->
<HomeRankings :key="selectedWorkspaceId" class="mt-4" :workspace-id="selectedWorkspaceId" :api="SystemHomepageApi" />
</div>
</el-scrollbar>
</template>
Loading