云
2026-10-25 c88ddec455babe8ed28649320caeb5d805d2f213
feat(core): 添加AI功能开关控制

- 在环境配置中新增VITE_APP_AI_ENABLED开关,默认设置为false
- 实现isAiFeatureEnabled工具函数用于判断AI功能是否启用
- 添加isAiRoute函数识别AI相关路由路径
- 实现filterDisabledAiRoutes函数过滤禁用的AI路由
- 在路由访问控制中集成AI功能开关过滤逻辑
- 为AI模块路由添加条件渲染支持
- 更新部署脚本中的服务器配置信息
- 在MES生产管理表单中集成AI功能启用状态检查
- 在质量控制表单中添加AI助手和建议组件的条件显示
- 为CRM销售报价单添加AI识别录入功能开关控制
- 移除百度地图密钥的重复定义
已添加1个文件
已删除1个文件
已修改12个文件
129 ■■■■■ 文件已修改
.env 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
deploy.bat 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
dist.zip 补丁 | 查看 | 原始文档 | blame | 历史
src/router/access.ts 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/index.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/ai.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/features.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/crm/saleQuotation/index.vue 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/mps/modules/form.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/workorder/modules/form.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/ipqc/modules/form.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/iqc/modules/form.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/oqc/modules/form.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/qc/rqc/modules/form.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env
@@ -19,6 +19,9 @@
# æ–‡æ¡£åœ°å€çš„开关
VITE_APP_DOCALERT_ENABLE=true
# AI åŠŸèƒ½æ€»å¼€å…³ï¼ˆè½»é‡ç‰ˆé»˜è®¤å…³é—­ï¼‰
VITE_APP_AI_ENABLED=false
# ç™¾åº¦ç»Ÿè®¡
VITE_APP_BAIDU_CODE = e98f2eab6ceb8688bc6d8fc5332ff093
@@ -35,4 +38,4 @@
# VITE_APP_API_ENCRYPT_RESPONSE_KEY = MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAOH8IfIFxL/MR9XIg1UDv5z1fGXQI93E8wrU4iPFovL/sEt9uSgSkjyidC2O7N+m7EKtoN6b1u7cEwXSkwf3kfK0jdWLSQaNpX5YshqXCBzbDfugDaxuyYrNA4/tIMs7mzZAk0APuRXB35Dmupou7Yw7TFW/7QhQmGfzeEKULQvnAgMBAAECgYAw8LqlQGyQoPv5p3gRxEMOCfgL0JzD3XBJKztiRd35RDh40Nx1ejgjW4dPioFwGiVWd2W8cAGHLzALdcQT2KDJh+T/tsd4SPmI6uSBBK6Ff2DkO+kFFcuYvfclQQKqxma5CaZOSqhgenacmgTMFeg2eKlY3symV6JlFNu/IKU42QJBAOhxAK/Eq3e61aYQV2JSguhMR3b8NXJJRroRs/QHEanksJtl+M+2qhkC9nQVXBmBkndnkU/l2tYcHfSBlAyFySMCQQD445tgm/J2b6qMQmuUGQAYDN8FIkHjeKmha+l/fv0igWm8NDlBAem91lNDIPBUzHL1X1+pcts5bjmq99YdOnhtAkAg2J8dN3B3idpZDiQbC8fd5bGPmdI/pSUudAP27uzLEjr2qrE/QPPGdwm2m7IZFJtK7kK1hKio6u48t/bg0iL7AkEAuUUs94h+v702Fnym+jJ2CHEkXvz2US8UDs52nWrZYiM1o1y4tfSHm8H8bv8JCAa9GHyriEawfBraILOmllFdLQJAQSRZy4wmlaG48MhVXodB85X+VZ9krGXZ2TLhz7kz9iuToy53l9jTkESt6L5BfBDCVdIwcXLYgK+8KFdHN5W7HQ==
# ç™¾åº¦åœ°å›¾
VITE_BAIDU_MAP_KEY=Y2aJXiswwPxy6mwFs1z9c7U5gwX9WfUN
VITE_BAIDU_MAP_KEY=Y2aJXiswwPxy6mwFs1z9c7U5gwX9WfUN
deploy.bat
@@ -4,11 +4,11 @@
:: ================= Config =================
set "COMPANY=TEST"
set "USER=root"
set "PASS=Xdrjkj@10086#"
set "IP=36.213.90.123"
set "PORT=65022"
set "PASS=Wqsdp@123"
set "IP=1.95.78.124"
set "PORT=22"
set "ZIP_FILE=dist.zip"
set "REMOTE_DIR=/data/project/mom/vue/test/dist"
set "REMOTE_DIR=/data/project/xgdl/vue/dist"
set "SSH_OPTS=-o StrictHostKeyChecking=no -o ConnectTimeout=10"
:: ===========================================
dist.zip
Binary files differ
src/router/access.ts
@@ -10,8 +10,34 @@
import { convertServerMenuToRouteRecordStringComponent } from '@vben/utils';
import { BasicLayout, IFrameView } from '#/layouts';
import { isAiFeatureEnabled } from '#/utils/features';
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
function isAiRoute(route: AppRouteRecordRaw) {
  const path = String(route.path || '').toLowerCase();
  const component = String(route.component || '').toLowerCase();
  return (
    path === '/ai' ||
    path.startsWith('/ai/') ||
    component.startsWith('ai/') ||
    component.includes('/views/ai/')
  );
}
function filterDisabledAiRoutes(routes: AppRouteRecordRaw[]): AppRouteRecordRaw[] {
  if (isAiFeatureEnabled) {
    return routes;
  }
  return routes
    .filter((route) => !isAiRoute(route))
    .map((route) => ({
      ...route,
      children: route.children
        ? filterDisabledAiRoutes(route.children as AppRouteRecordRaw[])
        : route.children,
    }));
}
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
  const pageMap: ComponentRecordType = import.meta.glob('../views/**/*.vue');
@@ -27,7 +53,9 @@
    fetchMenuListAsync: async () => {
      // ç”±äºŽ yudao é€šè¿‡ accessStore è¯»å–,所以不在进行 message.loading æç¤º
      // è¡¥å……说明:accessStore.accessMenus ä¸€å¼€å§‹æ˜¯ AppRouteRecordRaw ç±»åž‹ï¼ˆåŽç«¯åŠ è½½ï¼‰ï¼ŒåŽé¢è¢«èµ‹å€¼æˆ MenuRecordRaw ç±»åž‹ï¼ˆå‰ç«¯è½¬æ¢ï¼‰
      const accessMenus = accessStore.accessMenus as AppRouteRecordRaw[];
      const accessMenus = filterDisabledAiRoutes(
        accessStore.accessMenus as AppRouteRecordRaw[],
      );
      return convertServerMenuToRouteRecordStringComponent(accessMenus);
    },
    // å¯ä»¥æŒ‡å®šæ²¡æœ‰æƒé™è·³è½¬403页面
src/router/routes/index.ts
@@ -2,6 +2,8 @@
import { mergeRouteModules, traverseTreeValues } from '@vben/utils';
import { isAiFeatureEnabled } from '#/utils/features';
import { coreRoutes, fallbackNotFoundRoute } from './core';
const dynamicRouteFiles = import.meta.glob('./modules/**/*.ts', {
@@ -40,6 +42,7 @@
  import.meta.glob('../../views/**/*.vue'),
)
  .filter((item) => !item.includes('/modules/'))
  .filter((item) => isAiFeatureEnabled || !item.includes('/views/ai/'))
  .map((v) => {
    const path = v.replace('../../views/', '/');
    return path.endsWith('.vue') ? path.slice(0, -4) : path;
src/router/routes/modules/ai.ts
@@ -1,6 +1,8 @@
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
import { isAiFeatureEnabled } from '#/utils/features';
const routes: RouteRecordRaw[] = isAiFeatureEnabled ? [
  {
    path: '/ai',
    name: 'Ai',
@@ -108,6 +110,6 @@
      },
    ],
  },
];
] : [];
export default routes;
src/utils/features.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
/**
 * æ–°é«˜ç”µåŠ›åŠŸèƒ½å¼€å…³ã€‚
 * é»˜è®¤å…³é—­ AI,避免轻量后端未携带 AI æ¨¡å—时仍展示不可用入口。
 */
export const isAiFeatureEnabled =
  import.meta.env.VITE_APP_AI_ENABLED === 'true';
src/views/crm/saleQuotation/index.vue
@@ -1,5 +1,6 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { ActionItem } from '#/adapter/vxe-table';
import type { CrmSaleQuotationApi } from '#/api/crm/saleQuotation';
import { useRouter } from 'vue-router';
@@ -15,6 +16,7 @@
  getSaleQuotationPage,
} from '#/api/crm/saleQuotation';
import { $t } from '#/locales';
import { isAiFeatureEnabled } from '#/utils/features';
import type { CrmSaleQuotationAiApi } from '#/api/crm/saleQuotation/ai';
@@ -52,6 +54,24 @@
function handleOcrSuccess(data: CrmSaleQuotationAiApi.OcrResultVO) {
  nextTick(() => {
    formModalApi.setData({ ocrData: data } as any).open();
  });
}
const toolbarActions: ActionItem[] = [
  {
    label: $t('ui.actionTitle.create', ['报价单']),
    type: 'primary',
    icon: ACTION_ICON.ADD,
    auth: ['crm:sale-quotation:create'],
    onClick: handleCreate,
  },
];
if (isAiFeatureEnabled) {
  toolbarActions.push({
    label: 'AI è¯†åˆ«å½•å…¥',
    type: 'default',
    auth: ['crm:sale-quotation:create'],
    onClick: handleOcrCreate,
  });
}
@@ -146,26 +166,14 @@
<template>
  <Page auto-content-height>
    <FormModal @success="handleRefresh" />
    <OcrUpload ref="ocrUploadRef" @success="handleOcrSuccess" />
    <OcrUpload
      v-if="isAiFeatureEnabled"
      ref="ocrUploadRef"
      @success="handleOcrSuccess"
    />
    <Grid>
      <template #toolbar-tools>
        <TableAction
          :actions="[
            {
              label: $t('ui.actionTitle.create', ['报价单']),
              type: 'primary',
              icon: ACTION_ICON.ADD,
              auth: ['crm:sale-quotation:create'],
              onClick: handleCreate,
            },
            {
              label: 'AI è¯†åˆ«å½•å…¥',
              type: 'default',
              auth: ['crm:sale-quotation:create'],
              onClick: handleOcrCreate,
            },
          ]"
        />
        <TableAction :actions="toolbarActions" />
      </template>
      <template #name="{ row }">
        <Button type="link" @click="handleDetail(row)">
@@ -233,4 +241,4 @@
      </template>
    </Grid>
  </Page>
</template>
</template>
src/views/mes/pro/mps/modules/form.vue
@@ -12,6 +12,7 @@
import { useVbenForm } from '#/adapter/form';
import { createMps, getMps, updateMps } from '#/api/mes/pro/mps';
import { $t } from '#/locales';
import { isAiFeatureEnabled } from '#/utils/features';
import PredictDelivery from '../../workorder/modules/ai/predict-delivery.vue';
import PredictDuration from '../../workorder/modules/ai/predict-duration.vue';
@@ -25,7 +26,7 @@
const isEditable = computed(() => ['create', 'edit'].includes(formType.value));
const canUseAi = computed(() => !!formData.value?.id);
const canUseAi = computed(() => isAiFeatureEnabled && !!formData.value?.id);
const getTitle = computed(() => {
  switch (formType.value) {
src/views/mes/pro/workorder/modules/form.vue
@@ -23,6 +23,7 @@
  } from "#/api/mes/pro/workorder";
  import { batchSaveWorkOrderProcess, getWorkOrderProcessListByWorkOrderId } from "#/api/mes/pro/workorder/process";
  import { $t } from "#/locales";
  import { isAiFeatureEnabled } from "#/utils/features";
  // import { BarcodeDetail } from "#/views/wls/barcode/components";
  import { useFormSchema } from "../data";
@@ -56,6 +57,7 @@
  );
  const canUseAi = computed(
    () =>
      isAiFeatureEnabled &&
      formData.value?.id &&
      (formData.value?.status === MesProWorkOrderStatusEnum.PREPARE ||
        formData.value?.status === MesProWorkOrderStatusEnum.CONFIRMED)
src/views/mes/qc/ipqc/modules/form.vue
@@ -13,6 +13,7 @@
import { useVbenForm } from '#/adapter/form';
import { createIpqc, finishIpqc, getIpqc, updateIpqc } from '#/api/mes/qc/ipqc';
import { $t } from '#/locales';
import { isAiFeatureEnabled } from '#/utils/features';
import { QcIndicatorResultList } from '../../indicatorresult/components';
import MesQcAiSuggest from '../../components/ai-suggest.vue';
@@ -245,11 +246,11 @@
          å®Œæˆ
        </Button>
        <MesQcAiSuggest
          v-if="canFinish && formData?.id"
          v-if="isAiFeatureEnabled && canFinish && formData?.id"
          :qc-id="formData.id"
          :qc-type="MesQcTypeEnum.IPQC"
        />
        <MesQcAiAssistant />
        <MesQcAiAssistant v-if="isAiFeatureEnabled" />
      </div>
    </template>
  </Modal>
src/views/mes/qc/iqc/modules/form.vue
@@ -13,6 +13,7 @@
import { useVbenForm } from '#/adapter/form';
import { createIqc, finishIqc, getIqc, updateIqc } from '#/api/mes/qc/iqc';
import { $t } from '#/locales';
import { isAiFeatureEnabled } from '#/utils/features';
import { QcIndicatorResultList } from '../../indicatorresult/components';
import MesQcAiSuggest from '../../components/ai-suggest.vue';
@@ -246,11 +247,11 @@
          å®Œæˆ
        </Button>
        <MesQcAiSuggest
          v-if="canFinish && formData?.id"
          v-if="isAiFeatureEnabled && canFinish && formData?.id"
          :qc-id="formData.id"
          :qc-type="MesQcTypeEnum.IQC"
        />
        <MesQcAiAssistant />
        <MesQcAiAssistant v-if="isAiFeatureEnabled" />
      </div>
    </template>
  </Modal>
src/views/mes/qc/oqc/modules/form.vue
@@ -13,6 +13,7 @@
import { useVbenForm } from '#/adapter/form';
import { createOqc, finishOqc, getOqc, updateOqc } from '#/api/mes/qc/oqc';
import { $t } from '#/locales';
import { isAiFeatureEnabled } from '#/utils/features';
import { QcIndicatorResultList } from '../../indicatorresult/components';
import MesQcAiSuggest from '../../components/ai-suggest.vue';
@@ -249,11 +250,11 @@
          å®Œæˆ
        </Button>
        <MesQcAiSuggest
          v-if="canFinish && formData?.id"
          v-if="isAiFeatureEnabled && canFinish && formData?.id"
          :qc-id="formData.id"
          :qc-type="MesQcTypeEnum.OQC"
        />
        <MesQcAiAssistant />
        <MesQcAiAssistant v-if="isAiFeatureEnabled" />
      </div>
    </template>
  </Modal>
src/views/mes/qc/rqc/modules/form.vue
@@ -13,6 +13,7 @@
import { useVbenForm } from '#/adapter/form';
import { createRqc, finishRqc, getRqc, updateRqc } from '#/api/mes/qc/rqc';
import { $t } from '#/locales';
import { isAiFeatureEnabled } from '#/utils/features';
import { QcIndicatorResultList } from '../../indicatorresult/components';
import MesQcAiSuggest from '../../components/ai-suggest.vue';
@@ -245,11 +246,11 @@
          å®Œæˆ
        </Button>
        <MesQcAiSuggest
          v-if="canFinish && formData?.id"
          v-if="isAiFeatureEnabled && canFinish && formData?.id"
          :qc-id="formData.id"
          :qc-type="MesQcTypeEnum.RQC"
        />
        <MesQcAiAssistant />
        <MesQcAiAssistant v-if="isAiFeatureEnabled" />
      </div>
    </template>
  </Modal>