From f223fcb737d466280c3ec19e30409be465c07cda Mon Sep 17 00:00:00 2001 From: wangdan-fit2cloud Date: Thu, 17 Sep 2026 20:17:18 +0800 Subject: [PATCH] feat: home --- ui/src/api/API_README.md | 10 ++ ui/src/api/admin/workspace/homepage.ts | 57 +++++++ ui/src/api/types/homepage.ts | 54 ++++++ ui/src/api/types/index.ts | 2 + ui/src/assets/iconfont.js | 2 +- ui/src/components/COMPONENT_README.md | 25 ++- ui/src/components/mk-date-range/index.vue | 19 ++- ui/src/components/mk-echart/LineCharts.vue | 73 +++++++++ ui/src/components/mk-echart/index.vue | 45 +++++ ui/src/components/mk-echart/types.ts | 10 ++ ui/src/styles/STYLE_README.md | 15 +- ui/src/styles/app.scss | 34 ++++ ui/src/styles/element-plus.scss | 10 +- ui/src/views/VIEW_README.md | 60 ++++++- .../components/ButtonCreateApplication.vue | 62 +++++-- ui/src/views/home/HomeView.vue | 93 ++++++++++- .../views/home/components/HomeQuickCreate.vue | 108 ++++++++++++ .../home/components/HomeResourceOverview.vue | 155 ++++++++++++++++++ .../views/home/components/HomeStatistics.vue | 124 ++++++++++++++ .../home/components/ranking/HomeRankings.vue | 136 +++++++++++++++ .../home/components/ranking/RankingCard.vue | 58 +++++++ .../home/components/ranking/RankingDrawer.vue | 142 ++++++++++++++++ ui/src/views/knowledge/KnowledgeView.vue | 11 +- .../ButtonCreateKnowledge.vue} | 67 ++++++-- .../operate-logs/OperateLogListView.vue | 2 +- .../tool/components/ButtonCreateTool.vue | 74 +++++++-- 26 files changed, 1377 insertions(+), 71 deletions(-) create mode 100644 ui/src/api/admin/workspace/homepage.ts create mode 100644 ui/src/api/types/homepage.ts create mode 100644 ui/src/components/mk-echart/LineCharts.vue create mode 100644 ui/src/components/mk-echart/index.vue create mode 100644 ui/src/components/mk-echart/types.ts create mode 100644 ui/src/views/home/components/HomeQuickCreate.vue create mode 100644 ui/src/views/home/components/HomeResourceOverview.vue create mode 100644 ui/src/views/home/components/HomeStatistics.vue create mode 100644 ui/src/views/home/components/ranking/HomeRankings.vue create mode 100644 ui/src/views/home/components/ranking/RankingCard.vue create mode 100644 ui/src/views/home/components/ranking/RankingDrawer.vue rename ui/src/views/knowledge/{create-knowledge/CreateKnowledgeDropdown.vue => components/ButtonCreateKnowledge.vue} (70%) diff --git a/ui/src/api/API_README.md b/ui/src/api/API_README.md index cdd18f4effe..b1f206d446c 100644 --- a/ui/src/api/API_README.md +++ b/ui/src/api/API_README.md @@ -332,3 +332,13 @@ API 对象和工作空间上下文,作为该抽屉的范围选择例外;用 `putKnowledgeWorkflow` 接受互斥的 `work_flow` 保存载荷或 `work_flow_template` 覆盖载荷,覆盖成功后由 View 重载详情。 `exportKnowledgeWorkflow(knowledgeId, name)` 通过 GET `//workflow/export` 下载 `.kbwf` 文件, 只导出工作流,不调用包含文档的知识库包导出接口。 + +### 工作空间首页 + +`admin/workspace/homepage.ts` 维护 `/workspace//homepage` 的四类资源汇总、 +每日趋势、三类排行分页、Tokens/对话总量及三类导出。每个接口均以必填的 `workspaceId` 为首个参数, +由调用方显式传入,API 文件不再自行读取当前路由。 +资源汇总、日期范围、趋势及排行记录类型在 `types/homepage.ts`,统一从 `@/api/types` 导入。 +`getRanking` 与 `exportRanking` 通过 `HomeRankingKind` 选择后端排行路径,名称及起止日期 +筛选保持一致;分页使用 `ParamsPage` 与 `ResponsePage`。导出沿用 `getExportFile`。 +工作空间总量接口返回数值,不与 System 首页的对象响应混用。 diff --git a/ui/src/api/admin/workspace/homepage.ts b/ui/src/api/admin/workspace/homepage.ts new file mode 100644 index 00000000000..c96f08db147 --- /dev/null +++ b/ui/src/api/admin/workspace/homepage.ts @@ -0,0 +1,57 @@ +import { get, getExportFile } from '../core/request' +import type { ParamsPage, ResponsePage } from '../core/types' +import type { + HomeApplicationAggregation, + HomeKnowledgeAggregation, + HomeToolAggregation, + HomeModelAggregation, + HomeDateRange, + HomeMonitoringDay, + HomeRankingKind, + HomeRankingRecord, +} from '@/api/types' + +const getPrefix = (workspaceId: string) => `/workspace/${workspaceId}/homepage` +/** 获取智能体数量与发布状态。 */ +const getApplicationAggregation = (workspaceId: string) => get(`${getPrefix(workspaceId)}/application/aggregation`) +/** 获取知识库与文档数量。 */ +const getKnowledgeAggregation = (workspaceId: string) => get(`${getPrefix(workspaceId)}/knowledge/aggregation`) +/** 获取工具数量与类型分布。 */ +const getToolAggregation = (workspaceId: string) => get(`${getPrefix(workspaceId)}/tool/aggregation`) +/** 获取模型数量与类型分布。 */ +const getModelAggregation = (workspaceId: string) => get(`${getPrefix(workspaceId)}/model/aggregation`) +/** 获取指定日期及智能体范围内的每日使用趋势。 */ +const getMonitoring = (workspaceId: string, range: HomeDateRange, applicationId?: string) => + get(`${getPrefix(workspaceId)}/monitoring/aggregation`, { + ...range, + ...(applicationId ? { application_id: applicationId } : {}), + }) +/** 获取工作空间日期范围内的 Tokens 总量。 */ +const getTokensAggregation = (workspaceId: string, range: HomeDateRange) => get(`${getPrefix(workspaceId)}/tokens/aggregation`, { ...range }) +/** 获取工作空间日期范围内的对话轮次。 */ +const getChatRecordAggregation = (workspaceId: string, range: HomeDateRange) => + get(`${getPrefix(workspaceId)}/chat_record/aggregation`, { ...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>(`${getPrefix(workspaceId)}/application/${rankingPaths[kind]}/${page.currentPage}/${page.pageSize}`, { + ...range, + ...(name ? { name } : {}), + }) +/** 按当前日期与名称筛选导出完整排行。 */ +const exportRanking = (workspaceId: string, kind: HomeRankingKind, range: HomeDateRange, name?: string) => + getExportFile(`${rankingPaths[kind]}.xlsx`, `${getPrefix(workspaceId)}/${rankingPaths[kind]}/export`, { ...range, ...(name ? { name } : {}) }) + +export default { + getApplicationAggregation, + getKnowledgeAggregation, + getToolAggregation, + getModelAggregation, + getMonitoring, + getTokensAggregation, + getChatRecordAggregation, + getRanking, + exportRanking, +} diff --git a/ui/src/api/types/homepage.ts b/ui/src/api/types/homepage.ts new file mode 100644 index 00000000000..7b1fcc006e6 --- /dev/null +++ b/ui/src/api/types/homepage.ts @@ -0,0 +1,54 @@ +/** 首页资源汇总,由后端按当前用户可见范围统计。 */ +export interface HomeApplicationAggregation { + total: number + publish_count: number + un_publish_count: number +} +export interface HomeKnowledgeAggregation { + total: number + document_count: number + failure_count: number +} +export interface HomeToolAggregation { + total: number + custom_count: number + workflow_count: number + skill_count: number + mcp_count: number + data_source_count: number +} +export interface HomeModelAggregation { + total: number + llm_count: number + embedding_count: number +} +export interface HomeDateRange { + start_time: string + end_time: string +} +export interface HomeMonitoringDay { + day: string + customer_num: number + customer_added_count: number + chat_record_count: number + tokens_num: number + star_num: number + trample_num: number +} +/** + * 首页排行类型: + * - tokens:按智能体的 Tokens 消耗排行,数值取 total_tokens。 + * - questions:按智能体的对话次数排行,数值取 chat_record_count。 + * - userTokens:按用户的 Tokens 消耗排行,数值取 total_tokens,名称取 asker.username。 + */ +export type HomeRankingKind = 'tokens' | 'questions' | 'userTokens' +export interface HomeRankingRecord { + id?: string + name?: string + chat_user_id?: string + chat_user_type?: string + asker?: { username?: string } | null + total_tokens?: number + chat_record_count: number + chat_user_count?: number +} diff --git a/ui/src/api/types/index.ts b/ui/src/api/types/index.ts index 78483776e16..4fe18a1fc35 100644 --- a/ui/src/api/types/index.ts +++ b/ui/src/api/types/index.ts @@ -20,3 +20,5 @@ export type * from './trigger' export type * from './related-resources' export type * from './workflow-version' export type * from './workflow-template' + +export type * from './homepage' diff --git a/ui/src/assets/iconfont.js b/ui/src/assets/iconfont.js index 9b523f301e0..3fc0dde7538 100644 --- a/ui/src/assets/iconfont.js +++ b/ui/src/assets/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_5200172='',(h=>{var a=(l=(l=document.getElementsByTagName("script"))[l.length-1]).getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var o,v,t,i,c,d=function(a,l){l.parentNode.insertBefore(a,l)};if(a&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}o=function(){var a,l=document.createElement("div");l.innerHTML=h._iconfont_svg_string_5200172,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(a=document.body).firstChild?d(l,a.firstChild):a.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(o,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),o()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(t=o,i=h.document,c=!1,m(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,e())})}function e(){c||(c=!0,t())}function m(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(m,50)}e()}})(window); \ No newline at end of file +window._iconfont_svg_string_5200172='',(h=>{var a=(l=(l=document.getElementsByTagName("script"))[l.length-1]).getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var o,v,t,i,c,d=function(a,l){l.parentNode.insertBefore(a,l)};if(a&&!h.__iconfont__svg__cssinject__){h.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}o=function(){var a,l=document.createElement("div");l.innerHTML=h._iconfont_svg_string_5200172,(l=l.getElementsByTagName("svg")[0])&&(l.setAttribute("aria-hidden","true"),l.style.position="absolute",l.style.width=0,l.style.height=0,l.style.overflow="hidden",l=l,(a=document.body).firstChild?d(l,a.firstChild):a.appendChild(l))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(o,0):(v=function(){document.removeEventListener("DOMContentLoaded",v,!1),o()},document.addEventListener("DOMContentLoaded",v,!1)):document.attachEvent&&(t=o,i=h.document,c=!1,m(),i.onreadystatechange=function(){"complete"==i.readyState&&(i.onreadystatechange=null,e())})}function e(){c||(c=!0,t())}function m(){try{i.documentElement.doScroll("left")}catch(a){return void setTimeout(m,50)}e()}})(window); \ No newline at end of file diff --git a/ui/src/components/COMPONENT_README.md b/ui/src/components/COMPONENT_README.md index 07dc91a7c9d..a3ae2b27a9f 100644 --- a/ui/src/components/COMPONENT_README.md +++ b/ui/src/components/COMPONENT_README.md @@ -53,6 +53,10 @@ import { MkDynamicsForm, MkDynamicsFormConstructor } from '@/components/mk-dynam - Props、Emits、Slots 保持类型化。类型归属遵循 [API_README.md](../api/API_README.md), API 与组件共用类型从 `@/api/types` 导入。 - 样式默认 `scoped`;组件专属样式留在组件目录,应用级规则放在 `src/styles`。 +- 今后新增 Card、`div` 等展示组件或布局元素的循环时,统一使用外层 + `