cc hace 2 semanas
padre
commit
037d157bed

+ 4 - 5
ai-electron/frontend/src/codex/views/workbench/Workbench.vue

@@ -177,7 +177,6 @@
   import { useMessage } from '@/core/hooks/web/useMessage';
   import { exitCaseConfirmOptions } from '@/core/logics/exitCaseConfirm';
   import { useUserStore } from '@/core/store/modules/case';
-import { useAppStoreWithOut } from '@/core/store/modules/app';
   import WbModelPage from './WbModelPage.vue';
   import WbPluginPage from './WbPluginPage.vue';
   import WbSessionList from './WbSessionList.vue';
@@ -193,7 +192,6 @@ import { useAppStoreWithOut } from '@/core/store/modules/app';
 
   const router = useRouter();
   const userStore = useUserStore();
-  const appStore = useAppStoreWithOut();
   const { createConfirm } = useMessage();
 
   type WorkbenchView = 'chat' | 'model' | 'plugin';
@@ -265,16 +263,17 @@ import { useAppStoreWithOut } from '@/core/store/modules/app';
   });
 
   /**
-   * 退出案件:回传统模式的案件管理页重新选择(工作台不允许直接切案件)。
+   * 退出案件:回案件管理页重新选择(工作台不允许直接切案件)。
    * 破坏性动作(当前会话上下文、工作台状态都会离开)先弹确认框。
+   *
+   * 模式偏好保持不动:用户选的是「我的主界面是工作台」,退出案件只是去挑另一个案件,
+   * 重新开案后仍按偏好回工作台(落点在 case store 的 afteropenCaseAction)。
    */
   function onExitCase() {
     // 用项目的 createConfirm(居中 + 警告图标),与其它页面的确认框外观统一;
     // content 用 VNode 而非字符串:createConfirm 对字符串走 innerHTML,案件名不该进 HTML
     createConfirm(
       exitCaseConfirmOptions(async () => {
-        // 退出后回传统模式:重新打开案件时落在传统布局,需要工作台再切回来
-        appStore.setWorkbenchMode('traditional');
         await userStore.logout(true);
       }, caseName.value),
     );

+ 3 - 3
ai-electron/frontend/src/core/components/Application/src/AppTopBar.vue

@@ -37,8 +37,8 @@
     <!-- 中间信息区(工作台:数据就绪 / 路由层 / 模型 等 chip) -->
     <slot name="info"></slot>
 
-    <!-- 模式切换:两个壳共用同一个开关与跳转口径 -->
-    <AppWorkbenchToggle v-if="showModeSwitch" @change="setMode" />
+    <!-- 模式切换:两个壳共用同一个开关与跳转口径;本壳即智能工作台,故 current 固定为 smart -->
+    <AppWorkbenchToggle v-if="showModeSwitch" current="smart" @change="setMode" />
 
     <!-- 右侧动作区:传统壳放搜索 / 模板库 / 设置 / 退出;工作台放诊断 -->
     <div class="app-topbar__actions jeesite-layout-header-action">
@@ -77,7 +77,7 @@
     defineProps<{
       /** 顶栏主题(跟随布局设置,默认亮色) */
       theme?: 'light' | 'dark';
-      /** 是否显示模式切换开关 */
+      /** 是否显示模式切换开关(默认显示;开关按「智能工作台」渲染,传统壳自己往 #actions 里放) */
       showModeSwitch?: boolean;
       /** 当前案件名(空则不渲染案件区) */
       caseName?: string | null;

+ 5 - 3
ai-electron/frontend/src/core/components/Application/src/AppWorkbenchToggle.vue

@@ -25,9 +25,12 @@
   /**
    * 主界面模式切换:传统(现有布局)⇄ 智能(全屏 AI 工作台)。
    *
-   * 状态持久化在 app store(APP_WORKBENCH_MODE__,按用户记住);
-   * 路由跳转由使用方决定(传统壳里切智能 → /workbench;工作台里切传统 → 首页),
+   * 使用方通过 `current` 传「当前所在壳」的模式,开关因此不会与实际布局互相矛盾;
+   * 模式偏好的持久化与路由跳转由使用方决定(传统壳里切智能 → /workbench;工作台里切传统 → 首页),
    * 这里只负责发出意图事件,保持组件无路由依赖。
+   *
+   * 点当前已选中的一侧也会发事件:`current` 是「所在壳」,可能与「偏好」不同
+   * (例如偏好是智能、当前在业务页的传统壳),把偏好对齐同样要让使用方收到这一下。
    */
   import { computed } from 'vue';
   import Icon from '@/core/components/Icon/src/Icon.vue';
@@ -48,7 +51,6 @@
   const mode = computed<WorkbenchMode>(() => props.current ?? appStore.getWorkbenchMode);
 
   function setMode(value: WorkbenchMode) {
-    if (mode.value === value) return;
     emit('change', value);
   }
 </script>

+ 37 - 10
ai-electron/frontend/src/core/layouts/default/header/index.vue

@@ -28,8 +28,8 @@
         <AppSearch mode="bar" placeholder="搜索系统内容" />
       </div>
       <span :class="`${prefixCls}-search-divider`"></span>
-      <!-- 传统 ⇄ 智能:放在模板库前面 -->
-      <AppWorkbenchToggle @change="setWorkbenchMode" />
+      <!-- 传统 ⇄ 智能:放在模板库前面。current 传死为 traditional —— 本壳就是传统壳 -->
+      <AppWorkbenchToggle current="traditional" @change="handleWorkbenchModeChange" />
 
       <Tooltip v-if="showTemplateLibraryAction" title="模板库" placement="bottom" :mouseEnterDelay="0.5">
         <Button
@@ -49,6 +49,9 @@
       <SettingDrawer v-if="getShowSetting" />
     </template>
   </AppTopBar>
+
+  <!-- 模板库:挂在顶栏上就地打开,不再先跳到案件数据页(会把用户的位置与左侧菜单顶掉) -->
+  <CaseTemplateLibraryModal v-model:open="templateLibraryVisible" />
 </template>
 <script lang="ts">
   import { defineComponent, ref, unref, computed } from 'vue';
@@ -69,6 +72,8 @@
   import { useDesign } from '@/core/hooks/web/useDesign';
   import { useMessage } from '@/core/hooks/web/useMessage';
   import { useWorkbenchModeSwitch } from '@/core/logics/workbenchMode';
+  import { useAppStoreWithOut } from '@/core/store/modules/app';
+  import type { WorkbenchMode } from '@/core/store/modules/app';
   import { exitCaseConfirmOptions } from '@/core/logics/exitCaseConfirm';
   import { useGo } from '@/core/hooks/web/usePage';
   import { useLocale } from '@/core/locales/useLocale';
@@ -80,8 +85,7 @@
   import LayoutMenu from '../menu/index.vue';
   import LayoutTrigger from '../trigger/index.vue';
   import { LayoutBreadcrumb, FullScreen, ErrorAction, SettingDrawer } from './components';
-
-  const HEADER_TEMPLATE_LIBRARY_OPEN_EVENT = 'layout-header:open-template-library';
+  import CaseTemplateLibraryModal from '@/case/views/data/components/CaseTemplateLibraryModal.vue';
 
   export default defineComponent({
     name: 'LayoutHeader',
@@ -89,6 +93,7 @@
       Button,
       AppTopBar,
       AppWorkbenchToggle,
+      CaseTemplateLibraryModal,
       LayoutTrigger,
       LayoutBreadcrumb,
       LayoutMenu,
@@ -106,6 +111,7 @@
       const { prefixCls } = useDesign('layout-header');
       const { createConfirm } = useMessage();
       const { setMode: setWorkbenchMode } = useWorkbenchModeSwitch();
+      const appStore = useAppStoreWithOut();
       const userStore = useUserStore();
       const emitter = useEmitter();
       const route = useRoute();
@@ -185,12 +191,17 @@
 
       const showTemplateLibraryAction = computed(() => true);
 
+      /** 模板库弹框开关(组件挂在顶栏上,点按钮就地打开) */
+      const templateLibraryVisible = ref(false);
 
-      const handleTemplateLibraryClick = async () => {
-        if (route.name !== 'ViewsDataIndex') {
-          await go(PageEnum.BASE_HOME);
-        }
-        emitter.emit(HEADER_TEMPLATE_LIBRARY_OPEN_EVENT, undefined);
+      /**
+       * 打开模板库。
+       *
+       * 模板库已抽成独立组件挂在本顶栏上,所以这里只开弹框:
+       * 之前是「先跳到案件数据页 + 发事件」,会把用户当前页面和左侧菜单一起顶掉。
+       */
+      const handleTemplateLibraryClick = () => {
+        templateLibraryVisible.value = true;
       };
 
       /**
@@ -205,6 +216,21 @@
         );
       };
 
+      /**
+       * 传统壳里的模式开关(开关恒显示「传统」为当前,故只会收到"想切去哪一侧")。
+       *
+       * <p>切智能:记偏好 + 跳工作台(走统一口径,见 useWorkbenchModeSwitch)。
+       * <p>点传统:本壳已经是传统,只把偏好对齐成传统 —— 偏好会决定下次开案的落点,
+       * 用户在当前布局下点「传统」,意图就是「以后也走传统」,不能因为开关已高亮就吞掉这次点击。
+       */
+      const handleWorkbenchModeChange = (mode: WorkbenchMode) => {
+        if (mode === 'smart') {
+          setWorkbenchMode('smart');
+          return;
+        }
+        appStore.setWorkbenchMode('traditional');
+      };
+
       return {
         prefixCls,
         getHeaderClass,
@@ -229,9 +255,10 @@
         getShowSearch,
         currentCaseName,
         showTemplateLibraryAction,
+        templateLibraryVisible,
         handleTemplateLibraryClick,
         handleExitCase,
-        setWorkbenchMode,
+        handleWorkbenchModeChange,
       };
     },
   });

+ 10 - 0
ai-electron/frontend/src/core/logics/workbenchMode.ts

@@ -5,6 +5,16 @@ import { PageEnum } from '@/core/enums/pageEnum';
 /** 智能工作台的全屏路由地址(守卫与切换器同口径) */
 export const WORKBENCH_PATH = '/workbench';
 
+/**
+ * 当前模式下的「主界面」地址(打开案件后的落点)。
+ *
+ * 传统模式回案件数据首页,智能模式进工作台:用户在开关里选的主界面,
+ * 开案后就该落在那里,而不是被固定拉回传统布局。
+ */
+export function getModeHomePath(): string {
+  return useAppStoreWithOut().getWorkbenchMode === 'smart' ? WORKBENCH_PATH : PageEnum.BASE_HOME;
+}
+
 /**
  * 主界面模式切换的唯一实现:写持久化状态 + 只在「当前页与目标模式不匹配」时跳转。
  *

+ 5 - 22
ai-electron/frontend/src/core/router/guard/index.ts

@@ -15,6 +15,7 @@ import {
 } from '@/core/logics/analysisRefreshPrompt';
 import { createPermissionGuard } from './permissionGuard';
 import { createStateGuard } from './stateGuard';
+import { WORKBENCH_PATH } from '@/core/logics/workbenchMode';
 // import nProgress from 'nprogress';
 import projectSetting from '@/core/settings/projectSetting';
 // import { createParamMenuGuard } from './paramMenuGuard';
@@ -37,38 +38,20 @@ export function setupRouterGuard(router: Router) {
  * 主界面模式守卫:
  * - 传统模式敲 /workbench → 回首页;
  * - 智能模式敲 AI 菜单组页面 → 引导回工作台(AI 能力集中在工作台呈现)。
- * 业务分析各页面两种模式都可直接访问(直接敲 URL 不拦)。
+ * 业务分析各页面两种模式都可直接访问(直接敲 URL 不拦),页面本身自带布局壳,
+ * 所以这里**不改写**用户的模式偏好 —— 偏好只由模式开关设置,用来决定开案后的落点。
  */
 function createWorkbenchGuard(router: Router) {
-  const AI_GROUP_PATHS = [
-    '/aiAnalysis/index',
-    '/aiModel/index',
-    '/aiPlugin/index',
-    '/codexAnalysis/codexAnalysis',
-  ];
-  const WORKBENCH_PATH = '/workbench';
-  /** 各自成壳的全屏页:不套 LAYOUT,两种模式下都直接访问 */
-  const SHELL_FREE_PATHS = [
-    PageEnum.BASE_CASE,
-    PageEnum.BASE_UPLOAD,
-    PageEnum.BASE_CLEANING,
-    PageEnum.BASE_CLEAN_PROGRESS,
-    PageEnum.BASE_AUTHORIZATION,
-  ];
+  const AI_GROUP_PATHS = ['/aiAnalysis/index', '/aiModel/index', '/aiPlugin/index', '/codexAnalysis/codexAnalysis'];
 
   router.beforeEach((to) => {
-    const appStore = useAppStoreWithOut();
-    const mode = appStore.getWorkbenchMode;
+    const mode = useAppStoreWithOut().getWorkbenchMode;
 
     if (mode === 'smart') {
       // AI 能力集中在工作台:老的 AI 菜单页一律引导回工作台
       if (AI_GROUP_PATHS.some((p) => to.path === p || to.path.startsWith(`${p}/`))) {
         return { path: WORKBENCH_PATH, replace: true };
       }
-      // 业务分析页属于传统壳:智能模式下请求它们时把模式切回传统,
-      // 否则会出现「壳是传统布局、模式开关却还是智能」的错位
-      const shellFree = to.path === WORKBENCH_PATH || SHELL_FREE_PATHS.includes(to.path as PageEnum);
-      if (!shellFree) appStore.setWorkbenchMode('traditional');
       return;
     }
 

+ 6 - 3
ai-electron/frontend/src/core/store/modules/case.ts

@@ -16,6 +16,7 @@ import { RouteRecordRaw } from 'vue-router';
 import { PAGE_NOT_FOUND_ROUTE } from '@/core/router/routes/basic';
 import { mitt, Emitter } from '@/core/utils/mitt';
 import { openCase, exitCase, OpenCaseDTO, CaseInfo, list as listCases } from '@/plat/api/case/caseApi';
+import { getModeHomePath } from '@/core/logics/workbenchMode';
 
 function ensurePageNotFoundRoute() {
   if (!router.hasRoute(PAGE_NOT_FOUND_ROUTE.name as string)) {
@@ -194,14 +195,16 @@ export const useUserStore = defineStore('app-user', {
       }
       if (goHome) {
         const currentRoute = router.currentRoute.value;
+        // 没有明确来源页时按当前模式落主界面:传统 → 案件数据首页;智能 → 工作台
+        const modeHome = getModeHomePath();
         let path = currentRoute.query.redirect;
         if (path !== '/') {
-          path = path || PageEnum.BASE_HOME;
+          path = path || modeHome;
         } else {
-          path = PageEnum.BASE_HOME;
+          path = modeHome;
         }
         const targetPath = ['/test/testData/list'].includes(decodeURIComponent(path as string))
-          ? PageEnum.BASE_HOME
+          ? modeHome
           : decodeURIComponent(path as string);
         if (currentRoute.name === PAGE_NOT_FOUND_ROUTE.name && currentRoute.path === targetPath) {
           await router.replace(PageEnum.BASE_CASE);