diff --git a/ui/src/assets/iconfont.js b/ui/src/assets/iconfont.js index 816a752493a..b18dcddc372 100644 --- a/ui/src/assets/iconfont.js +++ b/ui/src/assets/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_5200172='',(l=>{var a=(h=(h=document.getElementsByTagName("script"))[h.length-1]).getAttribute("data-injectcss"),h=h.getAttribute("data-disable-injectsvg");if(!h){var o,v,t,i,c,d=function(a,h){h.parentNode.insertBefore(a,h)};if(a&&!l.__iconfont__svg__cssinject__){l.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(a){console&&console.log(a)}}o=function(){var a,h=document.createElement("div");h.innerHTML=l._iconfont_svg_string_5200172,(h=h.getElementsByTagName("svg")[0])&&(h.setAttribute("aria-hidden","true"),h.style.position="absolute",h.style.width=0,h.style.height=0,h.style.overflow="hidden",h=h,(a=document.body).firstChild?d(h,a.firstChild):a.appendChild(h))},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=l.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 98ece809cfe..eb80c597cd4 100644 --- a/ui/src/components/COMPONENT_README.md +++ b/ui/src/components/COMPONENT_README.md @@ -941,9 +941,10 @@ const formValue = ref>({}) ### FolderTree Workspace 的文件夹虚拟树业务组件。组件根据当前资源上下文和 `source` 调用统一文件夹接口,负责 -文件夹树查询、搜索、排序、创建、编辑、移动和删除。通过 `v-model` 控制当前文件夹 ID,选择 -文件夹时触发 `select`。`showAll`、`showShared` 分别控制全部和共享入口,`rootLabel` 可覆盖根入口 -文案,`disabledFolderIds` 用于只选场景中禁用指定节点。 +文件夹树查询、搜索、排序、创建、编辑、移动和删除。通过 `v-model` 控制当前文件夹 ID,首次加载 +完成后通过 `loaded` 返回该 ID 对应的完整文件夹,选择文件夹时触发 `select`。传入的 ID 不存在时, +优先回退到“全部”入口,否则回退到首个可用文件夹。`showAll`、`showShared` 分别控制全部和共享入口, +`rootLabel` 可覆盖根入口文案,`disabledFolderIds` 用于只选场景中禁用指定节点。 ```vue - + ``` 页面顶部需要触发根目录创建时,通过页面语义处理方法调用组件暴露的 `openCreate()`;外部数据变化 diff --git a/ui/src/components/business/folder-tree/index.vue b/ui/src/components/business/folder-tree/index.vue index 6b8aa0bed84..87f93748f78 100644 --- a/ui/src/components/business/folder-tree/index.vue +++ b/ui/src/components/business/folder-tree/index.vue @@ -29,7 +29,7 @@ const props = withDefaults( const currentNodeKey = defineModel({ default: FOLDER_ENTRY_ID.ALL }) -const emit = defineEmits<{ select: [folder: FolderItem] }>() +const emit = defineEmits<{ loaded: [folder?: FolderItem]; select: [folder: FolderItem] }>() const folderEntries = computed(() => FOLDER_ENTRIES[props.source]) const rootFolderEntry = computed(() => ({ ...folderEntries.value.all, name: props.rootLabel || folderEntries.value.all.name })) @@ -250,6 +250,13 @@ function findFolderById(folders: FolderItem[], folderId: string): FolderItem | u } } +function resolveCurrentFolder() { + if (currentNodeKey.value === FOLDER_ENTRY_ID.ALL && props.showAll) return rootFolderEntry.value + if (currentNodeKey.value === FOLDER_ENTRY_ID.SHARED && props.showShared) return folderEntries.value.shared + + return findFolderById(folderTreeData.value, currentNodeKey.value) ?? (props.showAll ? rootFolderEntry.value : sortTreeData.value[0]) +} + function getFolderDeleteContext(folder: FolderItem) { const deletedFolder = findFolderById(folderTreeData.value, folder.id) ?? folder const positionCache = readCustomPositions() @@ -314,7 +321,11 @@ onMounted(() => { if (Object.values(FOLDER_SORT).includes(savedSort as FolderSort)) { currentSort.value = savedSort as FolderSort } - loadFolders() + void loadFolders().then(() => { + const currentFolder = resolveCurrentFolder() + if (currentFolder) currentNodeKey.value = currentFolder.id + emit('loaded', currentFolder) + }) }) defineExpose({ refresh: loadFolders, openCreate: handleOpenCreateFolder }) diff --git a/ui/src/components/business/workspace-dropdown/index.vue b/ui/src/components/business/workspace-dropdown/index.vue index 17a5c62304d..83d6ae5cb97 100644 --- a/ui/src/components/business/workspace-dropdown/index.vue +++ b/ui/src/components/business/workspace-dropdown/index.vue @@ -12,7 +12,7 @@ const emit = defineEmits<{ select: [option: WorkspaceItem] }>() - + {{ text }} diff --git a/ui/src/layout/LayoutHeader.vue b/ui/src/layout/LayoutHeader.vue index 1633cd41375..7e5919a7dd4 100644 --- a/ui/src/layout/LayoutHeader.vue +++ b/ui/src/layout/LayoutHeader.vue @@ -21,7 +21,7 @@ function switchMode() { - 系统管理 + 系统管理 diff --git a/ui/src/layout/ResourceDetailLayout.vue b/ui/src/layout/ResourceDetailLayout.vue new file mode 100644 index 00000000000..2ee2933dcab --- /dev/null +++ b/ui/src/layout/ResourceDetailLayout.vue @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + {{ detailMenuItem.label }} + + + + + + + + + + {{ route.meta.title }} + + + + + diff --git a/ui/src/router/ROUTE_README.md b/ui/src/router/ROUTE_README.md index 25ad39e67b8..99bfbb53658 100644 --- a/ui/src/router/ROUTE_README.md +++ b/ui/src/router/ROUTE_README.md @@ -80,8 +80,9 @@ Admin 使用末尾 catch-all 路由 `not-found` 渲染 `views/error/NotFoundView Admin Router 在 `admin/index.ts` 中统一处理导航初始化: -1. 导航开始时调用 `NProgress.start()`,导航完成或发生路由错误时调用 - `NProgress.done()`;统一关闭 Spinner,视觉样式由 `styles/nprogress.scss` 维护。 +1. 路由 path 变化时调用 `NProgress.start()`;同一路径只更新 query 或 hash 时不启动顶部进度条。 + 导航完成或发生路由错误时调用 `NProgress.done()`;统一关闭 Spinner,视觉样式由 + `styles/nprogress.scss` 维护。 2. `login`、`forgot-password`、`not-found` 为公开路由,不校验登录状态。 3. URL 查询参数包含 `token` 时,先写入 Admin 认证 Store,用于外部认证回跳,然后使用替换 导航清理地址栏中的 token。 @@ -100,17 +101,18 @@ Chat 使用独立入口 `src/chat.ts` 和独立 Router,不要把 Chat 路由 路由扩展字段定义在 `admin/types.ts`: -| 字段 | 类型 | 说明 | -| --------------- | ----------------------------------------------------- | ------------------------------------------------------ | -| `scope` | `'workspace' \| 'system'` | 标记生成哪一套框架导航,只配置在布局根路由上 | -| `resourceScope` | `'workspace' \| 'system-resource' \| 'system-shared'` | 标记页面使用的资源范围,由对应资源父路由配置并向下继承 | -| `activeIcon` | `string` | 菜单激活状态的 iconfont Symbol ID | -| `activeMenu` | `string` | 进入子页面时需要保持激活的侧栏菜单路径 | -| `title` | `string` | 页面标题,同时作为导航名称 | -| `icon` | `string` | iconfont Symbol ID,子目录通常可以不配置 | -| `order` | `number` | 同级导航排序,数字越小越靠前 | -| `hidden` | `boolean` | 设置为 `true` 时不显示在导航中 | -| `resource` | `ResourceAuthorizationType` | 系统资源授权页面当前管理的后端资源类型 | +| 字段 | 类型 | 说明 | +| ------------------ | ----------------------------------------------------- | ------------------------------------------------------ | +| `scope` | `'workspace' \| 'system'` | 标记生成哪一套框架导航,只配置在布局根路由上 | +| `resourceScope` | `'workspace' \| 'system-resource' \| 'system-shared'` | 标记页面使用的资源范围,由对应资源父路由配置并向下继承 | +| `activeIcon` | `string` | 菜单激活状态的 iconfont Symbol ID | +| `activeMenu` | `string` | 进入子页面时需要保持激活的侧栏菜单路径 | +| `detailActiveMenu` | `string` | 更深层详情页面需要保持激活的二级菜单路由名称 | +| `title` | `string` | 页面标题,同时作为导航名称 | +| `icon` | `string` | iconfont Symbol ID,子目录通常可以不配置 | +| `order` | `number` | 同级导航排序,数字越小越靠前 | +| `hidden` | `boolean` | 设置为 `true` 时不显示在导航中 | +| `resource` | `ResourceAuthorizationType` | 系统资源授权页面当前管理的后端资源类型 | ## 导航生成 @@ -124,6 +126,13 @@ Chat 使用独立入口 `src/chat.ts` 和独立 Router,不要把 Chat 路由 `WorkspaceSidebar` 和 `SystemSidebar` 分别调用该方法,不需要在页面组件中维护菜单数组。 +资源详情页面统一使用 `ResourceDetailLayout`。`admin/utils.ts` 中的 +`getMatchedChildRouteList(route)` 根据当前匹配路由找到所属详情父路由,并复用与 +`getChildRouteList(scope)` 相同的过滤、排序和菜单字段转换逻辑生成二级导航。详情子路由通过 +`title`、`icon`、`activeIcon` 和 `order` 配置二级菜单;更深层页面通过 +`detailActiveMenu` 指定需要保持激活的二级菜单路由名称,不配置 `parentPath` 或 `parentName`。 +`ResourceDetailLayout` 切换二级菜单时保留当前 query,详情容器返回列表时也应透传来源 query。 + 路由导航图标统一使用 `src/assets/iconfont.js` 中的完整 Symbol ID 字符串,并由 `MkIcon` 的 `name` 属性渲染。需要在菜单激活后切换图标时,通过 `activeIcon` 配置激活状态 的 Symbol ID;未配置时继续使用 `icon`。Router 目录禁止导入或传入 Element Plus 图标组件。 @@ -177,14 +186,19 @@ Workspace 页面: /admin/workspace/:workspaceId/model /admin/workspace/:workspaceId/application -/admin/workspace/:workspaceId/application/:applicationId -/admin/workspace/:workspaceId/application/:applicationId/edit +/admin/workspace/:workspaceId/application/:applicationId/:type/overview +/admin/workspace/:workspaceId/application/:applicationId/:type/setting /admin/workspace/:workspaceId/knowledge /admin/workspace/:workspaceId/knowledge/:knowledgeId /admin/workspace/:workspaceId/knowledge/:knowledgeId/document/:documentId ``` +智能体详情路由的 `type` 来自当前 `ApplicationDetail.type`;卡片概览和简易智能体设置等详情入口 +必须与 `applicationId` 一并传入该参数。高级智能体设置继续进入独立的 Workflow 路由。 +智能体列表使用可选的 `folderId` query 保存当前文件夹;进入详情时继续携带该 query,返回列表后 +由 `FolderTree` 恢复选中项。`folderId` 是列表来源状态,不作为详情 path 参数。 + System 复用页面(仅替换根前缀): ```text @@ -268,6 +282,10 @@ System 共享资源页面: } ``` +使用 `ResourceDetailLayout` 的详情路由只在详情父路由配置 `hidden: true`;需要显示在二级导航中的 +直接子路由继续配置 `title`、`icon` 和 `order`,不要再配置 `hidden: true`。一级导航生成时会排除 +整个详情父路由,详情布局则读取该父路由的直接子路由生成自己的二级导航。 + ## 新增路由约定 - 每条路由必须使用全局唯一的 `name`。 diff --git a/ui/src/router/admin/index.ts b/ui/src/router/admin/index.ts index 3903c6ed633..3ce08ade5e6 100644 --- a/ui/src/router/admin/index.ts +++ b/ui/src/router/admin/index.ts @@ -22,8 +22,8 @@ const router = createRouter({ ], }) -router.beforeEach(async (to) => { - NProgress.start() +router.beforeEach(async (to, from) => { + if (to.path !== from.path) NProgress.start() const notAuthRouteNameList = ['login', 'forgot-password', 'not-found'] const { auth, user } = useStore() diff --git a/ui/src/router/admin/system/modules/application.ts b/ui/src/router/admin/system/modules/application.ts index c9fa17762d5..68db60a1a53 100644 --- a/ui/src/router/admin/system/modules/application.ts +++ b/ui/src/router/admin/system/modules/application.ts @@ -2,16 +2,5 @@ import type { RouteRecordRaw } from 'vue-router' /** System 来源的智能体详情路由,与 Workspace 复用页面组件。 */ export const systemApplicationRoutes: RouteRecordRaw[] = [ - // { - // path: 'application/:applicationId', - // name: 'system-application-detail', - // component: () => import('@/views/application/ApplicationDetailView.vue'), - // meta: { title: '智能体详情', hidden: true }, - // }, - // { - // path: 'application/:applicationId/edit', - // name: 'system-application-edit', - // component: () => import('@/views/application/ApplicationDetailView.vue'), - // meta: { title: '编辑智能体', hidden: true }, - // }, + ] diff --git a/ui/src/router/admin/types.ts b/ui/src/router/admin/types.ts index 55f43eb9057..0849eabe2d9 100644 --- a/ui/src/router/admin/types.ts +++ b/ui/src/router/admin/types.ts @@ -14,6 +14,8 @@ declare module 'vue-router' { activeIcon?: string /** 侧栏需要保持激活的菜单路径 */ activeMenu?: string + /** 资源详情页需要保持激活的二级菜单路由名称。 */ + detailActiveMenu?: string /** iconfont Symbol ID */ icon?: string order?: number diff --git a/ui/src/router/admin/utils.ts b/ui/src/router/admin/utils.ts index 6ca712920ee..7e73b4d34ab 100644 --- a/ui/src/router/admin/utils.ts +++ b/ui/src/router/admin/utils.ts @@ -1,32 +1,52 @@ -import type { RouteRecordRaw } from 'vue-router' +import type { RouteLocationNormalizedLoaded, RouteMeta, RouteRecordName, RouteRecordRaw } from 'vue-router' import type { LayoutMenuItem } from '@/layout/types' import type { RouteScope } from './types' import { systemRoutes } from './system' import { workspaceRoutes } from './workspace' +interface MenuRouteRecord { + children?: readonly MenuRouteRecord[] + meta?: RouteMeta + name?: RouteRecordName | null +} + const scopedRoutes: RouteRecordRaw[] = [workspaceRoutes, systemRoutes] -/** 根据 Workspace 或 System 路由树递归生成侧栏目录。 */ +/** 将路由记录递归转换为 Layout 菜单。 */ +function createMenuItems(routes: readonly MenuRouteRecord[]): LayoutMenuItem[] { + return routes + .filter((route) => route.name && route.meta?.title && route.meta.hidden !== true) + .sort((a, b) => (a.meta?.order ?? Number.MAX_SAFE_INTEGER) - (b.meta?.order ?? Number.MAX_SAFE_INTEGER)) + .map((route) => { + const children = route.children ? createMenuItems(route.children) : [] + + return { + name: String(route.name), + label: route.meta!.title!, + activeIcon: route.meta?.activeIcon, + icon: route.meta?.icon, + route: { name: route.name! }, + children: children.length ? children : undefined, + } + }) +} + +/** 根据 Workspace 或 System 路由树生成一级侧栏目录。 */ export function getChildRouteList(scope: RouteScope): LayoutMenuItem[] { const rootRoute = scopedRoutes.find((route) => route.meta?.scope === scope) - const createMenuItems = (routes: readonly RouteRecordRaw[]): LayoutMenuItem[] => { - return routes - .filter((route) => route.name && route.meta?.title && route.meta.hidden !== true) - .sort((a, b) => (a.meta?.order ?? Number.MAX_SAFE_INTEGER) - (b.meta?.order ?? Number.MAX_SAFE_INTEGER)) - .map((route) => { - const children = route.children ? createMenuItems(route.children) : [] - - return { - name: String(route.name), - label: route.meta!.title!, - activeIcon: route.meta?.activeIcon, - icon: route.meta?.icon, - route: { name: route.name! }, - children: children.length ? children : undefined, - } - }) - } - return createMenuItems(rootRoute?.children ?? []) } + +/** 根据当前匹配路由生成所属详情父路由的二级侧栏目录。 */ +export function getMatchedChildRouteList(route: RouteLocationNormalizedLoaded): LayoutMenuItem[] { + const activeRouteName = route.meta.detailActiveMenu ?? route.name + + if (!activeRouteName) return [] + + const parentRoute = [...route.matched] + .reverse() + .find((matchedRoute) => matchedRoute.children.some((childRoute) => childRoute.name === activeRouteName)) + + return createMenuItems(parentRoute?.children ?? []) +} diff --git a/ui/src/router/admin/workspace/modules/application.ts b/ui/src/router/admin/workspace/modules/application.ts index 2f251fccb9e..1379d3b3879 100644 --- a/ui/src/router/admin/workspace/modules/application.ts +++ b/ui/src/router/admin/workspace/modules/application.ts @@ -7,19 +7,32 @@ export const applicationRoutes: RouteRecordRaw[] = [ redirect: { name: 'workspace-application-list' }, meta: { title: '智能体', activeMenu: 'workspace-application', icon: 'icon_robot_outlined', activeIcon: 'icon_robot_filled', order: 20 }, children: [ - { path: '', name: 'workspace-application-list', component: () => import('@/views/application/ApplicationView.vue'), meta: { title: '智能体列表', hidden: true } }, - // { - // path: ':applicationId', - // name: 'workspace-application-detail', - // component: () => import('@/views/application/ApplicationDetailView.vue'), - // meta: { title: '智能体详情', hidden: true }, - // }, - // { - // path: ':applicationId/edit', - // name: 'workspace-application-edit', - // component: () => import('@/views/application/ApplicationDetailView.vue'), - // meta: { title: '编辑智能体', hidden: true }, - // }, + { + path: '', + name: 'workspace-application-list', + component: () => import('@/views/application/ApplicationView.vue'), + meta: { title: '智能体列表', hidden: true }, + }, + { + path: ':applicationId/:type', + name: 'workspace-application-detail-layout', + component: () => import('@/views/application-detail/WorkspaceApplicationDetail.vue'), + meta: { title: '智能体详情', hidden: true }, + children: [ + { + 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 }, + }, + { + path: 'setting', + name: 'workspace-application-simple-setting', + component: () => import('@/views/application-detail/setting/SimpleSettingView.vue'), + meta: { title: '设置', icon: 'icon-setting', activeIcon: 'icon_setting_filled', order: 20 }, + }, + ], + }, ], }, ] diff --git a/ui/src/styles/app.scss b/ui/src/styles/app.scss index e79f963ec33..02162f07322 100644 --- a/ui/src/styles/app.scss +++ b/ui/src/styles/app.scss @@ -209,6 +209,14 @@ h6 { grid-template-columns: repeat(auto-fill, minmax(min(100%, 270px), 1fr)); } +// 资源菜单的导入按钮 +.mk-import-button { + width: 100%; + .el-upload { + width: 100%; + } +} + // radio-group 垂直排列 .vertical-radio-group { align-items: stretch; diff --git a/ui/src/views/VIEW_README.md b/ui/src/views/VIEW_README.md index f2e056a2768..478a433d71b 100644 --- a/ui/src/views/VIEW_README.md +++ b/ui/src/views/VIEW_README.md @@ -9,15 +9,15 @@ 代码放在同一目录或其子目录中,不要把多个无关页面平铺在上级目录。 - 路由级 Vue 组件统一使用 `PascalCase` 并以 `View.vue` 结尾,例如 `UserListView.vue`。 -- 页面拆分出的普通子组件统一放在当前功能目录的 `components/` 中,不与路由页面、Dialog 或 - Drawer 混放。 -- 页面使用的 Dialog 统一放在当前功能目录的 `dialog/` 中,文件使用 `PascalCase` 并以 - `Dialog.vue` 结尾,例如 `UserPwdDialog.vue`。 +- 页面拆分出的普通子组件统一放在当前功能目录的 `components/` 中,不与路由页面或 Drawer + 混放。属于同一业务流程的入口、Dialog 和配套组件可以使用职责明确的业务目录集中维护,例如 + `create-application/`;Dialog 文件使用 `PascalCase` 并以 `Dialog.vue` 结尾。 - 页面使用的 Drawer 放在当前功能目录中,文件使用 `PascalCase` 并以 `Drawer.vue` 结尾, - 例如 `AddMemberDrawer.vue`。Drawer 不放入 `components/` 或 `dialog/`。 -- 卡片的单文件 Action 直接放入 `/action-dropdown/`;Action 独占且由 Action 自主管理 - Drawer、Dialog 或其他配套文件时,一起放入 `/action-dropdown/-action/`。 - 页面通过卡片的 `action-dropdown` 插槽组合操作,不直接导入或调用这些浮层。 + 例如 `AddMemberDrawer.vue`。Drawer 不放入 `components/`。 +- `action-dropdown/` 目录目前只优先用于 `application`、`knowledge`、`model`、`tool` 四类特殊 + 资源,用于维护跨 Workspace、System 资源管理或 System 共享资源复用的卡片 Action。其他业务 + 暂不主动创建这类目录,操作入口和配套流程优先留在所属页面或组件中;出现明确的跨范围共享需求 + 后再评估是否采用相同结构。 - 仅供一个页面使用的代码放在该页面功能目录;同一功能下多个页面复用的代码放在它们最近的 共同功能目录中。 - 不要为了复用一个页面内部实现而提前移动到 `src/components`。 @@ -49,9 +49,8 @@ src/views/// ├── FeatureEditDrawer.vue # 页面抽屉,统一以 Drawer.vue 结尾 ├── components/ # 页面拆分出的普通子组件 │ └── FeatureSetting.vue -├── dialog/ # 页面弹窗,统一放在独立目录 -│ ├── CreateFeatureDialog.vue # 创建弹窗,统一以 Dialog.vue 结尾 -│ └── DeleteFeatureDialog.vue # 删除弹窗 +├── create-feature/ # 创建流程的入口、弹窗和配套组件 +│ └── CreateFeatureDialog.vue └── constants.ts # 仅供该功能使用的常量 ``` @@ -90,15 +89,31 @@ src/views/application/ │ ├── ExportApplicationAction.vue │ ├── MoveApplicationAction.vue │ └── SettingApplicationAction.vue -├── components/ -│ └── ApplicationCreateDropdown.vue +├── create-application/ +│ ├── AdvancedCreateDialog.vue # 高级智能体创建弹窗 +│ ├── CreateApplicationDropdown.vue # 简易、高级和导入创建入口 +│ └── SimpleCreateDialog.vue # 简易智能体创建弹窗 └── template.ts + +src/views/application-detail/ +├── WorkspaceApplicationDetail.vue # 提供工作空间智能体详情上下文并组合资源详情布局 +├── context.ts # 智能体详情子路由共享数据与刷新能力 +├── overview/ +│ └── OverviewView.vue # 智能体概览内容 +└── setting/ + └── SimpleSettingView.vue # 简易智能体设置内容 ``` `ApplicationCard` 只维护展示内容、批量选择状态和 `action-dropdown` 插槽;`ApplicationView` 组合 设置、移动、导出和删除 Action,并管理批量选择、批量移动和批量删除流程。需要请求的 Action 接收 页面传入的完整 Application API。单项移动在具体目录中通过 `delete` 通知页面局部移除卡片,在 -“全部智能体”中保留卡片;移动弹窗复用公共 `MoveToDialog`。 +“全部智能体”中保留卡片;移动弹窗复用公共 `MoveToDialog`。非批量选择状态点击卡片时,卡片通过 +`open` 事件通知 `ApplicationView` 进入详情路由。列表通过 `folderId` query 保存当前文件夹,进入详情 +和返回列表时保持该 query。`WorkspaceApplicationDetail` 负责工作空间智能体详情上下文,共享的返回 +入口、二级导航和右侧子路由结构由 `layout/ResourceDetailLayout.vue` 维护。详情子路由通过 +`useApplicationDetailContext()` 读取只读详情;接口返回完整详情时调用 `replaceApplicationDetail()` +局部替换,接口只返回布尔值或部分数据时调用 `refreshApplicationDetail()` 重新获取详情。切换二级 +菜单不会重新挂载详情容器,也不会自动重复请求详情。 当前工具页面按工具类型组织维护表单,共用的参数和代码设置保留在 `tool-form/component/` 中: @@ -214,6 +229,9 @@ Dialog。新增或重命名文件时,应同步更新所有导入和页面功 | 页面 | 功能说明 | | ---------------------------------------------------------------------- | -------------------------------------- | | `application/ApplicationView.vue` | 工作空间智能体目录与智能体卡片页面 | +| `application-detail/WorkspaceApplicationDetail.vue` | 工作空间智能体详情上下文与资源详情布局 | +| `application-detail/overview/OverviewView.vue` | 智能体概览内容 | +| `application-detail/setting/SimpleSettingView.vue` | 简易智能体设置内容 | | `workflow/ApplicationWorkflowView.vue` | 智能体工作流页面头部与全屏画布 | | `chat/ChatView.vue` | Chat 入口的对话页面 | | `error/NotFoundView.vue` | Admin 未匹配路由和全局 404 页面 | diff --git a/ui/src/views/application-detail/WorkspaceApplicationDetail.vue b/ui/src/views/application-detail/WorkspaceApplicationDetail.vue new file mode 100644 index 00000000000..2e274fda74f --- /dev/null +++ b/ui/src/views/application-detail/WorkspaceApplicationDetail.vue @@ -0,0 +1,72 @@ + + + + + + + + + {{ application?.name }} + + + diff --git a/ui/src/views/application-detail/context.ts b/ui/src/views/application-detail/context.ts new file mode 100644 index 00000000000..c930aa780e2 --- /dev/null +++ b/ui/src/views/application-detail/context.ts @@ -0,0 +1,26 @@ +/** + * 智能体详情子路由的共享上下文契约。 + * 子页面只读取容器持有的详情;保存接口返回完整详情时直接替换,返回布尔值或部分数据时重新请求, + * 避免依赖父容器重新挂载来同步标题、图标和其他子页面共用的数据。 + */ + +import { inject, type ComputedRef, type InjectionKey } from 'vue' +import type { ApplicationDetail } from '@/api/types' + +export interface ApplicationDetailContext { + /** 当前共享详情,子页面不能直接替换其引用。 */ + application: ComputedRef + /** 重新请求当前 applicationId 的完整详情。 */ + refreshApplicationDetail: () => Promise + /** 使用接口返回的完整详情更新上下文,避免额外请求。 */ + replaceApplicationDetail: (applicationDetail: ApplicationDetail) => void +} + +export const applicationDetailContextKey: InjectionKey = Symbol('application-detail-context') + +export function useApplicationDetailContext() { + const applicationDetailContext = inject(applicationDetailContextKey) + if (!applicationDetailContext) throw new Error('Application detail context is unavailable') + + return applicationDetailContext +} diff --git a/ui/src/views/application-detail/overview/OverviewView.vue b/ui/src/views/application-detail/overview/OverviewView.vue new file mode 100644 index 00000000000..7b8b46cb048 --- /dev/null +++ b/ui/src/views/application-detail/overview/OverviewView.vue @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/views/application-detail/setting/SimpleSettingView.vue b/ui/src/views/application-detail/setting/SimpleSettingView.vue new file mode 100644 index 00000000000..7b8b46cb048 --- /dev/null +++ b/ui/src/views/application-detail/setting/SimpleSettingView.vue @@ -0,0 +1,3 @@ + + + diff --git a/ui/src/views/application/ApplicationView.vue b/ui/src/views/application/ApplicationView.vue index 25768ebbb7d..a3ecbc5988c 100644 --- a/ui/src/views/application/ApplicationView.vue +++ b/ui/src/views/application/ApplicationView.vue @@ -1,5 +1,6 @@ @@ -21,6 +25,8 @@ defineSlots<{ 'action-dropdown'?: () => unknown }>() :selectable="selectable" :selected="selected" :title="application.name" + :class="{ 'cursor-pointer': !selectable }" + @click="handleOpen" @selected="emit('selected', $event)" > diff --git a/ui/src/views/application/application-card/action-dropdown/SettingApplicationAction.vue b/ui/src/views/application/application-card/action-dropdown/SettingApplicationAction.vue index 9fa4c267fed..341520347d7 100644 --- a/ui/src/views/application/application-card/action-dropdown/SettingApplicationAction.vue +++ b/ui/src/views/application/application-card/action-dropdown/SettingApplicationAction.vue @@ -12,7 +12,18 @@ const route = useRoute() function handleSettingApplication(event: MouseEvent) { event.stopPropagation() - if (!isWorkFlow(props.application.type)) return + if (!isWorkFlow(props.application.type)) { + void router.push({ + name: 'workspace-application-simple-setting', + params: { + applicationId: props.application.id, + type: props.application.type, + workspaceId: route.params.workspaceId, + }, + query: route.query, + }) + return + } const workflowRoute = { name: 'workflow-application', diff --git a/ui/src/views/application/create-application/AdvancedCreateDialog.vue b/ui/src/views/application/create-application/AdvancedCreateDialog.vue new file mode 100644 index 00000000000..68e1547f3eb --- /dev/null +++ b/ui/src/views/application/create-application/AdvancedCreateDialog.vue @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + 空白创建 + + + + + + + + 知识库问答助手 + + 将从知识库中检索到的知识作为已知信息,回答用户提出的问题。 + + + 204 + + + + + + + + 取消 + 创建 + + + diff --git a/ui/src/views/application/components/ApplicationCreateDropdown.vue b/ui/src/views/application/create-application/CreateApplicationDropdown.vue similarity index 59% rename from ui/src/views/application/components/ApplicationCreateDropdown.vue rename to ui/src/views/application/create-application/CreateApplicationDropdown.vue index 336e8fc2b4e..b0b150f9901 100644 --- a/ui/src/views/application/components/ApplicationCreateDropdown.vue +++ b/ui/src/views/application/create-application/CreateApplicationDropdown.vue @@ -1,13 +1,15 @@ + + + + + + + + + + + + + + + 取消 + 创建 + + + diff --git a/ui/src/views/tool/components/CreateToolDropdown.vue b/ui/src/views/tool/components/CreateToolDropdown.vue index d330c47d8bb..1500fae8837 100644 --- a/ui/src/views/tool/components/CreateToolDropdown.vue +++ b/ui/src/views/tool/components/CreateToolDropdown.vue @@ -104,8 +104,8 @@ function handleRefresh() { 数据源 - - + +