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
1 change: 1 addition & 0 deletions ui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 6 additions & 0 deletions ui/src/api/admin/workspace/application/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ const postApplicationImport = (file: File, folderId: string) => {
return post<FormData, ApplicationDetail>(`${getPrefix()}/folder/${folderId}/import`, payload)
}

/** 创建工作空间智能体。 */
const postApplication = (data: ApplicationFormPayload) => {
return post<ApplicationFormPayload, ApplicationDetail>(getPrefix(), data)
}

/** 保存工作空间智能体配置。 */
const putApplication = (applicationId: string, data: ApplicationFormPayload) => {
return put<ApplicationFormPayload, ApplicationDetail>(`${getPrefix()}/${applicationId}`, data)
Expand Down Expand Up @@ -65,6 +70,7 @@ export default {
getApplicationDetail,
deleteApplication,
exportApplication,
postApplication,
postApplicationImport,
putApplication,
putMoveApplication,
Expand Down
2 changes: 1 addition & 1 deletion ui/src/api/types/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>
tts_model_params_setting?: Record<string, unknown>
stt_model_params_setting?: Record<string, unknown>
Expand Down
2 changes: 2 additions & 0 deletions ui/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -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']
Expand Down
17 changes: 16 additions & 1 deletion ui/src/components/COMPONENT_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 # 分页列表滚动触底加载与结束状态
Expand Down Expand Up @@ -447,18 +450,30 @@ Prop,未传入时读取当前路由的 `meta.title`;显式传入 `title=""`
<MkIcon :icon="Setting" :size="20" />
```

智能体、知识库和工具的资源图标分别使用自动注册的 `ApplicationIcon`、`KnowledgeIcon` 和
`ToolIcon`。`ApplicationIcon` 在未传入 `icon` 时会回退到系统默认图标。

```vue
<ApplicationIcon :icon="application.icon" :size="24" />
```

### MkInfiniteScroll

分页列表的滚动触底加载组件,使用 `IntersectionObserver` 监听组件所在滚动区域的底部,不依赖
Element Plus 的 `v-infinite-scroll`。组件通过 `v-model` 管理已经加载的列表数据,通过 `load`
传入按页请求方法,并在内部管理页码、首次加载、数据追加、加载状态、结束状态和过期请求。请求
方法接收 `{ currentPage, pageSize }`,返回包含 `records`、`current`、`size` 和 `total` 的分页结果。
默认每页加载 20 条,可通过 `pageSize` 修改。组件挂载后自动加载第一页,之后只在底部哨兵随
默认每页加载 30 条,可通过 `pageSize` 修改。组件挂载后自动加载第一页,之后只在底部哨兵随
用户滚动进入可视区域时加载下一页。查询条件变化时通过组件暴露的 `reset()` 重新加载。
首次加载或 `reset()` 请求第一页时,组件只显示加载状态;第一页完成且列表为空后才渲染 `empty`
插槽,避免请求过程中短暂显示空状态。已有数据时继续渲染默认插槽,并在触底请求期间保留列表。

```vue
<MkInfiniteScroll ref="infiniteScrollRef" v-model="resources" :load="loadResourcePage">
<ResourceCard v-for="resource in resources" :key="resource.id" :resource="resource" />
<template #empty>
<MkEmpty />
</template>
</MkInfiniteScroll>
```

Expand Down
13 changes: 13 additions & 0 deletions ui/src/components/global/mk-icon/ApplicationIcon.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script setup lang="ts">
import { resetUrl } from '@/utils/icon'

defineOptions({ name: 'ApplicationIcon' })

withDefaults(defineProps<{ icon?: string; size?: number | string }>(), { size: 24 })
</script>

<template>
<el-avatar class="bg-transparent!" shape="square" :size="size">
<img :src="resetUrl(icon, true)" alt="" />
</el-avatar>
</template>
24 changes: 17 additions & 7 deletions ui/src/components/global/mk-infinite-scroll/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,22 @@ interface InfiniteScrollPagination {
}

const dataList = defineModel<T[]>({ required: true })
const props = withDefaults(defineProps<{ load: (pagination: InfiniteScrollPagination) => Promise<ResponsePage<T>>; pageSize?: number }>(), { pageSize: 30 })
const props = withDefaults(defineProps<{ load: (pagination: InfiniteScrollPagination) => Promise<ResponsePage<T>>; pageSize?: number }>(), {
pageSize: 30,
})

defineSlots<{ default?: () => unknown }>()
defineSlots<{ default?: () => unknown; empty?: () => unknown }>()

const triggerRef = ref<HTMLElement>()
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
Expand All @@ -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
}
})
}

Expand All @@ -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)
Expand Down Expand Up @@ -81,10 +90,11 @@ defineExpose({ reset })
</script>

<template>
<div v-loading="loading">
<slot />
<div v-loading="loading && !dataList.length" class="min-h-full">
<slot v-if="dataList.length" />
<slot v-else-if="firstPageLoaded" name="empty" />
<div ref="triggerRef" aria-live="polite" class="flex-center py-4 shrink-0 text-sm text-N600">
<span v-if="loading">加载中...</span>
<span v-if="loading && dataList.length">加载中...</span>
<span v-else-if="showFinishedText">没有更多了</span>
</div>
</div>
Expand Down
11 changes: 8 additions & 3 deletions ui/src/components/mk-source-card/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function handleSelectedChange(selected: boolean | string | number) {

<template>
<el-card
:class="{ 'cursor-pointer': props.selectable, 'border-primary! bg-primary/10!': props.selectable && props.selected }"
:class="{ 'cursor-pointer': props.selectable, active: props.selectable && props.selected }"
:role="props.selectable ? 'checkbox' : undefined"
:tabindex="props.selectable ? 0 : undefined"
shadow="hover"
Expand All @@ -77,7 +77,7 @@ function handleSelectedChange(selected: boolean | string | number) {

<header class="flex-between items-start gap-2" :class="{ 'pr-7': props.selectable }">
<div class="flex min-w-0 flex-1 items-center gap-3">
<div v-if="slots.icon" class="shrink-0">
<div v-if="slots.icon" class="flex shrink-0">
<slot name="icon" />
</div>
<div class="min-w-0 flex-1">
Expand All @@ -86,7 +86,7 @@ function handleSelectedChange(selected: boolean | string | number) {
<h6 class="min-w-0 truncate" :title="props.title">{{ props.title }}</h6>
</slot>
</div>
<div class="text-sm text-N600">
<div class="text-sm text-N600" v-if="slots.subtitle || create_time || nick_name">
<slot name="subtitle">
<div class="flex gap-1">
<span>{{ props.nick_name }}</span>
Expand Down Expand Up @@ -114,5 +114,10 @@ function handleSelectedChange(selected: boolean | string | number) {
.mk-source-card {
min-width: 250px;
min-height: 172px;

&.active {
background-color: rgb(var(--mk-primary-rgb) / 10%) !important;
border-color: var(--mk-primary) !important;
}
}
</style>
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 @@ -13,7 +13,7 @@ export const systemRoutes: RouteRecordRaw = {
path: 'home',
name: 'system-home',
component: () => import('@/views/system/SystemView.vue'),
meta: { title: '首页', activeIcon: 'icon_screen_outlined', icon: 'icon_screen_outlined', order: 0 },
meta: { title: '首页', activeIcon: 'icon_screen_outlined', icon: 'icon_screen_filled', order: 0 },
},
{
path: 'identity',
Expand Down
2 changes: 1 addition & 1 deletion ui/src/router/admin/workspace/modules/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const applicationRoutes: RouteRecordRaw[] = [
path: 'overview',
name: 'workspace-application-detail',
component: () => import('@/views/application-detail/overview/OverviewView.vue'),
meta: { title: '概览', icon: 'icon_screen_outlined', activeIcon: 'icon_screen_outlined', order: 10 },
meta: { title: '概览', icon: 'icon_screen_outlined', activeIcon: 'icon_screen_filled', order: 10 },
},
{
path: 'setting',
Expand Down
4 changes: 4 additions & 0 deletions ui/src/styles/element-plus.scss
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@
// icon使用蓝色是固定的, 不能随主题变化
--el-avatar-bg-color: #3370ff;
flex-shrink: 0;
// ai的icon是固定的, 不能随主题变化
&.ai-avatar-gradient {
background: linear-gradient(180deg, #3370ff 0%, #9258f7 100%) !important;
}
}

/* button */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { useRoute, useRouter } from 'vue-router'
import ApplicationApi from '@/api/admin/workspace/application/application'
import type { ApplicationDetail } from '@/api/types'
import ResourceDetailLayout from '@/layout/ResourceDetailLayout.vue'
import { resetUrl } from '@/utils/icon'
import { applicationDetailContextKey } from './context'

defineOptions({ name: 'WorkspaceApplicationDetail' })
Expand Down Expand Up @@ -63,9 +62,7 @@ watch(
<template>
<ResourceDetailLayout :loading="loading" @back="navigateToApplicationList">
<template #resource-header>
<el-avatar class="shrink-0 bg-transparent!" shape="square" :size="24">
<img :src="resetUrl(application?.icon, true)" />
</el-avatar>
<ApplicationIcon :icon="application?.icon" />
<h6 class="min-w-0 truncate" :title="application?.name">{{ application?.name }}</h6>
</template>
</ResourceDetailLayout>
Expand Down
6 changes: 4 additions & 2 deletions ui/src/views/application/ApplicationView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ function handleBatchDelete() {

<div v-loading="applicationOperationLoading" class="min-h-0 flex-1">
<MkInfiniteScroll ref="infiniteScrollRef" v-model="applicationData" :load="loadApplicationPage">
<div v-if="applicationData.length" class="mk-resource-card-grid">
<div class="mk-resource-card-grid">
<template v-for="application in applicationData" :key="application.id">
<ApplicationCard
:application="application"
Expand Down Expand Up @@ -249,7 +249,9 @@ function handleBatchDelete() {
</ApplicationCard>
</template>
</div>
<MkEmpty v-else class="mt-24" />
<template #empty>
<MkEmpty class="mt-24" />
</template>
</MkInfiniteScroll>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script setup lang="ts">
import type { ApplicationDetail } from '@/api/types'
import MkSourceCard from '@/components/mk-source-card/index.vue'
import { resetUrl } from '@/utils/icon'
import { isWorkFlow } from '@/utils/application'
import { dateFormat } from '@/utils/time'

Expand Down Expand Up @@ -30,9 +29,7 @@ function handleOpen() {
@selected="emit('selected', $event)"
>
<template #icon>
<el-avatar class="bg-transparent!" shape="square" :size="24">
<img :src="resetUrl(application.icon, true)" />
</el-avatar>
<ApplicationIcon :icon="application.icon" />
</template>

<template #tag>
Expand Down
Loading
Loading