Vue3+Element Plus打造Flowable流程定义管理界面全攻略
2026/9/20 14:54:45 网站建设 项目流程

1. 先聊聊为什么要自己搭一套流程定义管理界面

Flowable作为Java生态里用得最多的工作流引擎之一,后端API能力确实强,但官方自带的管理界面说实话有点"上古"——不管是布局还是交互,放在今天的后台系统里都显得格格不入。很多团队实际落地时,往往需要在一个统一的后台里同时管用户、角色、菜单、权限,还要嵌入流程管理,这时候直接集成Flowable自带UI反而别扭。

所以"用Vue3+Element Plus自己搭一个流程定义管理后台界面"这件事,几乎成了每个做审批流、工单流、办公自动化项目的团队绕不开的活。流程定义(Process Definition)是工作流最核心的元数据,它决定了审批链路长什么样、由哪些节点组成、每个节点的处理人怎么配。一个好用的流程定义管理界面,至少要能完成四件事:

  • 展示所有已部署的流程定义,能区分版本、状态、所属分类
  • 支持按关键字快速检索,毕竟流程多了以后翻页找很痛苦
  • 提供挂起/激活、查看XML、删除等管理操作,而不是每次改状态都要手敲REST接口
  • 为后续流程实例的发起、任务列表、审批记录等模块提供入口锚点

技术选型上,Vue3+Element Plus在目前的前端后台领域基本是"标配级"组合。Vue3的组合式API在处理表格、弹窗、表单这类业务时比Vue2的Options API更清爽,Element Plus的表格组件功能也够强——排序、多选、自定义列、分页都有现成方案,不需要自己造轮子。这篇教程我会从工程初始化讲起,一直到对接Flowable的REST API、完成列表页的搜索分页、状态切换、XML预览等完整功能,尽量把每一步都拆开说清楚。

如果你已经有一个Vue3工程,可以直接跳到第三节看核心数据流和关键代码;如果是纯新手,建议从环境准备一节按顺序过一遍,把基础打牢。

2. 环境准备:Vue3工程初始化与依赖安装

2.1 用Vite创建工程并安装核心依赖

创建Vue3项目我习惯用Vite,启动速度快,热更新也跟手。命令如下:

npm create vite@latest flowable-admin -- --template vue-ts cd flowable-admin npm install npm install element-plus npm install @element-plus/icons-vue npm install axios npm install vue-router@4 npm install pinia

这里有个细节容易踩坑:Element Plus的图标库是独立包@element-plus/icons-vue,很多新手装完Element Plus就直接用<el-icon><Plus /></el-icon>,结果图标不显示,就是忘了装这个包。

如果你的项目需要TypeScript类型支持,建议再装一下unplugin-auto-importunplugin-vue-components这两个Vite插件,可以实现Element Plus组件的按需自动导入,不用在main.ts里全量注册,打包体积也能小一些。

vite.config.ts里配置:

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' export default defineConfig({ plugins: [ vue(), AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ resolvers: [ElementPlusResolver()] }) ] })

不过为了教程简洁直观,后文的示例我统一采用全量引入方式,实际项目中你可以根据团队规范自行选择。全量引入的写法是:

// main.ts import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import * as ElementPlusIconsVue from '@element-plus/icons-vue' import App from './App.vue' const app = createApp(App) for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } app.use(ElementPlus) app.mount('#app')

2.2 为什么前端要单独封装Axios实例

对接Flowable的REST API,建议不要在每个组件里直接axios.get,而是先封装一个统一的请求实例。原因很简单:Flowable的REST接口往往部署在独立的服务端口上,和前端开发服务器(默认5173端口)跨域,你需要在请求层统一处理baseURL、认证头、异常提示。另外后续肯定要接流程实例、任务、模型等多个模块,一个公共实例能减少大量重复代码。

新建src/utils/request.ts

import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/flowable', timeout: 15000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('flowable_token') if (token) { config.headers.Authorization = `Basic ${token}` } return config }) request.interceptors.response.use( response => response.data, error => { ElMessage.error(error.response?.data?.message || '请求失败') return Promise.reject(error) } ) export default request

Flowable的REST API默认支持Basic Auth认证,生产环境通常会用网关统一鉴权,本地开发调试时可以临时在请求头里带上编码后的用户名密码。这里用Basic ${token}的方式,token是btoa('rest-admin:rest-admin')的结果。当然这只是开发期方案,真实项目建议使用更安全的认证方式。

3. 核心数据流:Flowable REST API与前端类型的对齐设计

3.1 流程定义相关接口梳理

Flowable提供了完整的REST接口来做流程定义的管理,我这里只列最核心的几个:

功能请求方法与路径说明
查询流程定义列表GET/repository/process-definitions支持keynameLikecategorylatestVersion等参数
获取流程定义详情GET/repository/process-definitions/{definitionId}返回单条定义内容
挂起/激活PUT/repository/process-definitions/{definitionId}通过body里的suspended字段控制状态
获取XML资源GET/repository/process-definitions/{definitionId}/resourcedata返回流程定义的XML源码
删除流程定义DELETE/repository/process-definitions/{deploymentId}注意是按部署ID删除
流程定义列表(含流程图)GET/repository/process-definitions/{definitionId}/diagram返回PNG/SVG图片

这里有一个很重要的点:列表查询接口支持分页参数startsize,默认返回10条。实际开发中前端的分页组件要同时传两个参数,很多人只传了pageNumpageSize,发现怎么翻都是前10条,就是没看Flowable约定。

3.2 TypeScript类型定义模板

做一个管理界面,强烈建议先把接口返回数据结构用TypeScript定义清楚,后续写组件的时候能省很多事。Flowable的流程定义返回项大致长这样:

export interface FlowableProcessDefinition { id: string key: string name: string description: string version: number category: string deploymentId: string suspended: boolean tenantId: string resourceName: string diagramResourceName: string startFormDefined: boolean } export interface FlowableProcessDefinitionResponse { data: FlowableProcessDefinition[] total: number start: number size: number sort: string order: string }

注意Flowable返回的总数键是total,不是totalCount,和Element Plus分组件的total属性刚好天然对齐,这也是我为什么在分页里直接使用response.total

3.3 前端项目结构规划

单页面应用最怕的就是所有逻辑堆在一个文件里。这里我给出一套适合中小型后台项目的目录组织方式:

src/ api/ processDefinition.ts // 流程定义相关接口封装 types/ flowable.ts // TypeScript类型定义 views/ process/ ProcessDefinitionList.vue // 列表主页面 components/ process/ XmlPreviewDialog.vue // XML预览弹窗 PublishDialog.vue // 挂起/激活确认弹窗 router/ index.ts

这种划分方式的好处是通过API层和管理界面层分离,接口变了只改一处;类型统一管理,多个页面可以用同一套类型。特别是团队协作场景,别人接手你的代码时看到目录结构就能猜到逻辑边界,少走很多弯路。

3.4 接口封装示例

src/api/processDefinition.ts里把前面梳理的REST接口封装成函数:

import request from '@/utils/request' import type { FlowableProcessDefinition, FlowableProcessDefinitionResponse } from '@/types/flowable' export function getProcessDefinitions(params: { key?: string name?: string nameLike?: string latestVersion?: boolean start?: number size?: number sort?: string order?: string }) { return request.get<FlowableProcessDefinitionResponse>('/repository/process-definitions', { params }) } export function setProcessDefinitionSuspended(definitionId: string, suspended: boolean) { return request.put(`/repository/process-definitions/${definitionId}`, { suspended, includeProcessInstances: false }) } export function getProcessDefinitionXml(definitionId: string) { return request.get(`/repository/process-definitions/${definitionId}/resourcedata`, { responseType: 'text' }) } export function deleteProcessDefinition(deploymentId: string) { return request.delete(`/repository/process-definitions/${deploymentId}`) }

includeProcessInstances参数的作用是:挂起流程定义时,要不要同时挂起正在运行的流程实例。默认是false,如果你希望新流程无法发起的同事,老的流程实例还能继续走完,保持这个默认值就行。

4. 列表页完整实现:表格、搜索、分页、状态标签与操作栏

4.1 列表页的整体布局拆解

一个标准的后台列表页,布局上一般包括三个区域:顶部筛选区、中间表格区、底部或表格内的分页区。Element Plus有成熟的栅格布局,我习惯这样安排:

+-------------------------------------------------------+ | 流程名称: [输入框] 流程Key: [输入框] [查询] [重置] | +-------------------------------------------------------+ | 选中项: xxx (批量挂起/激活按钮) | +-------------------------------------------------------+ | 表格: 名称 | KEY | 版本 | 状态 | 分类 | 操作 | +-------------------------------------------------------+ | 分页组件 | +-------------------------------------------------------+

筛选区用el-forminline模式,表格用el-table,分页用el-pagination,这三个组件组合起来基本上没有理解成本,关键是数据流的联动要设计好。

4.2 完整模板代码

基于上面布局,模板部分长这样:

<template> <div class="process-definition-list"> <el-form :inline="true" :model="queryParams" class="filter-bar"> <el-form-item label="流程名称"> <el-input v-model="queryParams.name" placeholder="支持模糊匹配" clearable style="width: 200px" @keyup.enter="handleSearch" /> </el-form-item> <el-form-item label="流程Key"> <el-input v-model="queryParams.key" placeholder="请输入流程Key" clearable style="width: 200px" @keyup.enter="handleSearch" /> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSearch">查询</el-button> <el-button @click="handleReset">重置</el-button> </el-form-item> </el-form> <div class="batch-bar"> <el-button v-if="selection.length > 0" type="warning" plain @click="handleBatchSuspend"> 批量挂起 </el-button> <el-button v-if="selection.length > 0" type="success" plain @click="handleBatchActivate"> 批量激活 </el-button> <span v-if="selection.length > 0" class="selected-tip">已选择 {{ selection.length }} 项</span> </div> <el-table v-loading="loading" :data="tableData" row-key="id" @selection-change="handleSelectionChange" > <el-table-column type="selection" width="50" /> <el-table-column prop="name" label="流程名称" min-width="180" show-overflow-tooltip /> <el-table-column prop="key" label="流程Key" min-width="160" show-overflow-tooltip /> <el-table-column prop="version" label="版本" width="80" align="center"> <template #default="{ row }"> <el-tag>v{{ row.version }}</el-tag> </template> </el-table-column> <el-table-column prop="suspended" label="状态" width="100" align="center"> <template #default="{ row }"> <el-tag :type="row.suspended ? 'danger' : 'success'"> {{ row.suspended ? '已挂起' : '激活中' }} </el-tag> </template> </el-table-column> <el-table-column prop="category" label="分类" min-width="120"> <template #default="{ row }"> {{ row.category || '-' }} </template> </el-table-column> <el-table-column label="操作" width="260" fixed="right"> <template #default="{ row }"> <el-button link type="primary" @click="handleShowXml(row)">XML</el-button> <el-button link type="primary" @click="handleShowDiagram(row)">流程图</el-button> <el-button v-if="!row.suspended" link type="warning" @click="handleToggleSuspend(row, true)" >挂起</el-button> <el-button v-else link type="success" @click="handleToggleSuspend(row, false)" >激活</el-button> <el-button link type="danger" @click="handleDelete(row)">删除</el-button> </template> </el-table-column> <template #empty> <el-empty description="暂无流程定义数据" /> </template> </el-table> <el-pagination class="pagination" :current-page="queryParams.start / queryParams.size + 1" :page-size="queryParams.size" :page-sizes="[10, 20, 50, 100]" :total="total" layout="total, sizes, prev, pager, next, jumper" @current-change="handlePageChange" @size-change="handleSizeChange" /> </div> </template>

4.3 筛选区为什么用nameLike而不是name

这里有一个Flowable的细节:列表查询接口支持namenameLike两种参数。name要求精确匹配,nameLike走的是SQL的LIKE,可以做模糊搜索。因为后台管理界面的搜索框用户习惯是输入关键字就能搜,所以我封装接口时用的是nameLike,而不是name

搜索条件里的流程Key同理,Flowable官方对key支持的是精确匹配,没有keyLike参数。如果你确实需要按Key前缀模糊搜,只能前端先拉全部定义再做过滤,或者在后端代理层做二次查询。实际项目里流程Key一般不会太多,精确匹配其实够用,不需要过度设计。

4.4 事件处理与分页联动

Script部分是这个页面的核心逻辑:

import { ref, reactive, onMounted } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { getProcessDefinitions, setProcessDefinitionSuspended, getProcessDefinitionXml, deleteProcessDefinition } from '@/api/processDefinition' const loading = ref(false) const tableData = ref([]) const total = ref(0) const selection = ref([]) const queryParams = reactive({ name: '', nameLike: '', key: '', latestVersion: false, start: 0, size: 10, sort: 'version', order: 'desc' }) async function fetchList() { loading.value = true try { const params = { nameLike: queryParams.name || undefined, key: queryParams.key || undefined, latestVersion: queryParams.latestVersion, start: queryParams.start, size: queryParams.size, sort: queryParams.sort, order: queryParams.order } const res = await getProcessDefinitions(params) tableData.value = res.data total.value = res.total } finally { loading.value = false } } function handleSearch() { queryParams.start = 0 fetchList() } function handleReset() { queryParams.name = '' queryParams.nameLike = '' queryParams.key = '' queryParams.latestVersion = false queryParams.start = 0 fetchList() } function handlePageChange(page: number) { queryParams.start = (page - 1) * queryParams.size fetchList() } function handleSizeChange(size: number) { queryParams.size = size queryParams.start = 0 fetchList() } function handleSelectionChange(val: any[]) { selection.value = val } async function handleToggleSuspend(row: any, suspended: boolean) { const actionText = suspended ? '挂起' : '激活' await ElMessageBox.confirm(`确定要${actionText}流程 "${row.name}" 吗?`, '操作确认', { type: 'warning' }) await setProcessDefinitionSuspended(row.id, suspended) ElMessage.success(`${actionText}成功`) fetchList() }

分页这里容易搞混:Element Plus的current-change事件返回的是从1开始的页码,但Flowable的start参数是从0开始的偏移量。所以转换关系就是start = (page - 1) * size。这个坑我在第一次对接时踩过,当时直接传了页码进去,导致第二页查出来的数据和第一页重复,排查了半天才发现是偏移量的问题。

4.5 排序参数的最佳实践

Flowable的列表接口支持sortorder两个参数来控制排序。我建议默认按version倒序排列,这样同一个流程Key的最新版本会排在最前面,用户在管理界面第一眼看到的就是最新发布的内容。

这里要说一下latestVersion参数。如果不传latestVersion=true,接口会把这个流程Key下的所有历史版本都返回,也就是说同一个"请假流程"可能同时出现v1、v2、v3三条数据。对管理界面而言,大部分场景下用户想看的是"当前生效的版本",所以我倾向于默认设置latestVersion: true

但反过来,如果你要做的界面侧重于"流程版本追溯",那就不要传这个参数,让历史版本都展示出来,再配合版本号列的排序,就能清楚地看到演变过程。两种模式各有适用场景,看你的业务侧重。

5. 功能增强:XML预览、流程图展示与边界情况处理

5.1 XML预览弹窗实现

流程定义的管理界面,光看表格信息是不够的。排查问题的时候,免不了要看某个版本对应的BPMN XML内容,比如确认某个节点ID是不是写错了,某个条件表达式是不是没配。这时候在界面上直接预览XML比下载文件再打开高效得多。

新建XmlPreviewDialog.vue

<template> <el-dialog v-model="visible" title="流程定义XML" width="70%" top="5vh" destroy-on-close> <div class="xml-container"> <pre>{{ xmlContent }}</pre> </div> </el-dialog> </template> <script setup lang="ts"> import { ref, watch } from 'vue' import { getProcessDefinitionXml } from '@/api/processDefinition' const props = defineProps<{ modelValue: boolean definitionId: string }>() const visible = ref(props.modelValue) const xmlContent = ref('') watch(() => props.modelValue, async (val) => { visible.value = val if (val) { xmlContent.value = await getProcessDefinitionXml(props.definitionId) } }) watch(() => props.definitionId, async (id) => { if (id) { xmlContent.value = await getProcessDefinitionXml(id) } }) </script>

XML内容用一个pre标签包裹,配合white-space: pre-wrapword-break: break-all的CSS,就可以避免长内容溢出容器。加上destroy-on-close属性,每次关闭弹窗时销毁内部状态,下次打开重新拉取,保证数据新鲜。

5.2 流程图的两种展示思路

流程图预览在流程定义管理里属于"锦上添花"的功能,但还是很有用的。运维排查时,用户说"我的流程走到某个节点就卡住了",你光看XML很难直观定位,但配合流程图能迅速判断节点顺序和网关分支。

Flowable提供了两个接口:

  • 如果后端配置了流程图生成,可以通过GET /repository/process-definitions/{definitionId}/diagram拿到图片流
  • 也可以只拿XML,然后前端用bpmn-js渲染

我的建议是:如果只是"看个图",直接走后端接口拿图片最简单。接口返回的是二进制图片,前端请求时要用responseType: 'blob',然后用URL.createObjectURL生成临时地址放到<img>标签里。

export function getProcessDefinitionDiagram(definitionId: string) { return request.get(`/repository/process-definitions/${definitionId}/diagram`, { responseType: 'blob' }) }

组件里调用:

const res = await getProcessDefinitionDiagram(definitionId) const url = URL.createObjectURL(res) diagramUrl.value = url

如果你需要的是"在线编辑"能力,在当前场景下就用不上这个接口了,那属于模型设计器的范畴。我一般推荐的做法是:管理界面看静态图,设计器负责交互编辑,两者定位不一样,不要硬糅在一起。

5.3 删除流程定义的隐藏坑

删除操作有一个很大的坑,大家务必注意:Flowable的删除接口路径是/repository/process-definitions/{deploymentId},注意这里传的是deploymentId,不是流程定义的id

这两个ID在Flowable内部是不同维度的概念:一个部署包(Deployment)可以包含多个流程定义,比如一个部署包里同时放了请假流程XML和报销流程XML。如果传错ID,接口会返回404。所以我在前面接口封装时,特意把删除函数的入参命名为deploymentId,就是希望调用的人能看清楚。

删除确认弹窗里,我建议把部署ID也展示出来,提醒你自己也要注意:

async function handleDelete(row: any) { await ElMessageBox.confirm( `确定要删除流程 "${row.name}" 吗?此操作会删除该部署ID(${row.deploymentId})下的全部定义,请谨慎操作。`, '危险操作', { type: 'error', confirmButtonText: '确认删除', confirmButtonClass: 'el-button--danger' } ) await deleteProcessDefinition(row.deploymentId) ElMessage.success('删除成功') fetchList() }

删除功能还有一个附加注意事项:如果这个流程定义已经被实例化了,Flowable默认可能不允许直接删除。这时候要么在调用时加上级联删除参数,要么在界面上明确提示"该流程存在运行中的实例,无法删除"。我倾向于后者,因为级联删除对生产数据影响太大,不该轻易给用户这个权限。

6. 数据状态与交互反馈:挂起/激活、重复名称与空状态处理

6.1 挂起与激活的前端联动逻辑

挂起和激活算是对流程定义最常用的管理操作了。一个流程定义被挂起后,新发起流程时就不能再用它了,但已存在的流程实例不受影响(前提是includeProcessInstancesfalse)。这在版本迭代时很常用——新版上架,旧版挂起,避免用户再往旧版本里发数据。

前端做到"操作闭环",需要把状态反馈做完整。我在代码里用了三步流程:

  1. 用户点击"挂起"或"激活"按钮
  2. 弹出确认框,告知影响范围(是否影响已运行实例)
  3. 调接口成功后刷新列表,并把后端最新的suspended字段回写到表格

第3步看起来简单,但很多新手会漏掉。接口调用完成后如果不重新拉取列表,页面上的状态标签还是旧值,用户会以为操作没生效,然后又点了一次,导致重复请求。所以每次操作后fetchList()是必须的。

6.2 批量操作的实现细节

批量挂起/激活的逻辑和单条操作类似,只不过要遍历选中项依次调用接口。这里我建议用Promise.all并发处理,而不是for循环串行等待。流程定义数量通常不会太多,并发请求完全在合理范围内。

async function handleBatchSuspend() { const ids = selection.value.map(item => item.id) await ElMessageBox.confirm(`确定要挂起选中的 ${ids.length} 个流程定义吗?`, '批量操作', { type: 'warning' }) await Promise.all(ids.map(id => setProcessDefinitionSuspended(id, true))) ElMessage.success(`已挂起 ${ids.length} 个流程定义`) fetchList() }

Promise.all的好处是请求并行发出,响应时间基本等于最慢的那一个,而不是所有请求之和。缺点是如果其中某个请求失败,整个Promise会reject。对于批量管理操作来说,我认为可以接受,因为失败时用户会看到统一的错误提示,然后重新查询列表确认实际状态。

6.3 空数据和重复数据的展示策略

流程定义列表最常见的两个非理想情况是:查不到数据和存在多个同名定义。

查询不到数据时,Element Plus的el-table默认显示一个"暂无数据"的英文文本,看起来跟整个中文界面不太搭。我习惯用el-empty组件替换默认的空状态插槽,再配一句"没有找到匹配的流程定义,请调整筛选条件"的提示文案。这是个小细节,但对用户感知的提升很明显。

对于同名流程定义,不同版本会同时展示在列表里(如果把latestVersion设置为false的话)。这种情况下容易让人困惑——"到底哪个才是现在能用的?"。我的建议是增加两个辅助标识:

  • 在版本列用"v3"这样的Tag式展示,最新版本额外加一个"最新"标记
  • 在状态列后面增加一个"运行中实例数"字段,让用户能直观看到每个版本被使用的情况
<el-table-column prop="version" label="版本" width="100" align="center"> <template #default="{ row }"> <el-tag :type="isLatestVersion(row) ? 'primary' : 'info'"> v{{ row.version }} </el-tag> </template> </el-table-column>

isLatestVersion可以这样实现:比较当前列表里同Key下版本号最大的那条。前端拿到完整列表数据后,先按Key分组,取每个Key下最大版本,再判断当前行是否等于这个版本,逻辑清晰且性能开销可以忽略。

6.4 接口慢时的加载体验

Flowable的查询接口在大数据量时可能会出现几百毫秒甚至更长的响应时间。为了不让用户感觉"卡死了",我通常在列表加载时开启v-loading,同时加上一段合理的提示。

el-tablev-loading指令会覆盖整个表格区域,显示一个居中旋转的加载动画,体验还挺自然的。但要注意,加载动画必须放在finally里关闭,避免接口异常时遮罩一直转。上面的代码里我已经用try/finally处理了,这是一个行业共识级的最佳实践,希望你能记住。

7. 如果接口联调不通过:常见的跨域、鉴权与部署细节问题

7.1 Vite开发环境的跨域代理配置

我接手过的不少项目里,前端搭好了,页面也写好了,一调Flowable接口就报CORS错误。原因很简单:Flowable服务跑在8080端口,前端开发服务跑在5173端口,浏览器默认拦截跨域请求。

解决方式有两种:

  • 后端开启CORS:在Spring Boot里加一个WebMvcConfigurer,允许前端域名跨域访问
  • 前端Vite代理:在开发环境把/flowable前缀的请求代理到后端地址

我更喜欢第二种,理由是它不需要改后端代码,而且线上部署时前端通常和后端通过Nginx同域访问,开发环境用代理正好能模拟线上同域环境。

Vite的vite.config.ts配置:

export default defineConfig({ server: { port: 5173, proxy: { '/flowable': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/flowable/, '/flowable') } } } })

这里如果你把Flowable部署在/flowable前缀下,rewrite可以不加;如果后端没有统一前缀,就要用rewrite: path => path.replace(/^\/flowable/, '')把前缀去掉。具体看你的网关转发规则。

7.2 Basic Auth在浏览器环境的注意点

Flowable的REST API默认开启了Basic Auth,要求请求头里带Authorization: Basic base64(username:password)

前端要处理这个问题,有两条路:

  1. 在Axios的请求拦截器里统一加上这个Header
  2. 登录时先调用认证接口,拿到Token后存储,后续请求自动携带

如果是内网工具类的管理系统,为了快速上线,我见过不少团队直接在拦截器里放死了一个账号密码的Base64编码。这方便是方便,但安全性很差,强烈不建议在生产环境这么干。折中方案是做一个简单的登录页,登录时把用户名密码编码到Token里存到localStorage,接口调用时再取出来加到Header,至少给了运营人员独立账号,后续接SSO或OAuth2也好扩展。

7.3 生产环境的静态资源部署

前端工程构建后就是一堆静态文件,部署方式很灵活。常见的做法是构建出dist目录,然后交给Nginx托管。

server { listen 80; server_name your-domain.com; root /var/www/flowable-admin/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /flowable/ { proxy_pass http://127.0.0.1:8080/flowable/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

关键点在于try_files指令——因为Vue Router默认使用history模式,直接访问/process-definition这类子路径时,Nginx找不到对应文件,必须回退到index.html,让前端路由接管。

如果你使用的是hash模式(URL里带#),那Nginx配置就不需要用try_files了。两种模式各有优缺点,我倾向history模式,URL更干净,但要求部署方必须配合配置好回退规则。

7.4 Environment变量管理

不同环境(开发、测试、生产)的后端地址通常不一样,我不建议把地址写死在代码里。Vite支持通过.env文件区分环境:

# .env.development VITE_API_BASE_URL=/flowable # .env.production VITE_API_BASE_URL=/flowable

然后在request.ts里读取:

baseURL: import.meta.env.VITE_API_BASE_URL || '/flowable'

这种方式的好处是,开发环境和生产环境如果走不同的域名或路径,只需要维护各自的env文件,不需要改动业务代码。这个操作虽然简单,却是我在多个项目中踩坑后养成的好习惯。

8. 后续拓展:从流程定义到完整工作流平台

流程定义管理界面只是工作流平台的地基。做完这块,你自然会发现还有很多可以延伸的场景,而且核心设计思路是相通的。

流程模型管理:Flowable的模型(Model)是设计期的流程草稿,定义(Definition)是部署后的运行版本。管理界面里通常需要一个"模型列表+在线设计+部署"的操作流。在线设计器的集成方式一般是加两个按钮在模型列表里,一个跳到bpmn-js的独立页面(或弹窗),一个点击后调用部署接口。这块如果将来要做,可以参考同类的Vue3+BPMN组件方案,实现思路不复杂,但细节很多。

流程实例跟踪:做完流程定义管理后,用户最自然而然的需求就是"我想看看某个流程发起后跑到哪一步了"。流程实例的列表查询、流程图高亮、当前节点标注,这些都是围绕Flowable的runtime/process-instances接口展开的。前端需要的组件是画布高亮渲染,通常也是用bpmn-js配合后端返回的当前节点ID来实现。

版本对比:如果流程定义发布了多个版本,用户很关心"v2和v3到底改了哪些内容"。接口上可以用两次resourcedata拿到两个版本的XML,前端用diff库做文本差异展示。虽然XML文本对比不够美观,但定位问题很实用。

权限绑定:后台系统终究要落到权限上。流程定义的查看、挂起、删除这些操作,可以抽象成按钮级别的权限点,在路由守卫里校验,或者在操作按钮上用v-permission之类的自定义指令控制显示。等你把权限模型做完,这个流程定义管理页面就会真正融入整个后台体系。

说回技术选型,Vue3+Element Plus这套组合,应付流程定义管理这个体量是绰绰有余的。核心难点从来不是某个组件不会用,而是你对Flowable REST API的字段和行为理解得透不透。API对齐了,界面再复杂也只是时间问题。

我自己在多个项目里反复用过这套方案,最大的体会是:先把"流程定义列表"这个页面打磨到能在生产环境稳定运行,比一开始就追求流程图在线编辑、模型设计器这些高级功能重要得多。因为流程定义管理是整个工作流后台的入口,它稳定了,后续的功能才有依托。而且这个页面涉及了列表、搜索、分页、状态管理、二次确认、弹窗等多类常见后台场景,做完一遍,基本等于把工作流管理端的通用套路都摸了一遍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询