liu
昨天 b725cca0ff60728be04ef2ddb4cde5fcdf80bf8e
feat(bpm): 流程模型列表新增「免审批」开关与全局总开关

- 流程模型列表新增「免审批」列,行内切换单个流程是否跳过 OA 审批

- 页面右上角新增「全部免审批」总开关,开启后所有流程一律免审批(优先于各流程自己的开关)

- 无 bpm:process-auto-approve:update 权限时开关置灰;流程未发布时给出「开关不会生效」提示

Co-Authored-By: Claude <noreply@anthropic.com>
已添加1个文件
已修改4个文件
209 ■■■■■ 文件已修改
src/api/bpm/autoApprove/index.ts 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bpm/model/index.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/data.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/index.vue 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/bpm/model/modules/category-draggable-model.vue 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/bpm/autoApprove/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
import { requestClient } from '#/api/request';
export namespace BpmAutoApproveApi {
  /** æµç¨‹å…å®¡æ‰¹é…ç½® */
  export interface AutoApproveConfig {
    id: number;
    /** æµç¨‹å®šä¹‰çš„ Key */
    processDefinitionKey: string;
    /** æ˜¯å¦å…å®¡æ‰¹ï¼štrue-提交后系统自动通过,false-正常人工审批 */
    autoApprove: boolean;
    /** å¤‡æ³¨ */
    remark?: string;
    createTime: string;
  }
}
/** èŽ·å–å…¨éƒ¨æµç¨‹å…å®¡æ‰¹é…ç½® */
export async function getAutoApproveList() {
  return requestClient.get<BpmAutoApproveApi.AutoApproveConfig[]>(
    '/bpm/process-auto-approve/list',
  );
}
/** èŽ·å¾—å…¨å±€å…å®¡æ‰¹æ€»å¼€å…³çŠ¶æ€ */
export async function getGlobalAutoApprove() {
  return requestClient.get<boolean>('/bpm/process-auto-approve/global');
}
/** è®¾ç½®å…¨å±€å…å®¡æ‰¹æ€»å¼€å…³ï¼šå¼€å¯åŽæ‰€æœ‰æµç¨‹ä¸€å¾‹å…å®¡æ‰¹ */
export async function updateGlobalAutoApprove(
  autoApprove: boolean,
  remark?: string,
) {
  const params = new URLSearchParams({ autoApprove: String(autoApprove) });
  if (remark) {
    params.append('remark', remark);
  }
  return requestClient.put<boolean>(
    `/bpm/process-auto-approve/update-global?${params.toString()}`,
  );
}
/** æŒ‰æµç¨‹å®šä¹‰çš„ Key è®¾ç½®å…å®¡æ‰¹å¼€å…³ï¼ˆä¸å­˜åœ¨åˆ™æ–°å¢žã€å­˜åœ¨åˆ™æ›´æ–°ï¼‰ */
export async function updateAutoApproveByKey(
  processDefinitionKey: string,
  autoApprove: boolean,
  remark?: string,
) {
  const params = new URLSearchParams({
    processDefinitionKey,
    autoApprove: String(autoApprove),
  });
  if (remark) {
    params.append('remark', remark);
  }
  return requestClient.put<boolean>(
    `/bpm/process-auto-approve/update-by-key?${params.toString()}`,
  );
}
src/api/bpm/model/index.ts
@@ -20,6 +20,8 @@
    createTime: string;
    bpmnXml: string;
    startUsers?: UserInfo[];
    /** æ˜¯å¦å…å®¡æ‰¹ï¼ˆç”±é¡µé¢æ ¹æ®æµç¨‹å…å®¡æ‰¹é…ç½®åˆå¹¶ï¼ŒéžåŽç«¯è¿”回字段) */
    autoApprove?: boolean;
  }
  /** æµç¨‹å®šä¹‰ */
src/views/bpm/model/data.ts
@@ -40,6 +40,12 @@
      slots: { default: 'deploymentTime' },
    },
    {
      field: 'autoApprove',
      title: '免审批',
      minWidth: 110,
      slots: { default: 'autoApprove' },
    },
    {
      title: '操作',
      width: 150,
      fixed: 'right',
src/views/bpm/model/index.vue
@@ -1,15 +1,37 @@
<script lang="ts" setup>
import type { ModelCategoryInfo } from '#/api/bpm/model';
import { onActivated, onMounted, reactive, ref, useTemplateRef, watch } from 'vue';
import {
  computed,
  onActivated,
  onMounted,
  reactive,
  ref,
  useTemplateRef,
  watch,
} from 'vue';
import { useAccess } from '@vben/access';
import { Page, useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import { useSortable } from '@vueuse/integrations/useSortable';
import { Button, Card, Dropdown, Input, Menu, message } from 'ant-design-vue';
import {
  Button,
  Card,
  Dropdown,
  Input,
  Menu,
  message,
  Switch,
  Tooltip,
} from 'ant-design-vue';
import {
  getAutoApproveList,
  updateGlobalAutoApprove,
} from '#/api/bpm/autoApprove';
import {
  getCategorySimpleList,
  updateCategorySortBatch,
@@ -26,6 +48,17 @@
});
const modelListSpinning = ref(false); // æ¨¡åž‹åˆ—表加载状态
/** å…¨å±€å…å®¡æ‰¹æ€»å¼€å…³å ç”¨çš„æµç¨‹ Key(与后端 BpmProcessAutoApproveService.GLOBAL_KEY ä¿æŒä¸€è‡´ï¼‰ */
const GLOBAL_AUTO_APPROVE_KEY = '*';
const globalAutoApprove = ref(false); // å…¨å±€å…å®¡æ‰¹æ€»å¼€å…³
const globalAutoApproveLoading = ref(false); // æ€»å¼€å…³æäº¤çŠ¶æ€
const { hasAccessByCodes } = useAccess();
/** å…å®¡æ‰¹å¼€å…³çš„æƒé™ï¼šæ— æƒé™æ—¶å¼€å…³ç½®ç°ï¼Œé¿å…ç‚¹äº†æ‰æŠ¥é”™ */
const hasPermiAutoApprove = computed(() => {
  return hasAccessByCodes(['bpm:process-auto-approve:update']);
});
const saveSortLoading = ref(false); // ä¿å­˜æŽ’序状态
const categoryGroup = ref<ModelCategoryInfo[]>([]); // æŒ‰ç…§ category åˆ†ç»„的数据
@@ -59,8 +92,22 @@
async function getList() {
  modelListSpinning.value = true;
  try {
    const modelList = await getModelList(queryParams.name);
    const categoryList = await getCategorySimpleList();
    const [modelList, categoryList, autoApproveList] = await Promise.all([
      getModelList(queryParams.name),
      getCategorySimpleList(),
      getAutoApproveList(),
    ]);
    // å…å®¡æ‰¹é…ç½®æŒ‰ã€Œæµç¨‹ Key」建立映射,合并到模型行数据上,供列表内的免审批开关列直接读取
    const autoApproveMap = new Map<string, boolean>();
    autoApproveList.forEach((config) => {
      autoApproveMap.set(config.processDefinitionKey, config.autoApprove);
    });
    modelList.forEach((model: any) => {
      model.autoApprove = autoApproveMap.get(model.key) ?? false;
    });
    // å…¨å±€æ€»å¼€å…³ä»¥ç‰¹æ®Š Key å­˜æ”¾åœ¨åŒä¸€æ¡é…ç½®é‡Œ
    globalAutoApprove.value =
      autoApproveMap.get(GLOBAL_AUTO_APPROVE_KEY) ?? false;
    // æŒ‰ç…§ category èšåˆ
    categoryGroup.value = categoryList.map((category: any) => ({
      ...category,
@@ -142,6 +189,28 @@
    sortableInstance.value.option('disabled', true);
  }
}
/** åˆ‡æ¢å…¨å±€å…å®¡æ‰¹æ€»å¼€å…³ï¼šå¼€å¯åŽæ‰€æœ‰æµç¨‹ä¸€å¾‹å…å®¡æ‰¹ */
async function handleGlobalAutoApproveChange(checked: boolean) {
  globalAutoApprove.value = checked;
  globalAutoApproveLoading.value = true;
  try {
    await updateGlobalAutoApprove(checked);
    message.success(
      checked
        ? '已开启全局免审批:所有流程提交后将直接通过'
        : '已关闭全局免审批:恢复为按各流程开关判定',
    );
    // é‡æ–°æ‹‰å–列表,同步各流程开关的展示
    await getList();
  } catch (error) {
    // å¤±è´¥å›žæ»š
    globalAutoApprove.value = !checked;
    console.error('设置全局免审批开关失败', error);
  } finally {
    globalAutoApproveLoading.value = false;
  }
}
</script>
<template>
@@ -156,6 +225,16 @@
    >
      <template #extra>
        <div v-if="!isCategorySorting">
          <Tooltip title="开启后所有流程一律免审批(优先于各流程自己的开关)">
            <span class="mr-1">全部免审批</span>
          </Tooltip>
          <Switch
            :checked="globalAutoApprove"
            :loading="globalAutoApproveLoading"
            :disabled="!hasPermiAutoApprove"
            class="mr-3"
            @change="(checked: any) => handleGlobalAutoApproveChange(checked)"
          />
          <Input
            v-model:value="queryParams.name"
            placeholder="搜索流程"
src/views/bpm/model/modules/category-draggable-model.vue
@@ -21,11 +21,13 @@
  Dropdown,
  Menu,
  message,
  Switch,
  Tag,
  Tooltip,
} from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { updateAutoApproveByKey } from '#/api/bpm/autoApprove';
import { deleteCategory } from '#/api/bpm/category';
import {
  cleanModel,
@@ -64,6 +66,7 @@
const isModelSorting = ref(false);
const originalData = ref<BpmModelApi.Model[]>([]);
const modelList = ref<BpmModelApi.Model[]>([]);
const autoApproveLoadingId = ref<null | number>(null); // å…å®¡æ‰¹å¼€å…³ï¼šæ­£åœ¨æäº¤çš„æ¨¡åž‹ç¼–号
// TODO @jason:可以全部展开么? @芋艿 ä¸Šæ¬¡è®¨è®ºã€‚好像是因为性能问题才只展开第一个分类
const isExpand = ref(props.isFirst); // æ ¹æ®æ˜¯å¦ä¸ºç¬¬ä¸€ä¸ªåˆ†ç±», æ¥è®¾ç½®åˆå§‹å±•开状态
@@ -81,6 +84,10 @@
});
const hasPermiDeploy = computed(() => {
  return hasAccessByCodes(['bpm:model:deploy']);
});
/** å…å®¡æ‰¹å¼€å…³çš„æƒé™ï¼šæ— æƒé™æ—¶å¼€å…³ç½®ç°ï¼Œé¿å…ç‚¹äº†æ‰æŠ¥é”™ */
const hasPermiAutoApprove = computed(() => {
  return hasAccessByCodes(['bpm:process-auto-approve:update']);
});
const [Grid, gridApi] = useVbenVxeGrid({
@@ -287,6 +294,32 @@
  message.success(`发布[${row.name}]流程成功`);
  // åˆ·æ–°åˆ—表
  emit('success');
}
/** åˆ‡æ¢å…å®¡æ‰¹å¼€å…³ï¼šå¼€å¯åŽè¯¥æµç¨‹æäº¤æ—¶ä¸å†è¿›å…¥äººå·¥å®¡æ‰¹ï¼Œç”±ç³»ç»Ÿè‡ªåŠ¨é€šè¿‡ */
async function handleAutoApproveChange(checked: boolean, row: any) {
  const oldValue = row.autoApprove;
  // ä¹è§‚更新,避免等待接口期间开关状态回跳
  row.autoApprove = checked;
  gridApi.setGridOptions({ data: modelList.value });
  autoApproveLoadingId.value = row.id;
  try {
    await updateAutoApproveByKey(row.key, checked);
    message.success(
      checked
        ? `已开启[${row.name}]免审批:该流程提交后将由系统自动通过`
        : `已关闭[${row.name}]免审批:恢复为正常人工审批`,
    );
    // é‡æ–°æ‹‰å–列表,保证前端状态与后端一致
    emit('success');
  } catch (error) {
    // å¤±è´¥å›žæ»š
    row.autoApprove = oldValue;
    gridApi.setGridOptions({ data: modelList.value });
    console.error('设置免审批开关失败', error);
  } finally {
    autoApproveLoadingId.value = null;
  }
}
/** '更多'操作按钮 */
@@ -661,6 +694,28 @@
                </Tag>
              </div>
            </template>
            <template #autoApprove="{ row }">
              <div class="flex items-center gap-1">
                <Switch
                  :checked="row.autoApprove"
                  :loading="autoApproveLoadingId === row.id"
                  :disabled="!hasPermiAutoApprove"
                  size="small"
                  @change="
                    (checked: any) => handleAutoApproveChange(checked, row)
                  "
                />
                <Tooltip
                  v-if="!row.processDefinition"
                  title="流程尚未发布,免审批开关不会生效,请先发布流程"
                >
                  <IconifyIcon
                    icon="lucide:alert-circle"
                    class="text-base text-orange-400"
                  />
                </Tooltip>
              </div>
            </template>
            <template #actions="{ row }">
              <div class="flex items-center space-x-0">
                <Button