zhangwencui
7 天以前 dab35bff798cfce2ddf7e8a67e4e1cdb144f5b4c
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
<script setup lang="ts">
  import { computed, ref } from "vue";
  import { onLoad } from "@dcloudio/uni-app";
  import {
    getCategorySimpleList,
    getProcessDefinitionList,
    type BpmCategory,
    type BpmProcessDefinition,
  } from "../../../../api/bpm";
  import { useAuth } from "../../../../stores/auth";
  import { toast } from "../../shared";
 
  const { state } = useAuth();
  const SELECTED_DEFINITION_KEY = "bpm_create_selected_definition";
 
  const categories = ref<BpmCategory[]>([]);
  const definitions = ref<BpmProcessDefinition[]>([]);
  const searchName = ref("");
  const activeCategory = ref("");
  const loading = ref(false);
  const loadError = ref("");
 
  /** 按分类分组(与 web 端 groupBy 一致) */
  const groupedDefinitions = computed(() => {
    const keyword = searchName.value.trim().toLowerCase();
    const list = definitions.value.filter(item => {
      if (activeCategory.value && item.category !== activeCategory.value) return false;
      if (!keyword) return true;
      return String(item.name || "").toLowerCase().includes(keyword);
    });
    const groups: Array<{ code: string; name: string; items: BpmProcessDefinition[] }> = [];
    list.forEach(item => {
      const code = item.category || "";
      let group = groups.find(g => g.code === code);
      if (!group) {
        group = {
          code,
          name: categories.value.find(c => c.code === code)?.name || "未分类",
          items: [],
        };
        groups.push(group);
      }
      group.items.push(item);
    });
    return groups;
  });
 
  /** 分类 Tab:仅保留有流程的分类(与 web 端 availableCategories 一致) */
  const availableCategories = computed(() => {
    const used = new Set(definitions.value.map(item => item.category || ""));
    return categories.value.filter(c => used.has(c.code || ""));
  });
 
  const categoryTabs = computed(() => [
    { code: "", name: "全部" },
    ...availableCategories.value.map(c => ({ code: c.code || "", name: c.name || "" })),
  ]);
 
  async function loadDefinitions() {
    if (!state.accessToken || loading.value) return;
    loading.value = true;
    loadError.value = "";
    try {
      const [categoryList, definitionList] = await Promise.all([
        getCategorySimpleList(state.accessToken),
        getProcessDefinitionList(state.accessToken, 1),
      ]);
      categories.value = categoryList || [];
      definitions.value = definitionList || [];
    } catch (error) {
      loadError.value = error instanceof Error ? error.message : "流程列表加载失败";
    } finally {
      loading.value = false;
    }
  }
 
  /**
   * 选择流程 -> 跳转到独立的「填写表单」页面(使用系统返回)。
   * 本页只负责「挑选流程定义」,「重新发起」由我的流程页直接跳表单页,不经过这里。
   */
  function handleSelect(definition: BpmProcessDefinition) {
    if (!definition.id) {
      toast("流程定义信息不完整");
      return;
    }
    // 与 Web 端一致,直接复用列表接口已经返回的表单配置。
    // 流程定义详情接口异常时,也不应阻断填写表单。
    uni.setStorageSync(SELECTED_DEFINITION_KEY, definition);
    uni.navigateTo({
      url: `/pages/bpm/process-instance/create/form?id=${encodeURIComponent(String(definition.id))}`,
    });
  }
 
  onLoad(() => {
    loadDefinitions();
  });
</script>
 
<template>
  <view class="page">
    <view class="search">
      <input v-model="searchName" placeholder="请输入流程名称检索" />
      <text v-if="searchName" @click="searchName = ''">清除</text>
    </view>
 
    <scroll-view class="tabs" scroll-x :show-scrollbar="false">
      <text
        v-for="tab in categoryTabs"
        :key="tab.code"
        class="tab"
        :class="{ active: activeCategory === tab.code }"
        @click="activeCategory = tab.code"
      >
        {{ tab.name }}
      </text>
    </scroll-view>
 
    <scroll-view class="content" scroll-y>
      <view v-if="loadError" class="state error" @click="loadDefinitions">
        {{ loadError }},点击重试
      </view>
      <template v-else-if="groupedDefinitions.length">
        <view v-for="group in groupedDefinitions" :key="group.code" class="group">
          <text class="group-title">{{ group.name }}</text>
          <view
            v-for="item in group.items"
            :key="item.id"
            class="card"
            @click="handleSelect(item)"
          >
            <image v-if="item.icon" class="card-icon" :src="item.icon" mode="aspectFit" />
            <view v-else class="card-icon text-icon">{{ (item.name || '流程').slice(0, 2) }}</view>
            <view class="card-main">
              <text class="card-name">{{ item.name || '未命名流程' }}</text>
              <text v-if="item.description" class="card-desc">{{ item.description }}</text>
            </view>
            <text class="card-arrow">›</text>
          </view>
        </view>
      </template>
      <view v-else class="state">{{ loading ? '加载中...' : '暂无可发起的流程' }}</view>
    </scroll-view>
  </view>
</template>
 
<style lang="scss" scoped>
  .page{height:100vh;display:flex;flex-direction:column;padding:0 30rpx;box-sizing:border-box;overflow:hidden;background:#f3f6fb}
  .search{display:flex;align-items:center;height:80rpx;margin:20rpx 0 12rpx;padding:0 20rpx;background:#fff;border-radius:16rpx;box-shadow:0 8rpx 20rpx rgba(43,69,107,.06)}
  .search input{flex:1;font-size:27rpx}
  .search text{color:var(--color-primary, #1769d8);font-size:24rpx}
  .tabs{flex:none;margin-bottom:14rpx;white-space:nowrap}
  .tab{display:inline-block;padding:10rpx 24rpx;margin-right:14rpx;color:#3d5069;font-size:25rpx;background:#fff;border:1rpx solid #dbe4ef;border-radius:999rpx}
  .tab.active{color:#fff;background:var(--color-primary, #1769d8);border-color:var(--color-primary, #1769d8)}
  .content{height:0;flex:1;padding-bottom:40rpx;box-sizing:border-box}
  .group{padding:20rpx 22rpx;margin-bottom:18rpx;background:#fff;border-radius:16rpx;box-shadow:0 10rpx 24rpx rgba(43,69,107,.08)}
  .group-title{display:block;margin-bottom:14rpx;color:#20334c;font-size:27rpx;font-weight:600}
  .card{display:flex;align-items:center;gap:18rpx;padding:18rpx 0;border-top:1rpx solid #edf1f6}
  .card-icon{flex:none;width:76rpx;height:76rpx;border-radius:14rpx;background:#eef4fd}
  .card-icon.text-icon{display:flex;align-items:center;justify-content:center;color:#fff;font-size:26rpx;background:#7c5cff}
  .card-main{display:flex;min-width:0;flex:1;flex-direction:column;gap:6rpx}
  .card-name{color:#20334c;font-size:28rpx}
  .card-desc{overflow:hidden;color:#8795a8;font-size:23rpx;text-overflow:ellipsis;white-space:nowrap}
  .card-arrow{flex:none;color:#c2cad6;font-size:34rpx;line-height:1}
  .state{padding:60rpx 20rpx;color:#8795a8;font-size:25rpx;text-align:center}
  .state.error{color:#c65050}
</style>