From 60164ea59da7178473fba7d0c09150950b4587a7 Mon Sep 17 00:00:00 2001 From: wangdan-fit2cloud Date: Tue, 1 Sep 2026 12:24:26 +0800 Subject: [PATCH] feat: create application --- ui/AGENTS.md | 1 + .../workspace/application/application.ts | 6 + ui/src/api/types/application.ts | 2 +- ui/src/components.d.ts | 2 + ui/src/components/COMPONENT_README.md | 17 +- .../global/mk-icon/ApplicationIcon.vue | 13 + .../global/mk-infinite-scroll/index.vue | 24 +- ui/src/components/mk-source-card/index.vue | 11 +- ui/src/router/admin/system/index.ts | 2 +- .../admin/workspace/modules/application.ts | 2 +- ui/src/styles/element-plus.scss | 4 + .../WorkspaceApplicationDetail.vue | 5 +- ui/src/views/application/ApplicationView.vue | 6 +- .../application-card/ApplicationCard.vue | 5 +- .../AdvancedCreateDialog.vue | 120 +++++--- .../CreateApplicationDropdown.vue | 24 +- .../create-application/SimpleCreateDialog.vue | 82 +++++- .../create-application/template.ts | 260 ++++++++++++++++++ ui/src/views/knowledge/KnowledgeView.vue | 6 +- .../{application => knowledge}/template.ts | 0 .../components/PermissionTable.vue | 16 +- .../theme/components/LoginPreview.vue | 1 - ui/src/views/tool/ToolView.vue | 6 +- .../workflow/ApplicationWorkflowView.vue | 7 +- .../icons/ai-chat-node-icon.vue | 2 +- 25 files changed, 521 insertions(+), 103 deletions(-) create mode 100644 ui/src/components/global/mk-icon/ApplicationIcon.vue create mode 100644 ui/src/views/application/create-application/template.ts rename ui/src/views/{application => knowledge}/template.ts (100%) diff --git a/ui/AGENTS.md b/ui/AGENTS.md index f01b466ffff..527dbbe4433 100644 --- a/ui/AGENTS.md +++ b/ui/AGENTS.md @@ -190,6 +190,7 @@ Keep these behaviors: - Prefer existing project structure and naming. - Keep UI code typed and data-driven. +- Use Lodash `cloneDeep` for deep cloning; do not use `structuredClone`. - Explicitly import APIs used from Vue, Vue Router, and Pinia; do not restore API auto import. - Name business variables after their domain and purpose. Avoid context-free collection names such as `items` or `list` when a name such as `systemMenuItems` makes the contents clear. diff --git a/ui/src/api/admin/workspace/application/application.ts b/ui/src/api/admin/workspace/application/application.ts index bcc80f24e84..402a3c868db 100644 --- a/ui/src/api/admin/workspace/application/application.ts +++ b/ui/src/api/admin/workspace/application/application.ts @@ -35,6 +35,11 @@ const postApplicationImport = (file: File, folderId: string) => { return post(`${getPrefix()}/folder/${folderId}/import`, payload) } +/** 创建工作空间智能体。 */ +const postApplication = (data: ApplicationFormPayload) => { + return post(getPrefix(), data) +} + /** 保存工作空间智能体配置。 */ const putApplication = (applicationId: string, data: ApplicationFormPayload) => { return put(`${getPrefix()}/${applicationId}`, data) @@ -65,6 +70,7 @@ export default { getApplicationDetail, deleteApplication, exportApplication, + postApplication, postApplicationImport, putApplication, putMoveApplication, diff --git a/ui/src/api/types/application.ts b/ui/src/api/types/application.ts index fde507aac34..02a0f9e5bd3 100644 --- a/ui/src/api/types/application.ts +++ b/ui/src/api/types/application.ts @@ -38,7 +38,7 @@ export interface ApplicationFormPayload { problem_optimization_prompt?: string icon?: string type?: ApplicationType - work_flow?: LogicFlow.GraphData + work_flow?: LogicFlow.GraphConfigData model_params_setting?: Record tts_model_params_setting?: Record stt_model_params_setting?: Record diff --git a/ui/src/components.d.ts b/ui/src/components.d.ts index 6ee62ee86e5..5890da4b476 100644 --- a/ui/src/components.d.ts +++ b/ui/src/components.d.ts @@ -12,6 +12,7 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + ApplicationIcon: typeof import('./components/global/mk-icon/ApplicationIcon.vue')['default'] KnowledgeIcon: typeof import('./components/global/mk-icon/KnowledgeIcon.vue')['default'] LayoutAside: typeof import('./components/global/mk-view-layout/layout-aside.vue')['default'] LayoutBatchFooter: typeof import('./components/global/mk-view-layout/layout-batch-footer.vue')['default'] @@ -42,6 +43,7 @@ declare module 'vue' { // For TSX support declare global { + const ApplicationIcon: typeof import('./components/global/mk-icon/ApplicationIcon.vue')['default'] const KnowledgeIcon: typeof import('./components/global/mk-icon/KnowledgeIcon.vue')['default'] const LayoutAside: typeof import('./components/global/mk-view-layout/layout-aside.vue')['default'] const LayoutBatchFooter: typeof import('./components/global/mk-view-layout/layout-batch-footer.vue')['default'] diff --git a/ui/src/components/COMPONENT_README.md b/ui/src/components/COMPONENT_README.md index eb80c597cd4..0b7708bfb72 100644 --- a/ui/src/components/COMPONENT_README.md +++ b/ui/src/components/COMPONENT_README.md @@ -64,6 +64,9 @@ src/components/ │ ├── mk-filterable-dropdown/ │ │ └── index.vue # 带搜索过滤和滚动列表的下拉选择 │ ├── mk-icon/ +│ │ ├── ApplicationIcon.vue # 智能体头像与默认图标 +│ │ ├── KnowledgeIcon.vue # 知识库类型与自定义图标 +│ │ ├── ToolIcon.vue # 工具类型与自定义图标 │ │ └── index.vue # SVG Symbol 与 Element Plus 图标统一入口 │ ├── mk-infinite-scroll/ │ │ └── index.vue # 分页列表滚动触底加载与结束状态 @@ -447,18 +450,30 @@ Prop,未传入时读取当前路由的 `meta.title`;显式传入 `title=""` ``` +智能体、知识库和工具的资源图标分别使用自动注册的 `ApplicationIcon`、`KnowledgeIcon` 和 +`ToolIcon`。`ApplicationIcon` 在未传入 `icon` 时会回退到系统默认图标。 + +```vue + +``` + ### MkInfiniteScroll 分页列表的滚动触底加载组件,使用 `IntersectionObserver` 监听组件所在滚动区域的底部,不依赖 Element Plus 的 `v-infinite-scroll`。组件通过 `v-model` 管理已经加载的列表数据,通过 `load` 传入按页请求方法,并在内部管理页码、首次加载、数据追加、加载状态、结束状态和过期请求。请求 方法接收 `{ currentPage, pageSize }`,返回包含 `records`、`current`、`size` 和 `total` 的分页结果。 -默认每页加载 20 条,可通过 `pageSize` 修改。组件挂载后自动加载第一页,之后只在底部哨兵随 +默认每页加载 30 条,可通过 `pageSize` 修改。组件挂载后自动加载第一页,之后只在底部哨兵随 用户滚动进入可视区域时加载下一页。查询条件变化时通过组件暴露的 `reset()` 重新加载。 +首次加载或 `reset()` 请求第一页时,组件只显示加载状态;第一页完成且列表为空后才渲染 `empty` +插槽,避免请求过程中短暂显示空状态。已有数据时继续渲染默认插槽,并在触底请求期间保留列表。 ```vue + ``` diff --git a/ui/src/components/global/mk-icon/ApplicationIcon.vue b/ui/src/components/global/mk-icon/ApplicationIcon.vue new file mode 100644 index 00000000000..e1a1c2f0d03 --- /dev/null +++ b/ui/src/components/global/mk-icon/ApplicationIcon.vue @@ -0,0 +1,13 @@ + + + diff --git a/ui/src/components/global/mk-infinite-scroll/index.vue b/ui/src/components/global/mk-infinite-scroll/index.vue index 18dc0955117..fce14df5df5 100644 --- a/ui/src/components/global/mk-infinite-scroll/index.vue +++ b/ui/src/components/global/mk-infinite-scroll/index.vue @@ -10,17 +10,22 @@ interface InfiniteScrollPagination { } const dataList = defineModel({ required: true }) -const props = withDefaults(defineProps<{ load: (pagination: InfiniteScrollPagination) => Promise>; pageSize?: number }>(), { pageSize: 30 }) +const props = withDefaults(defineProps<{ load: (pagination: InfiniteScrollPagination) => Promise>; pageSize?: number }>(), { + pageSize: 30, +}) -defineSlots<{ default?: () => unknown }>() +defineSlots<{ default?: () => unknown; empty?: () => unknown }>() const triggerRef = ref() const loading = ref(false) +const firstPageLoaded = ref(false) // 是否到可见区域。 const isTriggerVisible = ref(false) const pagination = ref({ currentPage: 0, pageSize: props.pageSize, total: 0 }) -const finished = computed(() => pagination.value.currentPage > 0 && pagination.value.currentPage * pagination.value.pageSize >= pagination.value.total) +const finished = computed( + () => pagination.value.currentPage > 0 && pagination.value.currentPage * pagination.value.pageSize >= pagination.value.total, +) const showFinishedText = computed(() => finished.value && pagination.value.total > pagination.value.pageSize) // 查询条件快速变化时可能并发 reset,只有最新版本的请求可以更新列表。 let requestVersion = 0 @@ -42,7 +47,10 @@ function loadPage(currentPage: number) { .catch(() => {}) .finally(() => { // 旧请求不能关闭新请求正在显示的 loading。 - if (currentRequestVersion === requestVersion) loading.value = false + if (currentRequestVersion === requestVersion) { + if (currentPage === 1) firstPageLoaded.value = true + loading.value = false + } }) } @@ -54,6 +62,7 @@ function tryLoadNextPage() { function reset() { // 搜索或筛选条件变化后清空旧数据,并从第一页重新查询。 + firstPageLoaded.value = false dataList.value = [] pagination.value = { currentPage: 0, pageSize: props.pageSize, total: 0 } return loadPage(1) @@ -81,10 +90,11 @@ defineExpose({ reset })