liu
昨天 b725cca0ff60728be04ef2ddb4cde5fcdf80bf8e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
<script lang="ts" setup>
import type { ModelCategoryInfo } from '#/api/bpm/model';
 
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,
  Switch,
  Tooltip,
} from 'ant-design-vue';
 
import {
  getAutoApproveList,
  updateGlobalAutoApprove,
} from '#/api/bpm/autoApprove';
import {
  getCategorySimpleList,
  updateCategorySortBatch,
} from '#/api/bpm/category';
import { getModelList } from '#/api/bpm/model';
import { router } from '#/router';
 
import CategoryForm from '../category/modules/form.vue';
import CategoryDraggableModel from './modules/category-draggable-model.vue';
 
const [CategoryFormModal, categoryFormModalApi] = useVbenModal({
  connectedComponent: CategoryForm,
  destroyOnClose: true,
});
 
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 分组的数据
const originalData = ref<ModelCategoryInfo[]>([]); // 未排序前的原始数据
 
const sortable = useTemplateRef<HTMLElement>('categoryGroupRef'); // 可以排序元素的容器
const sortableInstance = ref<any>(null); // 排序引用,以便后续启用或禁用排序
const isCategorySorting = ref(false); // 分类排序状态
 
const queryParams = reactive({
  name: '',
}); // 查询参数
 
/** 监听分类排序模式切换 */
watch(
  () => isCategorySorting.value,
  (newValue) => {
    if (sortableInstance.value) {
      if (newValue) {
        // 启用排序功能
        sortableInstance.value.option('disabled', false);
      } else {
        // 禁用排序功能
        sortableInstance.value.option('disabled', true);
      }
    }
  },
);
 
/** 加载数据 */
async function getList() {
  modelListSpinning.value = true;
  try {
    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,
      modelList: modelList.filter(
        (model: any) => model.categoryName === category.name,
      ),
    }));
    // 重置排序实例
    sortableInstance.value = null;
  } finally {
    modelListSpinning.value = false;
  }
}
 
/** 初始化 */
onMounted(() => {
  getList();
});
 
/** 激活时刷新(keep-alive 缓存场景下从编辑页返回时触发) */
onActivated(() => {
  getList();
});
 
/** 新增模型 */
function createModel() {
  router.push({
    name: 'BpmModelCreate',
  });
}
 
/** 处理下拉菜单命令 */
function handleCommand(command: string) {
  if (command === 'handleCategoryAdd') {
    // 打开新建流程分类弹窗
    categoryFormModalApi.open();
  } else if (command === 'handleCategorySort') {
    originalData.value = cloneDeep(categoryGroup.value);
    isCategorySorting.value = true;
    // 如果排序实例不存在,则初始化
    if (sortableInstance.value) {
      // 已存在实例,则启用排序功能
      sortableInstance.value.option('disabled', false);
    } else {
      sortableInstance.value = useSortable(sortable, categoryGroup, {
        disabled: false, // 启用排序
      });
    }
  }
}
 
/** 取消分类排序 */
function handleCategorySortCancel() {
  // 恢复初始数据
  categoryGroup.value = cloneDeep(originalData.value);
  isCategorySorting.value = false;
  // 直接禁用排序功能
  if (sortableInstance.value) {
    sortableInstance.value.option('disabled', true);
  }
}
 
/** 提交分类排序 */
async function handleCategorySortSubmit() {
  saveSortLoading.value = true;
  try {
    // 保存排序逻辑
    const ids = categoryGroup.value.map((item: any) => item.id);
    await updateCategorySortBatch(ids);
    message.success('分类排序成功');
  } finally {
    saveSortLoading.value = false;
  }
  isCategorySorting.value = false;
  // 刷新列表
  await getList();
  // 禁用排序功能
  if (sortableInstance.value) {
    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>
  <Page auto-content-height>
    <!-- 流程分类表单弹窗 -->
    <CategoryFormModal @success="getList" />
    <Card
      :body-style="{ padding: '10px' }"
      class="mb-4"
      title="流程模型"
      v-spinning="modelListSpinning"
    >
      <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="搜索流程"
            allow-clear
            @press-enter="getList"
            class="!w-60"
          />
          <Button class="ml-2" type="primary" @click="createModel">
            <IconifyIcon icon="lucide:plus" /> 新建模型
          </Button>
          <Dropdown class="ml-2" placement="bottomRight" arrow>
            <Button>
              <template #icon>
                <div class="flex items-center justify-center">
                  <IconifyIcon icon="lucide:settings" />
                </div>
              </template>
            </Button>
            <template #overlay>
              <Menu @click="(e) => handleCommand(e.key as string)">
                <Menu.Item key="handleCategoryAdd">
                  <div class="flex items-center gap-1">
                    <IconifyIcon icon="lucide:plus" />
                    新建分类
                  </div>
                </Menu.Item>
                <Menu.Item key="handleCategorySort">
                  <div class="flex items-center gap-1">
                    <IconifyIcon icon="lucide:align-start-vertical" />
                    分类排序
                  </div>
                </Menu.Item>
              </Menu>
            </template>
          </Dropdown>
        </div>
        <div class="flex h-full items-center justify-between" v-else>
          <Button @click="handleCategorySortCancel" class="mr-3">
            取 消
          </Button>
          <Button
            type="primary"
            :loading="saveSortLoading"
            @click="handleCategorySortSubmit"
          >
            保存排序
          </Button>
        </div>
      </template>
 
      <!-- 按照分类,展示其所属的模型列表 -->
      <div class="px-3" ref="categoryGroupRef">
        <CategoryDraggableModel
          v-for="(element, index) in categoryGroup"
          :class="isCategorySorting ? 'cursor-move' : ''"
          :key="element.id"
          :category-info="element"
          :is-category-sorting="isCategorySorting"
          :is-first="index === 0"
          @success="getList"
        />
      </div>
    </Card>
  </Page>
</template>