<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>
|