gaoluyang
2026-06-24 c0cb161bb52ce0fbdce5c66ec391d107c75e2452
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
303
304
<script lang="ts" setup>
import type { BpmCategoryApi } from '#/api/bpm/category';
import type { BpmProcessDefinitionApi } from '#/api/bpm/definition';
 
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
 
import { Page } from '..\..\..\..\packages\effects\common-ui\src';
import { groupBy } from '..\..\..\..\packages\utils\src';
 
import {
  Card,
  Col,
  InputSearch,
  message,
  Row,
  Space,
  Tabs,
  Tooltip,
} from 'ant-design-vue';
 
import { getCategorySimpleList } from '#/api/bpm/category';
import { getProcessDefinitionList } from '#/api/bpm/definition';
import { getProcessInstance } from '#/api/bpm/processInstance';
 
import ProcessDefinitionDetail from './modules/form.vue';
 
defineOptions({ name: 'BpmProcessInstanceCreate' });
 
const route = useRoute();
 
const loading = ref(true); // 加载中
const processInstanceId: any = route.query.processInstanceId; // 流程实例编号。场景:重新发起时
 
const categoryList: any = ref([]); // 分类的列表
const activeCategory = ref(''); // 当前选中的分类
 
const searchName = ref(''); // 当前搜索关键字
const processDefinitionList = ref<BpmProcessDefinitionApi.ProcessDefinition[]>(
  [],
); // 流程定义的列表
const filteredProcessDefinitionList = ref<
  BpmProcessDefinitionApi.ProcessDefinition[]
>([]); // 用于存储搜索过滤后的流程定义
 
const selectProcessDefinition = ref(); // 选择的流程定义
const processDefinitionDetailRef = ref();
 
/** 查询列表 */
async function getList() {
  loading.value = true;
  try {
    // 1.1 所有流程分类数据
    await loadCategoryList();
    // 1.2 所有流程定义数据
    await loadProcessDefinitionList();
 
    // 2. 如果 processInstanceId 非空,说明是重新发起
    if (processInstanceId?.length > 0) {
      const processInstance = await getProcessInstance(processInstanceId);
      if (!processInstance) {
        message.error('重新发起流程失败,原因:流程实例不存在');
        return;
      }
      const processDefinition = processDefinitionList.value.find(
        (item: any) => item.key === processInstance.processDefinition?.key,
      );
      if (!processDefinition) {
        message.error('重新发起流程失败,原因:流程定义不存在');
        return;
      }
      await handleSelect(processDefinition, processInstance.formVariables);
    }
  } finally {
    loading.value = false;
  }
}
 
/** 获取所有流程分类数据 */
async function loadCategoryList() {
  categoryList.value = await getCategorySimpleList();
}
 
/** 获取所有流程定义数据 */
async function loadProcessDefinitionList() {
  // 流程定义
  processDefinitionList.value = await getProcessDefinitionList({
    suspensionState: 1,
  });
 
  // 空搜索,初始化相关数据
  handleQuery();
}
 
/** 搜索流程 */
function handleQuery() {
  if (searchName.value.trim()) {
    // 如果有搜索关键字,进行过滤
    filteredProcessDefinitionList.value = processDefinitionList.value.filter(
      (definition: any) =>
        definition.name.toLowerCase().includes(searchName.value.toLowerCase()),
    );
    // 如果有匹配,切换到第一个包含匹配结果的分类
    activeCategory.value = availableCategories.value[0]?.name;
  } else {
    // 如果没有搜索关键字,恢复所有数据
    filteredProcessDefinitionList.value = processDefinitionList.value;
    // 恢复到第一个可用分类
    if (availableCategories.value.length > 0) {
      activeCategory.value = availableCategories.value[0].code;
    }
  }
}
 
/** 流程定义的分组 */
const processDefinitionGroup = computed(() => {
  if (!processDefinitionList.value?.length) {
    return {};
  }
  // 按照 categoryList 的顺序重新组织数据
  const grouped = groupBy(filteredProcessDefinitionList.value, 'category');
  const orderedGroup: Record<
    string,
    BpmProcessDefinitionApi.ProcessDefinition[]
  > = {};
  categoryList.value.forEach((category: BpmCategoryApi.Category) => {
    if (grouped[category.code]) {
      orderedGroup[category.code] = grouped[
        category.code
      ] as BpmProcessDefinitionApi.ProcessDefinition[];
    }
  });
  return orderedGroup;
});
 
/** 处理选择流程的按钮操作 */
async function handleSelect(
  row: BpmProcessDefinitionApi.ProcessDefinition,
  formVariables?: any,
) {
  // 设置选择的流程
  selectProcessDefinition.value = row;
  // 初始化流程定义详情
  await nextTick();
  processDefinitionDetailRef.value?.initProcessInfo(row, formVariables);
}
 
/** 过滤出有流程的分类列表。目的:只展示有流程的分类 */
const availableCategories = computed(() => {
  if (!categoryList.value?.length || !processDefinitionGroup.value) {
    return [];
  }
  // 获取所有有流程的分类代码
  const availableCategoryCodes = Object.keys(processDefinitionGroup.value);
  // 过滤出有流程的分类
  return categoryList.value.filter((category: BpmCategoryApi.Category) =>
    availableCategoryCodes.includes(category.code),
  );
});
 
/** 监听可用分类变化,自动设置正确的活动分类 */
watch(
  availableCategories,
  (newCategories) => {
    if (newCategories.length > 0) {
      // 如果当前活动分类不在可用分类中,切换到第一个可用分类
      const currentCategoryExists = newCategories.some(
        (category: BpmCategoryApi.Category) =>
          category.code === activeCategory.value,
      );
      if (!currentCategoryExists) {
        activeCategory.value = newCategories[0].code;
      }
    }
  },
  { immediate: true },
);
 
/** 初始化 */
onMounted(() => {
  getList();
});
</script>
 
<template>
  <Page auto-content-height>
    <!-- TODO @jason:这里交互,可以做成类似 vue3 + element-plus 那个一样,滚动切换分类哈?对标钉钉、飞书哈; -->
    <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
    <template v-if="!selectProcessDefinition">
      <Card
        class="h-full"
        title="全部流程"
        :class="{
          'process-definition-container': filteredProcessDefinitionList?.length,
        }"
        :loading="loading"
      >
        <template #extra>
          <div class="flex h-full items-center justify-center">
            <InputSearch
              v-model:value="searchName"
              class="!w-50%"
              placeholder="请输入流程名称检索"
              allow-clear
              @input="handleQuery"
              @clear="handleQuery"
            />
          </div>
        </template>
 
        <div v-if="filteredProcessDefinitionList?.length" class="-ml-6">
          <Tabs v-model:active-key="activeCategory" tab-position="left">
            <Tabs.TabPane
              v-for="category in availableCategories"
              :key="category.code"
              :tab="category.name"
            >
              <Row :gutter="[16, 16]" :wrap="true">
                <Col
                  v-for="definition in processDefinitionGroup[category.code]"
                  :key="definition.id"
                  :xs="24"
                  :sm="12"
                  :md="8"
                  :lg="8"
                  :xl="6"
                  @click="handleSelect(definition)"
                >
                  <Card
                    hoverable
                    class="w-full cursor-pointer"
                    :class="{
                      'animate-bounce-once !bg-[rgb(63_115_247_/_10%)]':
                        searchName.trim().length > 0,
                    }"
                    :body-style="{
                      width: '100%',
                      padding: '16px',
                    }"
                  >
                    <div class="flex items-center">
                      <img
                        v-if="definition.icon"
                        :src="definition.icon"
                        class="size-12 rounded object-contain"
                        alt="流程图标"
                      />
                      <div
                        v-else
                        class="flex size-12 flex-shrink-0 items-center justify-center rounded bg-primary"
                      >
                        <span class="text-xs text-white">
                          {{ definition.name?.slice(0, 2) }}
                        </span>
                      </div>
                      <span class="ml-3 flex-1 truncate text-base">
                        <Tooltip
                          placement="topLeft"
                          :title="`${definition.description}`"
                        >
                          {{ definition.name }}
                        </Tooltip>
                      </span>
                    </div>
                  </Card>
                </Col>
              </Row>
            </Tabs.TabPane>
          </Tabs>
        </div>
        <div v-else class="!py-48 text-center">
          <Space direction="vertical" size="large">
            <span class="text-gray-500">没有找到搜索结果</span>
          </Space>
        </div>
      </Card>
    </template>
 
    <!-- 第二步,填写表单,进行流程的提交 -->
    <ProcessDefinitionDetail
      v-else
      ref="processDefinitionDetailRef"
      :select-process-definition="selectProcessDefinition"
      @cancel="selectProcessDefinition = undefined"
    />
  </Page>
</template>
 
<style lang="scss" scoped>
@keyframes bounce {
  0%,
  50% {
    transform: translateY(-5px);
  }
 
  100% {
    transform: translateY(0);
  }
}
 
.animate-bounce-once {
  animation: bounce 0.5s ease;
}
</style>