!11 follow后端发布
* docs: readme * fix: missing formPath * chore: 去除锁定的esbuild版本 * perf: 去除debug组件 * perf: 参数键值 自动高度 * refactor: 代码生成配置页面重构 去除步骤条 * perf: 移除文件 * docs: 文件夹说明 * chore: 移除一些配置项 * chore: 注释优化 * refactor: 移除ele和naive目录 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * perf: request support to set how to return response (#5436) * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * refactor: 登录超时的i18n * fix: requestClient缺失i18n内容 * refactor: 优化oss下载进度提示 * feat: 下载进度loading * fix: antd button icon style (#5421) * feat: oss下载进度(已下载的KB 无法作为进度显示 total返回为null) * fix: 下载文件时(responseType === 'blob')需要判断下载失败(返回json而非二进制)的情况 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * refactor: 新增后跳转到未发布流程 * fix: same name route * chore: 调整为部署json类型 * fix: mouse events ignored on modal loading (#5409) * docs: update docs (#5408) * refactor: 移除已经弃用的方法 * refactor: follow官方handleRangeTimeValue更新 * chore: 删除文件夹(前端路由需要的) * chore: 修改本地路由写法(新版)/新增本地菜单图标 * fix: form update state error before form mounted (#5406) * fix: demos route fixed (#5405) * chore: 不使用基础布局(仅在顶级生效) * feat: modal state locked on submitting (#5401) * chore: 修改zIndex * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * refactor: fix popup component zIndex (#5397) * style: element plus loading style fixed (#5393) * perf: improve fieldMappingTime to support format function (#5392) * fix: hide root route in breadcrumb * feat: support set default props for drawer and modal (#5390) * fix: root router config fixed (#5389) * fix: 修改Vxe默认zIndex为995 解决右上角全屏后modal/drawer(zIndex: 1000)被遮挡 * feat: add `noBasicLayout` in route meta (#5386) * chore: wechat image * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * chore: 改为全局参数配置 去除局部参数 * fix: spinner may stop playing animation after dismiss (#5365) * style: popover bgColor is too close to common (#5364) * docs: version update * docs: changelog * chore: 文件上传 描述 * ci: retry deploy while faild * feat: 文件上传 进度条+提示文字 * feat: 文件上传 进度条 * feat: 上传文件格式说明 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into warmflow * fix: useEcharts return invalid instance (#5360) * feat: popup component support overlay blur effect (#5359) * feat: improve `tippy` demo (#5357) * feat: integrate new component `Tippy` with demo (#5355) * chore: 优化表格图片显示 * perf: add nested modal demo (#5353) * chore: 默认显示右边的滚动条 防止出现滚动条被挤压 * perf: modal and drawer api support chain calls (#5351) * feat: allow close tab when mouse middle button click (#5347) * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into warmflow * refactor: 重构显示total的逻辑 * chore: 调整高度自适应代码 * chore: vxe升级4.10.0版本(锁定) * fix: 添加失效的option * fix: 需要为数组 * fix: locale switching logic correction (#5344) * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into warmflow * chore: 导入类型优化 解决eslint报红 * refactor: type/注释优化 去除大量any * fix: vxeGrid init without search form (#5342) * chore: 锁屏默认false 关闭该功能 * chore: 调整接口 * chore: update deps * fix: primaryColor calculation (#5337) * fix: form valid-error style in naive (#5336) * fix: form `fieldMappingTime` improve and `modelPropName` support (#5335) * fix: code lint * fix: form `fieldMappingTime` is not working (#5333) * chore: 选人组件样式 * fix: download from url triggered twice sometimes (#5319) * chore: 优化代码 * chore: 动态类名(无效)改为style * refactor: 字典相关功能重构 采用一个Map储存字典(之前为两个Map) * feat: 字典支持number类型存储 * chore: 调整样式 * chore: 修改选中border为1px * chore: 字段 * chore: 改为新窗口打开(适用于pdf/图片)而非直接下载 * chore: 更新样式 * chore: 更新字段 * chore: 改为computed * chore: 跳转到未发布流程tab * chore: 优化样式 * docs: readme * fix: name重复导致的404 * Merge branch 'dev' of https://gitee.com/dapppp/ruoyi-plus-vben5 into warmflow * chore: 使用legacy来保证copy的兼容性 * chore: 去除log 添加说明 * chore: 优化代码 * feat: 节点关联/节点独立的切换逻辑 * chore: remove logic * chore: vxe可编辑表格demo * chore: 不允许在按钮下添加数据 * docs: changelog * fix: wrong code * chore: 移除测试菜单 * chore: 优化代码 * refactor: 租户套餐菜单替换为新版 * refactor: 使用新版菜单勾选 * chore: 点行会勾选/取消全部权限 点权限不会勾选行 * chore: 全屏引导+样式优化 * chore: 调整间距 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * feat: useEcharts exports echarts instance#5294 (#5299) * chore: update quick-start.md (#5303) * chore: updateCheckedNumber * refactor: 优化代码 * chore: 优化代码 * chore: 优化样式 * chore: keys依赖于menu 需要先加载menu * chore: 菜单加载完毕再显示 * feat: 新的菜单选择组件(beta) * chore: $t * chore: 测试菜单页面 * chore: 优化代码 * feat: 对ossId回显的支持 * chore: 只获取一次默认密码而非每次打开modal都获取 * fix: vben select placeholder color (#5286) * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * perf: format code with better style (#5283) * chore: 工作流演示站 * fix: sidebar preferences fixed (#5276) * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * fix: breadcrumb setting not valid for `header-sidebar-nav` layout (#5275) * fix: header logo may not be collapsed in `header-sidebar-nav` layout (#5274) * feat: new layout `sidebar nav with full header` (#5270) * feat: drawer close icon placement (#5269) * docs: update dialog and drawer docs * feat: drawer support destroy on close * feat: drawer support `onOpened` & `onClosed` * feat: modal support destroy on close * fix: wrong boolean * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * chore: 调整tab位置 * chore: 删除历史流程 改为tab切换 * fix: header-mixed layout side-menu active (#5265) * feat: header mixed layout (#5263) * chore: release 5.5.2 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * chore: downgrade vue-tsc version * feat: header menu align support (#5256) * chore: update deps * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * chore: add apiSelect remote search demo (#5246) * chore: 审批改为description而非disabled的表单 * chore: 改为ts * chore: 错误的conetnt * refactor: 终止/转办/委托支持填写意见 * chore: 第一次拿到的是readonly的数据 如果需要修改 需要cloneDeep * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * fix: grid form submit button locale switch (#5205) * chore: 调整驳回 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * fix: build error (#5199) * fix: esbuild自动升级导致运行/打包报错 * fix: esbuild自动升级导致运行/打包报错 * chore: 流程定义 激活改为switch * chore: 流程申请支持上传文件 * chore: title 审批通过 * fix: vxeGrid top padding (#5193) * fix: 表格排序翻页会丢失排序参数 * chore: 去除log打印 * chore: 流程监控 待办任务 * chore: 我发起的 * chore: 去除已经移除的菜单页面 * chore: 我的已办 * chore: 页面优化 * chore: 重置tooltip * feat: 我的抄送搜索/优化重复触发的接口 * feat: 流程定义 历史 * chore: 修改分类 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * fix: grid tools in toolbar config not working as expected (#5190) * feat: add `resizable` and `ColPage` component (#5188) * chore: 条件 * chore: break-all * feat: 流程分类 搜索 * chore: 弹窗关闭后仍然显示表单浮层 * chore: 选人组件的样式 * chore: 搜索的样式 * chore: 漏掉的导入 * chore: 最大显示的头像数量 超过显示为省略号头像 * fix: 选人的一些问题 * Merge branch 'warmflow' of https://gitee.com/dapppp/ruoyi-plus-vben5 i… * chore: 没有更多数据了 * fix: sidebar header height (#5183) * chore: 搜索表单布局+申请人 * fix: remove the overlap caused by border-b (#5160) * docs: fix typos (#5169) * fix: resolve eslint errors as well as TS type errors (#5172) * chore: enter提交表单 * chore: 修改文案 * chore: 默认全部展开 * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * feat: page content class override (#5179) * fix: sidebar style on focus (#5178) * fix: 抄送选人 最右侧已选中删除item无效 * feat: 复制 * chore: 昵称过长的显示 * chore: 默认选中第一个 * chore: 修改relative位置 * chore: 搜索 * feat: 我的待办 - 搜索条件 * chore: 流程监控 - 待办任务页面的id不唯一 改为前端处理 * feat: 修改办理人 * chore: 流程干预 - 加签/减签 * chore: avatar大小 * chore: 抄送需要手动添加createByName * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * chore: 审批通过 抄送 * feat: 流程实例-流程预览 * chore: spell * chore: clientid * chore: 分类条件 * chore: 修改办理人 * chore: 更改postMessage参数 * chore: 内嵌iframe高度根据表单高度调整 * chore: 流程详情 * feat: 抄送选择 * chore: 调整分类树 * fix: user homePath no effect sometimes (#5166) * feat: form compact mode support (#5165) * fix: form auto submit no effect when showDefaultActions is false (#5163) * chore: 修改width * feat: 待办任务 * feat: 我的抄送 * chore: 流程定义 样式 * chore: 退回后重新申请 * chore: 请假申请布局 * chore: 请假申请-并行会签网关 * chore: 分类去除根目录 * chore: 详情modal(未完成) * chore: 请假申请根据不同状态显示按钮 * chore: 流程删除/撤销 * chore: 审批完成后刷新当前页 * feat: 选人组件(未完成) 加签减签 * docs: fix docs-link and add `EllipsisText` docs (#5158) * chore: 新窗口打开文件 * chore: 审批通过 * chore: 使用useEventListener替换原生 * chore: 字段错误 * chore: iframe通信 加载完毕后才显示表单 解决卡顿问题 * chore: 审批终止/驳回 * chore: 附件图标 * chore: process_running显示按钮 * chore: label错误 * chore: 保存的事件 * chore: 需要加上clientId * Merge branch 'main' of https://github.com/vbenjs/vue-vben-admin into dev * feat: form `colon` support (#5156) * chore: 完善请假申请 * feat: improve code login demo (#5154) * chore: 客户端管理 行高自适应 * chore: 内嵌表单的路径 * chore: 修改avatar背景色 * chore: 注释 * chore: activePath * chore: leave表单 * chore: 修改请假demo路径 * chore: categoryId * chore: 我的已办 * chore: 我发起的 * chore: loading * chore: 历史版本 * chore: 完善task api * chore: 隐藏'菜单加载中' * chore: missing import * feat: add demo for modify menu badge data * chore: 流程实例 * chore: 审批附件 * chore: 我的待办 提取公共组件 * chore: 流程部署 * chore: 新增/编辑/导出xml * chore: 流程定义(除历史版本) * feat: `autoActivateChild` support more layout mode (#5148) * feat: auto activate subMenu on select root menu (#5147) * fix: `disabledOnChangeListener` not work in form (#5146) * fix: login expired modal z-index (#5145) * feat: user-dropdown support `hover` trigger (#5143) * fix: pinInput value synchronous update (#5142) * fix: vxeGrid default sort data no effect in first query (#5141) * fix: vscode debug profile (#5140) * fix: form component events bind (#5137) * chore: 在线用户样式 开启虚拟滚动 * chore: 去掉个人中心 在线设备的分页 * chore: 去掉在线用户的分页 * chore: changelog * refactor: 获取字典的方法 提取公共函数 减少冗余代码 * fix: element plus validate failed style (#5130) * chore: 使用私有桶的提示 * feat: tabbar support mouse wheel vertical (#5129) * fix: form support `disabledOnInputListener` (#5127) * fix: form submission not appropriate (#5126) * Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev * chore: release 5.5.1 * feat: table search form visible control (#5121) * chore: 需要隐藏菜单 * chore: 我的待办 & 请假 * chore: 流程定义(未完成) * chore: 流程定义(开发中) * Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev * Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev * Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev * chore: version * chore: 锁定vxe-table版本 4.9.8版本存在样式问题 * chore: 暂时锁定cspell版本 * refactor: 由于不能输入 需要使用watch监听 * chore: https://gitee.com/dapppp/ruoyi-plus-vben5/issues/IB7ANL * chore: 移除冗余代码 * chore: 组件卸载时移除emitter * fix: the route path did not synchronize with the page (#4947) * style: typo (#4948) * chore: 替换为commonDownloadExcel * fix: 左边部门树错误emit导致会调用两次列表api * chore: label样式 * chore: 改为Textarea * chore: 滚动条宽度 * chore: 审批样式 * chore: 部门及以下或本人数据权限 * Merge branch 'main' of https://gitee.com/dapppp/ruoyi-plus-vben5 into dev * chore: 个人中心强退设备接口路径
This commit is contained in:
@@ -3,7 +3,12 @@ import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { cloneDeep, getPopupContainer, listToTree } from '@vben/utils';
|
||||
import {
|
||||
addFullName,
|
||||
cloneDeep,
|
||||
getPopupContainer,
|
||||
listToTree,
|
||||
} from '@vben/utils';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
@@ -40,25 +45,20 @@ const [BasicForm, formApi] = useVbenForm({
|
||||
|
||||
async function setupCategorySelect() {
|
||||
const listData = await categoryList();
|
||||
let treeData = listToTree(listData, {
|
||||
id: 'id',
|
||||
const treeData = listToTree(listData, {
|
||||
id: 'categoryId',
|
||||
pid: 'parentId',
|
||||
});
|
||||
treeData = [
|
||||
{
|
||||
categoryName: '根目录',
|
||||
id: 0,
|
||||
children: treeData,
|
||||
},
|
||||
];
|
||||
addFullName(treeData, 'categoryName', ' / ');
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'parentId',
|
||||
componentProps: {
|
||||
treeData,
|
||||
treeLine: { showLeafIcon: false },
|
||||
fieldNames: { label: 'categoryName', value: 'id' },
|
||||
fieldNames: { label: 'categoryName', value: 'categoryId' },
|
||||
treeDefaultExpandAll: true,
|
||||
treeNodeLabelProp: 'fullName',
|
||||
getPopupContainer,
|
||||
},
|
||||
},
|
||||
@@ -120,7 +120,11 @@ async function handleCancel() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal :close-on-click-modal="false" :title="title">
|
||||
<BasicModal
|
||||
:close-on-click-modal="false"
|
||||
:title="title"
|
||||
class="min-h-[500px]"
|
||||
>
|
||||
<BasicForm />
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
@@ -21,11 +21,7 @@ export const columns: VxeGridProps['columns'] = [
|
||||
treeNode: true,
|
||||
},
|
||||
{
|
||||
field: 'categoryCode',
|
||||
title: '分类编码',
|
||||
},
|
||||
{
|
||||
field: 'sortNum',
|
||||
field: 'orderNum',
|
||||
title: '排序',
|
||||
},
|
||||
{
|
||||
@@ -39,8 +35,8 @@ export const columns: VxeGridProps['columns'] = [
|
||||
|
||||
export const modalSchema: FormSchemaGetter = () => [
|
||||
{
|
||||
label: 'id',
|
||||
fieldName: 'id',
|
||||
label: 'categoryId',
|
||||
fieldName: 'categoryId',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
@@ -51,7 +47,7 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
fieldName: 'parentId',
|
||||
label: '父级分类',
|
||||
rules: 'required',
|
||||
defaultValue: 0,
|
||||
defaultValue: 100,
|
||||
component: 'TreeSelect',
|
||||
},
|
||||
{
|
||||
@@ -61,15 +57,8 @@ export const modalSchema: FormSchemaGetter = () => [
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'categoryCode',
|
||||
label: '分类编码',
|
||||
component: 'Input',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'sortNum',
|
||||
fieldName: 'orderNum',
|
||||
label: '排序',
|
||||
component: 'InputNumber',
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
|
@@ -56,11 +56,11 @@ const gridOptions: VxeGridProps = {
|
||||
gt: 0,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
keyField: 'categoryId',
|
||||
},
|
||||
treeConfig: {
|
||||
parentField: 'parentId',
|
||||
rowField: 'id',
|
||||
rowField: 'categoryId',
|
||||
transform: true,
|
||||
},
|
||||
// 表格全局唯一表示 保存列配置需要用到
|
||||
@@ -73,17 +73,17 @@ const [CategoryModal, modalApi] = useVbenModal({
|
||||
});
|
||||
|
||||
function handleAdd(row?: Recordable<any>) {
|
||||
modalApi.setData({ parentId: row?.id });
|
||||
modalApi.setData({ parentId: row?.categoryId });
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
async function handleEdit(row: Recordable<any>) {
|
||||
modalApi.setData({ id: row.id });
|
||||
modalApi.setData({ id: row.categoryId });
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
async function handleDelete(row: Recordable<any>) {
|
||||
await categoryRemove(row.id);
|
||||
await categoryRemove(row.categoryId);
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
|
135
apps/web-antd/src/views/workflow/components/apply-modal.vue
Normal file
135
apps/web-antd/src/views/workflow/components/apply-modal.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<!-- 流程发起(启动)的弹窗 -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CompleteTaskReqData } from '#/api/workflow/task/model';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { completeTask } from '#/api/workflow/task';
|
||||
|
||||
import { CopyComponent } from '.';
|
||||
|
||||
interface Emits {
|
||||
/**
|
||||
* 完成
|
||||
*/
|
||||
complete: [];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
interface ModalProps {
|
||||
taskId: string;
|
||||
taskVariables: Record<string, any>;
|
||||
variables?: any; // 这个干啥的
|
||||
}
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '流程发起',
|
||||
fullscreenButton: false,
|
||||
onConfirm: handleSubmit,
|
||||
});
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-2',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 100,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'messageType',
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '站内信', value: '1', disabled: true },
|
||||
{ label: '邮件', value: '2' },
|
||||
{ label: '短信', value: '3' },
|
||||
],
|
||||
},
|
||||
label: '通知方式',
|
||||
defaultValue: ['1'],
|
||||
},
|
||||
{
|
||||
fieldName: 'attachment',
|
||||
component: 'FileUpload',
|
||||
componentProps: {
|
||||
resultField: 'ossId',
|
||||
maxNumber: 10,
|
||||
maxSize: 20,
|
||||
accept: [
|
||||
'png',
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'doc',
|
||||
'docx',
|
||||
'xlsx',
|
||||
'xls',
|
||||
'ppt',
|
||||
'pdf',
|
||||
],
|
||||
},
|
||||
defaultValue: [],
|
||||
label: '附件上传',
|
||||
formItemClass: 'items-start',
|
||||
},
|
||||
{
|
||||
fieldName: 'flowCopyList',
|
||||
component: 'Input',
|
||||
defaultValue: [],
|
||||
label: '抄送人',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
const { messageType, flowCopyList, attachment } = cloneDeep(
|
||||
await formApi.getValues(),
|
||||
);
|
||||
const { taskId, taskVariables, variables } =
|
||||
modalApi.getData() as ModalProps;
|
||||
// 需要转换数据 抄送人员
|
||||
const flowCCList = (flowCopyList as Array<any>).map((item) => ({
|
||||
userId: item.userId,
|
||||
userName: item.nickName,
|
||||
}));
|
||||
const requestData = {
|
||||
fileId: attachment.join(','),
|
||||
messageType,
|
||||
flowCopyList: flowCCList,
|
||||
taskId,
|
||||
taskVariables,
|
||||
variables,
|
||||
} as CompleteTaskReqData;
|
||||
await completeTask(requestData);
|
||||
modalApi.close();
|
||||
emit('complete');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<BasicForm>
|
||||
<template #flowCopyList="slotProps">
|
||||
<CopyComponent v-model:user-list="slotProps.modelValue" />
|
||||
</template>
|
||||
</BasicForm>
|
||||
</BasicModal>
|
||||
</template>
|
@@ -1,51 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { Avatar, Descriptions, DescriptionsItem, Tag } from 'ant-design-vue';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
endTime: string;
|
||||
startTime: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
status: string;
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { VbenAvatar } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { Descriptions, DescriptionsItem, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { getDiffTimeString } from './helper';
|
||||
|
||||
interface Props extends TaskInfo {
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{ info: Props }>(), {});
|
||||
const props = withDefaults(defineProps<{ info: Props; rowKey?: string }>(), {
|
||||
rowKey: 'id',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ click: [string] }>();
|
||||
|
||||
/**
|
||||
* TODO: 这里要优化 事件没有用到
|
||||
*/
|
||||
function handleClick() {
|
||||
emit('click', props.info.id);
|
||||
const idKey = props.rowKey as keyof TaskInfo;
|
||||
emit('click', props.info[idKey]);
|
||||
}
|
||||
|
||||
const diffUpdateTimeString = computed(() => {
|
||||
return getDiffTimeString(props.info.updateTime);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'border-primary': info.active,
|
||||
'border-[2px]': info.active,
|
||||
}"
|
||||
class="cursor-pointer rounded-lg border-[1px] border-solid p-3 transition-shadow duration-300 ease-in-out hover:shadow-lg"
|
||||
@click.stop="handleClick"
|
||||
>
|
||||
<Descriptions :column="1" :title="info.title" size="middle">
|
||||
<Descriptions :column="1" :title="info.flowName" size="middle">
|
||||
<template #extra>
|
||||
<Tag color="warning">审批中</Tag>
|
||||
</template>
|
||||
<DescriptionsItem label="描述">{{ info.desc }}</DescriptionsItem>
|
||||
<DescriptionsItem label="开始时间">{{ info.startTime }}</DescriptionsItem>
|
||||
<DescriptionsItem label="结束时间">{{ info.endTime }}</DescriptionsItem>
|
||||
</Descriptions>
|
||||
<div class="flex items-center justify-between text-[14px]">
|
||||
<div class="flex items-center gap-1">
|
||||
<Avatar
|
||||
size="small"
|
||||
src="https://plus.dapdap.top/minio-server/plus/2024/11/21/925ed278e2d441beb7f695b41e13c4dd.jpg"
|
||||
<component
|
||||
:is="renderDict(info.flowStatus, DictEnum.WF_BUSINESS_STATUS)"
|
||||
/>
|
||||
<span class="opacity-50">疯狂的牛子Li</span>
|
||||
</template>
|
||||
<DescriptionsItem label="当前任务">
|
||||
<div class="font-bold">{{ info.nodeName }}</div>
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="提交时间">
|
||||
{{ info.createTime }}
|
||||
</DescriptionsItem>
|
||||
<!-- <DescriptionsItem label="更新时间">
|
||||
{{ info.updateTime }}
|
||||
</DescriptionsItem> -->
|
||||
</Descriptions>
|
||||
<div class="flex w-full items-center justify-between text-[14px]">
|
||||
<div class="flex items-center gap-1 overflow-hidden whitespace-nowrap">
|
||||
<VbenAvatar
|
||||
:alt="info.createByName"
|
||||
class="bg-primary size-[24px] rounded-full text-[10px] text-white"
|
||||
src=""
|
||||
/>
|
||||
<span class="overflow-hidden text-ellipsis opacity-50">
|
||||
{{ info.createByName }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-nowrap opacity-50">
|
||||
<Tooltip placement="top" :title="`更新时间: ${info.updateTime}`">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[mdi--clock-outline] size-[16px]"></span>
|
||||
<span>{{ diffUpdateTimeString }}前更新</span>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="opacity-50">处理时间: 2022-01-01</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@@ -0,0 +1,26 @@
|
||||
<!-- 审批终止 Modal弹窗的content属性专用 用于填写审批意见 -->
|
||||
<script setup lang="ts">
|
||||
import { Textarea } from 'ant-design-vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ApprovalContent',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
defineProps<{ description: string; value: string }>();
|
||||
|
||||
defineEmits<{ 'update:value': [string] }>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div>{{ description }}</div>
|
||||
<Textarea
|
||||
:allow-clear="true"
|
||||
:auto-size="true"
|
||||
:value="value"
|
||||
placeholder="审批意见(可选)"
|
||||
@change="(e) => $emit('update:value', e.target.value!)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
154
apps/web-antd/src/views/workflow/components/approval-modal.vue
Normal file
154
apps/web-antd/src/views/workflow/components/approval-modal.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<!-- 审批同意的弹窗 -->
|
||||
<!-- 审批驳回窗口 -->
|
||||
<script setup lang="ts">
|
||||
import type { CompleteTaskReqData } from '#/api/workflow/task/model';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import { omit } from 'lodash-es';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { completeTask } from '#/api/workflow/task';
|
||||
|
||||
import { CopyComponent } from '.';
|
||||
|
||||
const emit = defineEmits<{ complete: [] }>();
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-2',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 100,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'taskId',
|
||||
component: 'Input',
|
||||
label: '任务ID',
|
||||
dependencies: {
|
||||
show: false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'messageType',
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '站内信', value: '1', disabled: true },
|
||||
{ label: '邮件', value: '2' },
|
||||
{ label: '短信', value: '3' },
|
||||
],
|
||||
},
|
||||
label: '通知方式',
|
||||
defaultValue: ['1'],
|
||||
},
|
||||
{
|
||||
fieldName: 'attachment',
|
||||
component: 'FileUpload',
|
||||
componentProps: {
|
||||
resultField: 'ossId',
|
||||
maxNumber: 10,
|
||||
maxSize: 20,
|
||||
accept: [
|
||||
'png',
|
||||
'jpg',
|
||||
'jpeg',
|
||||
'doc',
|
||||
'docx',
|
||||
'xlsx',
|
||||
'xls',
|
||||
'ppt',
|
||||
'pdf',
|
||||
],
|
||||
},
|
||||
defaultValue: [],
|
||||
label: '附件上传',
|
||||
formItemClass: 'items-start',
|
||||
},
|
||||
{
|
||||
fieldName: 'flowCopyList',
|
||||
component: 'Input',
|
||||
defaultValue: [],
|
||||
label: '抄送人',
|
||||
},
|
||||
{
|
||||
fieldName: 'message',
|
||||
component: 'Textarea',
|
||||
label: '审批意见',
|
||||
formItemClass: 'items-baseline',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
interface ModalProps {
|
||||
taskId: string;
|
||||
}
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '审批通过',
|
||||
fullscreenButton: false,
|
||||
class: 'min-h-[365px]',
|
||||
onConfirm: handleSubmit,
|
||||
async onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
await formApi.resetForm();
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const { taskId } = modalApi.getData() as ModalProps;
|
||||
await formApi.setFieldValue('taskId', taskId);
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
// 需要转换数据 抄送人员
|
||||
const flowCopyList = (data.flowCopyList as Array<any>).map((item) => ({
|
||||
userId: item.userId,
|
||||
userName: item.nickName,
|
||||
}));
|
||||
const requestData = {
|
||||
...omit(data, ['attachment']),
|
||||
fileId: data.attachment.join(','),
|
||||
taskVariables: {},
|
||||
variables: {},
|
||||
flowCopyList,
|
||||
} as CompleteTaskReqData;
|
||||
await completeTask(requestData);
|
||||
modalApi.close();
|
||||
emit('complete');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<BasicForm>
|
||||
<template #flowCopyList="slotProps">
|
||||
<CopyComponent v-model:user-list="slotProps.modelValue" />
|
||||
</template>
|
||||
</BasicForm>
|
||||
</BasicModal>
|
||||
</template>
|
520
apps/web-antd/src/views/workflow/components/approval-panel.vue
Normal file
520
apps/web-antd/src/views/workflow/components/approval-panel.vue
Normal file
@@ -0,0 +1,520 @@
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/core/user';
|
||||
import type { FlowInfoResponse } from '#/api/workflow/instance/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Fallback, useVbenModal, VbenAvatar } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { CopyOutlined } from '@ant-design/icons-vue';
|
||||
import { useClipboard, useEventListener } from '@vueuse/core';
|
||||
import {
|
||||
Card,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Menu,
|
||||
MenuItem,
|
||||
message,
|
||||
Modal,
|
||||
Skeleton,
|
||||
Space,
|
||||
TabPane,
|
||||
Tabs,
|
||||
} from 'ant-design-vue';
|
||||
import { isObject } from 'lodash-es';
|
||||
|
||||
import {
|
||||
cancelProcessApply,
|
||||
deleteByInstanceIds,
|
||||
flowInfo,
|
||||
} from '#/api/workflow/instance';
|
||||
import {
|
||||
getTaskByTaskId,
|
||||
taskOperation,
|
||||
terminationTask,
|
||||
updateAssignee,
|
||||
} from '#/api/workflow/task';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import {
|
||||
approvalModal,
|
||||
approvalRejectionModal,
|
||||
ApprovalTimeline,
|
||||
flowInterfereModal,
|
||||
} from '.';
|
||||
import { approveWithReasonModal } from './helper';
|
||||
import userSelectModal from './user-select-modal.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'ApprovalPanel',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
const props = defineProps<{ task?: TaskInfo; type: ApprovalType }>();
|
||||
|
||||
/**
|
||||
* 下面按钮点击后会触发的事件
|
||||
*/
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
const currentTask = ref<TaskInfo>();
|
||||
/**
|
||||
* 是否显示 加签/减签操作
|
||||
*/
|
||||
const showMultiActions = computed(() => {
|
||||
if (!currentTask.value) {
|
||||
return false;
|
||||
}
|
||||
if (Number(currentTask.value.nodeRatio) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
/**
|
||||
* myself 我发起的
|
||||
* readonly 只读 只用于查看
|
||||
* approve 审批
|
||||
* admin 流程监控 - 待办任务使用
|
||||
*/
|
||||
type ApprovalType = 'admin' | 'approve' | 'myself' | 'readonly';
|
||||
const showFooter = computed(() => {
|
||||
if (props.type === 'readonly') {
|
||||
return false;
|
||||
}
|
||||
// 我发起的 && [已完成, 已作废] 不显示
|
||||
if (
|
||||
props.type === 'myself' &&
|
||||
['finish', 'invalid'].includes(props.task?.flowStatus ?? '')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const currentFlowInfo = ref<FlowInfoResponse>();
|
||||
/**
|
||||
* card的loading状态
|
||||
*/
|
||||
const loading = ref(false);
|
||||
const iframeLoaded = ref(false);
|
||||
const iframeHeight = ref(300);
|
||||
useEventListener('message', (event) => {
|
||||
const data = event.data as { [key: string]: any; type: string };
|
||||
if (!isObject(data)) return;
|
||||
/**
|
||||
* iframe通信 加载完毕后才显示表单 解决卡顿问题
|
||||
*/
|
||||
if (data.type === 'mounted') {
|
||||
iframeLoaded.value = true;
|
||||
}
|
||||
/**
|
||||
* 高度与表单高度保持一致
|
||||
*/
|
||||
if (data.type === 'height') {
|
||||
const height = data.height;
|
||||
iframeHeight.value = height;
|
||||
}
|
||||
});
|
||||
|
||||
async function handleLoadInfo(task: TaskInfo | undefined) {
|
||||
try {
|
||||
if (!task) return null;
|
||||
loading.value = true;
|
||||
iframeLoaded.value = false;
|
||||
const resp = await flowInfo(task.businessId);
|
||||
currentFlowInfo.value = resp;
|
||||
|
||||
const taskResp = await getTaskByTaskId(props.task!.id);
|
||||
currentTask.value = taskResp;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.task, handleLoadInfo);
|
||||
|
||||
onUnmounted(() => (currentFlowInfo.value = undefined));
|
||||
|
||||
// 进行中 可以撤销
|
||||
const revocable = computed(() => props.task?.flowStatus === 'waiting');
|
||||
async function handleCancel() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要撤销该申请吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await cancelProcessApply({
|
||||
businessId: props.task!.businessId,
|
||||
message: '申请人撤销流程!',
|
||||
});
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否可编辑/删除
|
||||
*/
|
||||
const editableAndRemoveable = computed(() => {
|
||||
if (!props.task) {
|
||||
return false;
|
||||
}
|
||||
return ['back', 'cancel', 'draft'].includes(props.task.flowStatus);
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
function handleEdit() {
|
||||
const path = props.task?.formPath;
|
||||
if (path) {
|
||||
router.push({ path, query: { id: props.task!.businessId } });
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemove() {
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定删除该申请吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await deleteByInstanceIds([props.task!.id]);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批驳回
|
||||
*/
|
||||
const [RejectionModal, rejectionModalApi] = useVbenModal({
|
||||
connectedComponent: approvalRejectionModal,
|
||||
});
|
||||
function handleRejection() {
|
||||
rejectionModalApi.setData({
|
||||
taskId: props.task?.id,
|
||||
definitionId: props.task?.definitionId,
|
||||
nodeCode: props.task?.nodeCode,
|
||||
});
|
||||
rejectionModalApi.open();
|
||||
}
|
||||
/**
|
||||
* 审批终止
|
||||
*/
|
||||
function handleTermination() {
|
||||
approveWithReasonModal({
|
||||
title: '审批终止',
|
||||
description: '确定终止当前审批流程吗?',
|
||||
onOk: async (reason) => {
|
||||
await terminationTask({ taskId: props.task!.id, comment: reason });
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批通过
|
||||
*/
|
||||
const [ApprovalModal, approvalModalApi] = useVbenModal({
|
||||
connectedComponent: approvalModal,
|
||||
});
|
||||
function handleApproval() {
|
||||
approvalModalApi.setData({ taskId: props.task?.id });
|
||||
approvalModalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: 1提取公共函数 2原版是可以填写意见的(message参数)
|
||||
*/
|
||||
|
||||
/**
|
||||
* 委托
|
||||
*/
|
||||
const [DelegationModal, delegationModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleDelegation(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
approveWithReasonModal({
|
||||
title: '委托',
|
||||
description: `确定委托给[${current?.nickName}]吗?`,
|
||||
onOk: async (reason) => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userId: current!.userId, message: reason },
|
||||
'delegateTask',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 转办
|
||||
*/
|
||||
const [TransferModal, transferModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleTransfer(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
approveWithReasonModal({
|
||||
title: '转办',
|
||||
description: `确定转办给[${current?.nickName}]吗?`,
|
||||
onOk: async (reason) => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userId: current!.userId, message: reason },
|
||||
'transferTask',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [AddSignatureModal, addSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleAddSignature(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认加签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation({ taskId: props.task!.id, userIds }, 'addSignature');
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [ReductionSignatureModal, reductionSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleReductionSignature(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认减签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: props.task!.id, userIds },
|
||||
'reductionSignature',
|
||||
);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 流程干预
|
||||
const [FlowInterfereModal, flowInterfereModalApi] = useVbenModal({
|
||||
connectedComponent: flowInterfereModal,
|
||||
});
|
||||
function handleFlowInterfere() {
|
||||
flowInterfereModalApi.setData({ taskId: props.task?.id });
|
||||
flowInterfereModalApi.open();
|
||||
}
|
||||
|
||||
// 修改办理人
|
||||
const [UpdateAssigneeModal, updateAssigneeModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleUpdateAssignee(userList: User[]) {
|
||||
if (userList.length === 0) return;
|
||||
const current = userList[0];
|
||||
if (!current) return;
|
||||
Modal.confirm({
|
||||
title: '修改办理人',
|
||||
content: `确定修改办理人为${current?.nickName}吗?`,
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await updateAssignee([props.task!.id], current.userId);
|
||||
emit('reload');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 不加legacy在本地开发没有问题
|
||||
* 打包后在一些设备会无法复制 使用legacy来保证兼容性
|
||||
*/
|
||||
const { copy } = useClipboard({ legacy: true });
|
||||
async function handleCopy(text: string) {
|
||||
await copy(text);
|
||||
message.success('复制成功');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card
|
||||
v-if="task"
|
||||
:body-style="{ overflowY: 'auto', height: '100%' }"
|
||||
:loading="loading"
|
||||
class="thin-scrollbar flex-1 overflow-y-hidden"
|
||||
size="small"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2">
|
||||
<div>编号: {{ task.id }}</div>
|
||||
<CopyOutlined class="cursor-pointer" @click="handleCopy(task.id)" />
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-button size="small" @click="() => handleLoadInfo(task)">
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[material-symbols--refresh] size-24px"></span>
|
||||
</div>
|
||||
</a-button>
|
||||
</template>
|
||||
<div class="flex flex-col gap-5 p-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-2xl font-bold">{{ task.flowName }}</div>
|
||||
<div>
|
||||
<component
|
||||
:is="renderDict(task.flowStatus, DictEnum.WF_BUSINESS_STATUS)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<VbenAvatar
|
||||
:alt="task.createByName"
|
||||
class="bg-primary size-[28px] rounded-full text-white"
|
||||
src=""
|
||||
/>
|
||||
<span>{{ task.createByName }}</span>
|
||||
<div class="flex items-center opacity-50">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[bxs--category-alt] size-[16px]"></span>
|
||||
流程分类: {{ task.categoryName }}
|
||||
</div>
|
||||
<Divider type="vertical" />
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[mdi--clock-outline] size-[16px]"></span>
|
||||
提交时间: {{ task.createTime }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tabs v-if="currentFlowInfo" class="flex-1">
|
||||
<TabPane key="1" tab="审批详情">
|
||||
<div class="h-fulloverflow-y-auto">
|
||||
<!-- 约定${task.formPath}/frame 为内嵌表单 用于展示 需要在本地路由添加 -->
|
||||
<iframe
|
||||
v-show="iframeLoaded"
|
||||
:src="`${task.formPath}/iframe?readonly=true&id=${task.businessId}`"
|
||||
:style="{ height: `${iframeHeight}px` }"
|
||||
class="w-full"
|
||||
></iframe>
|
||||
<Skeleton v-show="!iframeLoaded" :paragraph="{ rows: 6 }" active />
|
||||
<Divider />
|
||||
<ApprovalTimeline :list="currentFlowInfo.list" />
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane key="2" tab="审批流程图">
|
||||
<img
|
||||
:src="`data:image/png;base64,${currentFlowInfo.image}`"
|
||||
class="rounded-lg border"
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
<!-- 固定底部 -->
|
||||
<div class="h-[57px]"></div>
|
||||
<div
|
||||
v-if="showFooter"
|
||||
class="border-t-solid bg-background absolute bottom-0 left-0 w-full border-t-[1px] p-3"
|
||||
>
|
||||
<div class="flex justify-end">
|
||||
<Space v-if="type === 'myself'">
|
||||
<a-button
|
||||
v-if="revocable"
|
||||
danger
|
||||
type="primary"
|
||||
@click="handleCancel"
|
||||
>
|
||||
撤销申请
|
||||
</a-button>
|
||||
<a-button v-if="editableAndRemoveable" @click="handleEdit">
|
||||
重新编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
v-if="editableAndRemoveable"
|
||||
danger
|
||||
type="primary"
|
||||
@click="handleRemove"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</Space>
|
||||
<Space v-if="type === 'approve'">
|
||||
<a-button type="primary" @click="handleApproval">通过</a-button>
|
||||
<a-button danger type="primary" @click="handleTermination">
|
||||
终止
|
||||
</a-button>
|
||||
<a-button danger type="primary" @click="handleRejection">
|
||||
驳回
|
||||
</a-button>
|
||||
<Dropdown
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<MenuItem key="1" @click="() => delegationModalApi.open()">
|
||||
委托
|
||||
</MenuItem>
|
||||
<MenuItem key="2" @click="() => transferModalApi.open()">
|
||||
转办
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions"
|
||||
key="3"
|
||||
@click="() => addSignatureModalApi.open()"
|
||||
>
|
||||
加签
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
v-if="showMultiActions"
|
||||
key="4"
|
||||
@click="() => reductionSignatureModalApi.open()"
|
||||
>
|
||||
减签
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</template>
|
||||
<a-button> 其他 </a-button>
|
||||
</Dropdown>
|
||||
<ApprovalModal @complete="$emit('reload')" />
|
||||
<RejectionModal @complete="$emit('reload')" />
|
||||
<DelegationModal mode="single" @finish="handleDelegation" />
|
||||
<TransferModal mode="single" @finish="handleTransfer" />
|
||||
<AddSignatureModal mode="multiple" @finish="handleAddSignature" />
|
||||
<ReductionSignatureModal
|
||||
mode="multiple"
|
||||
@finish="handleReductionSignature"
|
||||
/>
|
||||
</Space>
|
||||
<Space v-if="type === 'admin'">
|
||||
<a-button @click="handleFlowInterfere"> 流程干预 </a-button>
|
||||
<a-button @click="() => updateAssigneeModalApi.open()">
|
||||
修改办理人
|
||||
</a-button>
|
||||
<FlowInterfereModal @complete="$emit('reload')" />
|
||||
<UpdateAssigneeModal mode="single" @finish="handleUpdateAssignee" />
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Fallback v-else title="点击左侧选择" />
|
||||
</template>
|
@@ -0,0 +1,131 @@
|
||||
<!-- 审批驳回窗口 -->
|
||||
<script setup lang="ts">
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { cloneDeep, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { backProcess, getBackTaskNode } from '#/api/workflow/task';
|
||||
|
||||
const emit = defineEmits<{ complete: [] }>();
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-2',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 100,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'taskId',
|
||||
component: 'Input',
|
||||
label: '任务ID',
|
||||
dependencies: {
|
||||
show: false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'messageType',
|
||||
component: 'CheckboxGroup',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '站内信', value: '1', disabled: true },
|
||||
{ label: '邮件', value: '2' },
|
||||
{ label: '短信', value: '3' },
|
||||
],
|
||||
},
|
||||
label: '通知方式',
|
||||
defaultValue: ['1'],
|
||||
},
|
||||
{
|
||||
fieldName: 'nodeCode',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
getPopupContainer,
|
||||
},
|
||||
label: '驳回节点',
|
||||
},
|
||||
{
|
||||
fieldName: 'message',
|
||||
component: 'Textarea',
|
||||
label: '审批意见',
|
||||
formItemClass: 'items-baseline',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
interface ModalProps {
|
||||
taskId: string;
|
||||
definitionId: string;
|
||||
nodeCode: string;
|
||||
}
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '审批驳回',
|
||||
fullscreenButton: false,
|
||||
class: 'min-h-[365px]',
|
||||
onConfirm: handleSubmit,
|
||||
async onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
await formApi.resetForm();
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const { taskId, definitionId, nodeCode } = modalApi.getData() as ModalProps;
|
||||
await formApi.setFieldValue('taskId', taskId);
|
||||
|
||||
const resp = await getBackTaskNode(definitionId, nodeCode);
|
||||
const options = resp.map((item) => ({
|
||||
label: item.nodeName,
|
||||
value: item.nodeCode,
|
||||
}));
|
||||
formApi.updateSchema([
|
||||
{
|
||||
fieldName: 'nodeCode',
|
||||
componentProps: {
|
||||
options,
|
||||
},
|
||||
},
|
||||
]);
|
||||
// 默认选中第一个节点
|
||||
if (options.length > 0) {
|
||||
formApi.setFieldValue('nodeCode', options[0]?.value);
|
||||
}
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
console.log(data);
|
||||
await backProcess(data);
|
||||
modalApi.close();
|
||||
emit('complete');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<BasicForm />
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts">
|
||||
import type { Flow } from '#/api/workflow/instance/model';
|
||||
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
import { VbenAvatar } from '@vben/common-ui';
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { TimelineItem } from 'ant-design-vue';
|
||||
|
||||
import { ossInfo } from '#/api/system/oss';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
defineOptions({
|
||||
name: 'ApprovalTimelineItem',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = defineProps<{ item: Flow }>();
|
||||
|
||||
interface AttachmentInfo {
|
||||
ossId: string;
|
||||
url: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理附件信息
|
||||
*/
|
||||
const attachmentInfo = ref<AttachmentInfo[]>([]);
|
||||
onMounted(async () => {
|
||||
if (!props.item.ext) {
|
||||
return null;
|
||||
}
|
||||
const resp = await ossInfo(props.item.ext.split(','));
|
||||
attachmentInfo.value = resp.map((item) => ({
|
||||
ossId: item.ossId,
|
||||
url: item.url,
|
||||
name: item.originalName,
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TimelineItem :key="item.id">
|
||||
<template #dot>
|
||||
<div class="relative rounded-full border">
|
||||
<VbenAvatar
|
||||
:alt="item.approveName"
|
||||
class="bg-primary size-[36px] rounded-full text-white"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ml-2 flex flex-col gap-0.5">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="font-bold">{{ item.nodeName }}</div>
|
||||
<component :is="renderDict(item.flowStatus, DictEnum.WF_TASK_STATUS)" />
|
||||
</div>
|
||||
<div>{{ item.approveName }}</div>
|
||||
<div>{{ item.updateTime }}</div>
|
||||
<div v-if="item.message" class="rounded-lg border p-1">
|
||||
<div class="break-all opacity-70">{{ item.message }}</div>
|
||||
</div>
|
||||
<div v-if="attachmentInfo.length > 0" class="flex flex-wrap gap-2">
|
||||
<!-- 这里下载的文件名不是原始文件名 -->
|
||||
<a
|
||||
v-for="attachment in attachmentInfo"
|
||||
:key="attachment.ossId"
|
||||
:href="attachment.url"
|
||||
class="text-primary"
|
||||
target="_blank"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="icon-[mingcute--attachment-line] size-[18px]"></span>
|
||||
<span>{{ attachment.name }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</TimelineItem>
|
||||
</template>
|
@@ -1,50 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { Timeline, TimelineItem } from 'ant-design-vue';
|
||||
import type { Flow } from '#/api/workflow/instance/model';
|
||||
|
||||
/**
|
||||
* TODO: 仅为demo 后期会替换
|
||||
*/
|
||||
import { VbenAvatar } from '../../../../../../packages/@core/ui-kit/shadcn-ui/src/components';
|
||||
import { Timeline } from 'ant-design-vue';
|
||||
|
||||
interface ApprovalItem {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
remark?: string;
|
||||
time: string;
|
||||
}
|
||||
import ApprovalTimelineItem from './approval-timeline-item.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
list: ApprovalItem[];
|
||||
list: Flow[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Timeline>
|
||||
<TimelineItem v-for="item in props.list" :key="item.id">
|
||||
<template #dot>
|
||||
<div class="relative rounded-full border">
|
||||
<VbenAvatar
|
||||
class="size-[36px]"
|
||||
src="https://plus.dapdap.top/minio-server/plus/2024/11/21/925ed278e2d441beb7f695b41e13c4dd.jpg"
|
||||
/>
|
||||
<div
|
||||
class="border-background absolute bottom-0 right-0 size-[16px] rounded-full border-2 bg-green-500 content-['']"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[mdi--success-bold] text-white"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="ml-2 flex flex-col">
|
||||
<div>发起人</div>
|
||||
<div>疯狂的牛子Li</div>
|
||||
<div>2022-01-01 12:00:00</div>
|
||||
<div class="rounded-lg border p-1">
|
||||
<span class="opacity-70">这里是备注信息</span>
|
||||
</div>
|
||||
</div>
|
||||
</TimelineItem>
|
||||
<Timeline v-if="props.list.length > 0">
|
||||
<ApprovalTimelineItem
|
||||
v-for="(item, index) in props.list"
|
||||
:key="index"
|
||||
:item="item"
|
||||
/>
|
||||
</Timeline>
|
||||
</template>
|
||||
|
@@ -0,0 +1,85 @@
|
||||
<!--抄送组件-->
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/system/user/model';
|
||||
|
||||
import { computed, type PropType } from 'vue';
|
||||
|
||||
import { useVbenModal, VbenAvatar } from '@vben/common-ui';
|
||||
|
||||
import { Avatar, AvatarGroup, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import { userSelectModal } from '.';
|
||||
|
||||
defineOptions({
|
||||
name: 'CopyComponent',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{ ellipseNumber?: number }>(), {
|
||||
/**
|
||||
* 最大显示的头像数量 超过显示为省略号头像
|
||||
*/
|
||||
ellipseNumber: 3,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{ cancel: []; finish: [User[]] }>();
|
||||
|
||||
const [UserSelectModal, modalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
|
||||
const userListModel = defineModel('userList', {
|
||||
type: Array as PropType<User[]>,
|
||||
default: () => [],
|
||||
});
|
||||
|
||||
function handleOpen() {
|
||||
modalApi.setData({ userList: userListModel.value });
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
function handleFinish(userList: User[]) {
|
||||
// 清空 直接赋值[]会丢失响应性
|
||||
userListModel.value.splice(0, userListModel.value.length);
|
||||
userListModel.value.push(...userList);
|
||||
emit('finish', userList);
|
||||
}
|
||||
|
||||
const displayedList = computed(() => {
|
||||
return userListModel.value.slice(0, props.ellipseNumber);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<AvatarGroup v-if="userListModel.length > 0">
|
||||
<Tooltip
|
||||
v-for="user in displayedList"
|
||||
:key="user.userId"
|
||||
:title="user.nickName"
|
||||
placement="top"
|
||||
>
|
||||
<div>
|
||||
<VbenAvatar
|
||||
:alt="user.nickName"
|
||||
class="bg-primary size-[36px] cursor-pointer rounded-full border text-white"
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
:title="`等${userListModel.length - props.ellipseNumber}人`"
|
||||
placement="top"
|
||||
>
|
||||
<Avatar
|
||||
v-if="userListModel.length > ellipseNumber"
|
||||
class="flex size-[36px] cursor-pointer items-center justify-center rounded-full border bg-[gray] text-white"
|
||||
>
|
||||
+{{ userListModel.length - props.ellipseNumber }}
|
||||
</Avatar>
|
||||
</Tooltip>
|
||||
</AvatarGroup>
|
||||
<a-button size="small" @click="handleOpen">选择人员</a-button>
|
||||
<UserSelectModal @cancel="$emit('cancel')" @finish="handleFinish" />
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { useAppConfig, useTabs } from '@vben/hooks';
|
||||
import { stringify } from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
|
||||
defineOptions({ name: 'FlowDesigner' });
|
||||
|
||||
const route = useRoute();
|
||||
const definitionId = route.query.definitionId as string;
|
||||
const disabled = route.query.disabled === 'true';
|
||||
|
||||
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
|
||||
const accessStore = useAccessStore();
|
||||
const params = {
|
||||
Authorization: `Bearer ${accessStore.accessToken}`,
|
||||
id: definitionId,
|
||||
clientid: clientId,
|
||||
disabled,
|
||||
};
|
||||
|
||||
/**
|
||||
* iframe设计器的地址
|
||||
*/
|
||||
const url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;
|
||||
|
||||
const { closeCurrentTab } = useTabs();
|
||||
const router = useRouter();
|
||||
|
||||
function messageHandler(event: MessageEvent) {
|
||||
switch (event.data.method) {
|
||||
case 'close': {
|
||||
// 关闭当前tab
|
||||
closeCurrentTab();
|
||||
// 跳转到流程定义列表
|
||||
router.push('/workflow/processDefinition');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// iframe监听组件内设计器保存事件
|
||||
useEventListener('message', messageHandler);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<iframe :src="url" class="size-full"></iframe>
|
||||
</template>
|
@@ -0,0 +1,38 @@
|
||||
<!-- 弹窗查看流程信息 -->
|
||||
<script setup lang="ts">
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { getTaskByBusinessId } from '#/api/workflow/instance';
|
||||
|
||||
import { ApprovalPanel } from '.';
|
||||
|
||||
interface ModalProps {
|
||||
businessId: string;
|
||||
}
|
||||
|
||||
const taskInfo = ref<TaskInfo>();
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '流程信息',
|
||||
class: 'w-[1000px]',
|
||||
footer: false,
|
||||
onOpenChange: async (isOpen) => {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
const { businessId } = modalApi.getData() as ModalProps;
|
||||
const taskResp = await getTaskByBusinessId(businessId);
|
||||
taskInfo.value = taskResp;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<ApprovalPanel :task="taskInfo" type="readonly" />
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,171 @@
|
||||
<script setup lang="ts">
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Descriptions, DescriptionsItem, Modal } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
getTaskByTaskId,
|
||||
taskOperation,
|
||||
terminationTask,
|
||||
} from '#/api/workflow/task';
|
||||
|
||||
import { userSelectModal } from '.';
|
||||
|
||||
const emit = defineEmits<{ complete: [] }>();
|
||||
|
||||
const taskInfo = ref<TaskInfo>();
|
||||
|
||||
/**
|
||||
* 是否显示 加签/减签操作
|
||||
*/
|
||||
const showMultiActions = computed(() => {
|
||||
if (!taskInfo.value) {
|
||||
return false;
|
||||
}
|
||||
if (Number(taskInfo.value.nodeRatio) > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '流程干预',
|
||||
class: 'w-[800px]',
|
||||
fullscreenButton: false,
|
||||
async onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
const { taskId } = modalApi.getData() as { taskId: string };
|
||||
taskInfo.value = await getTaskByTaskId(taskId);
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* 转办
|
||||
*/
|
||||
const [TransferModal, transferModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleTransfer(userList: User[]) {
|
||||
if (userList.length === 0 || !taskInfo.value) return;
|
||||
const current = userList[0];
|
||||
Modal.confirm({
|
||||
title: '转办',
|
||||
content: `确定转办给${current?.nickName}吗?`,
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: taskInfo.value!.id, userId: current!.userId },
|
||||
'transferTask',
|
||||
);
|
||||
emit('complete');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批终止
|
||||
*/
|
||||
function handleTermination() {
|
||||
if (!taskInfo.value) {
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '审批终止',
|
||||
content: '确定终止当前审批流程吗?',
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
await terminationTask({ taskId: taskInfo.value!.id });
|
||||
emit('complete');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [AddSignatureModal, addSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleAddSignature(userList: User[]) {
|
||||
if (userList.length === 0 || !taskInfo.value) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认加签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: taskInfo.value!.id, userIds },
|
||||
'addSignature',
|
||||
);
|
||||
emit('complete');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const [ReductionSignatureModal, reductionSignatureModalApi] = useVbenModal({
|
||||
connectedComponent: userSelectModal,
|
||||
});
|
||||
function handleReductionSignature(userList: User[]) {
|
||||
if (userList.length === 0 || !taskInfo.value) return;
|
||||
const userIds = userList.map((user) => user.userId);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确认减签吗?',
|
||||
centered: true,
|
||||
onOk: async () => {
|
||||
await taskOperation(
|
||||
{ taskId: taskInfo.value!.id, userIds },
|
||||
'reductionSignature',
|
||||
);
|
||||
emit('complete');
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<Descriptions v-if="taskInfo" :column="2" bordered size="small">
|
||||
<DescriptionsItem label="任务名称">
|
||||
{{ taskInfo.nodeName }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="节点编码">
|
||||
{{ taskInfo.nodeCode }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="开始时间">
|
||||
{{ taskInfo.createTime }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="流程实例ID">
|
||||
{{ taskInfo.instanceId }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="版本号">
|
||||
{{ taskInfo.version }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="业务ID">
|
||||
{{ taskInfo.businessId }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
<TransferModal mode="single" @finish="handleTransfer" />
|
||||
<AddSignatureModal mode="multiple" @finish="handleAddSignature" />
|
||||
<ReductionSignatureModal
|
||||
mode="multiple"
|
||||
@finish="handleReductionSignature"
|
||||
/>
|
||||
<template #footer>
|
||||
<template v-if="showMultiActions">
|
||||
<a-button @click="() => addSignatureModalApi.open()">加签</a-button>
|
||||
<a-button @click="() => reductionSignatureModalApi.open()">
|
||||
减签
|
||||
</a-button>
|
||||
</template>
|
||||
<a-button @click="() => transferModalApi.open()">转办</a-button>
|
||||
<a-button danger type="primary" @click="handleTermination">终止</a-button>
|
||||
</template>
|
||||
</BasicModal>
|
||||
</template>
|
60
apps/web-antd/src/views/workflow/components/helper.tsx
Normal file
60
apps/web-antd/src/views/workflow/components/helper.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { defineComponent, h, ref } from 'vue';
|
||||
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
import ApprovalContent from './approval-content.vue';
|
||||
|
||||
export interface ApproveWithReasonModalProps {
|
||||
title: string;
|
||||
description: string;
|
||||
onOk: (reason: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 带审批意见的confirm
|
||||
* @param props props
|
||||
*/
|
||||
export function approveWithReasonModal(props: ApproveWithReasonModalProps) {
|
||||
const { onOk, title, description } = props;
|
||||
const content = ref('');
|
||||
Modal.confirm({
|
||||
title,
|
||||
content: h(
|
||||
defineComponent({
|
||||
setup() {
|
||||
return () =>
|
||||
h(ApprovalContent, {
|
||||
description,
|
||||
value: content.value,
|
||||
'onUpdate:value': (v) => (content.value = v),
|
||||
});
|
||||
},
|
||||
}),
|
||||
),
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => onOk(content.value),
|
||||
});
|
||||
}
|
||||
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(relativeTime);
|
||||
/**
|
||||
* 计算相差的时间
|
||||
* @param dateTime 时间字符串
|
||||
* @returns 相差的时间
|
||||
*/
|
||||
export function getDiffTimeString(dateTime: string) {
|
||||
// 计算相差秒数
|
||||
const diffSeconds = dayjs().diff(dayjs(dateTime), 'second');
|
||||
/**
|
||||
* 转为时间显示(x月 x天)
|
||||
* https://dayjs.fenxianglu.cn/category/duration.html#%E4%BA%BA%E6%80%A7%E5%8C%96
|
||||
*
|
||||
*/
|
||||
const diffText = dayjs.duration(diffSeconds, 'seconds').humanize();
|
||||
return diffText;
|
||||
}
|
@@ -1,2 +1,30 @@
|
||||
export { default as applyModal } from './apply-modal.vue';
|
||||
export { default as ApprovalCard } from './approval-card.vue';
|
||||
/**
|
||||
* 审批同意
|
||||
*/
|
||||
export { default as approvalModal } from './approval-modal.vue';
|
||||
export { default as ApprovalPanel } from './approval-panel.vue';
|
||||
/**
|
||||
* 审批驳回
|
||||
*/
|
||||
export { default as approvalRejectionModal } from './approval-rejection-modal.vue';
|
||||
export { default as ApprovalTimeline } from './approval-timeline.vue';
|
||||
/**
|
||||
* 选择抄送人
|
||||
*/
|
||||
export { default as CopyComponent } from './copy-component.vue';
|
||||
|
||||
/**
|
||||
* 详情信息 modal
|
||||
*/
|
||||
export { default as flowInfoModal } from './flow-info-modal.vue';
|
||||
/**
|
||||
* 流程干预 modal
|
||||
*/
|
||||
export { default as flowInterfereModal } from './flow-interfere-modal.vue';
|
||||
|
||||
/**
|
||||
* 选人 支持单选/多选
|
||||
*/
|
||||
export { default as userSelectModal } from './user-select-modal.vue';
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,365 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
import type { User } from '#/api';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal, VbenAvatar } from '@vben/common-ui';
|
||||
|
||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { userList } from '#/api/system/user';
|
||||
import DeptTree from '#/views/system/user/dept-tree.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'UserSelectModal',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = withDefaults(defineProps<{ mode?: 'multiple' | 'single' }>(), {
|
||||
mode: 'multiple',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* 取消的事件
|
||||
*/
|
||||
cancel: [];
|
||||
/**
|
||||
* 选择完成的事件
|
||||
*/
|
||||
finish: [User[]];
|
||||
}>();
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '选择人员',
|
||||
class: 'w-[1060px]',
|
||||
fullscreenButton: false,
|
||||
onClosed: () => emit('cancel'),
|
||||
onConfirm: handleSubmit,
|
||||
async onOpened() {
|
||||
const { userList = [] } = modalApi.getData() as { userList: User[] };
|
||||
// 暂时只处理多选 目前并没有单选的情况
|
||||
if (props.mode === 'multiple') {
|
||||
// 左边选中
|
||||
await tableApi.grid.setCheckboxRow(userList, true);
|
||||
// 右边赋值
|
||||
await rightTableApi.grid.loadData(userList);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 左边部门用
|
||||
const selectDeptId = ref<string[]>([]);
|
||||
const formOptions: VbenFormProps = {
|
||||
schema: [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'userName',
|
||||
label: '用户账号',
|
||||
hideLabel: true,
|
||||
componentProps: {
|
||||
placeholder: '请输入账号',
|
||||
},
|
||||
},
|
||||
],
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
wrapperClass: 'grid-cols-2',
|
||||
handleReset: async () => {
|
||||
selectDeptId.value = [];
|
||||
const { formApi, reload } = tableApi;
|
||||
await formApi.resetForm();
|
||||
const formValues = formApi.form.values;
|
||||
formApi.setLatestSubmissionValues(formValues);
|
||||
await reload(formValues);
|
||||
},
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
// 点击行选中
|
||||
trigger: 'row',
|
||||
},
|
||||
radioConfig: {
|
||||
trigger: 'row',
|
||||
strict: true,
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
type: props.mode === 'single' ? 'radio' : 'checkbox',
|
||||
width: 60,
|
||||
resizable: false,
|
||||
},
|
||||
{
|
||||
field: 'userName',
|
||||
title: '用户',
|
||||
headerAlign: 'left',
|
||||
resizable: false,
|
||||
slots: {
|
||||
default: 'user',
|
||||
},
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
layouts: ['PrevPage', 'Number', 'NextPage', 'Sizes', 'Total'],
|
||||
},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 部门树选择处理
|
||||
if (selectDeptId.value.length === 1) {
|
||||
formValues.deptId = selectDeptId.value[0];
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'deptId');
|
||||
}
|
||||
|
||||
// 加载完毕需要设置选中的行
|
||||
if (props.mode === 'multiple') {
|
||||
const records = rightTableApi.grid.getData();
|
||||
await tableApi.grid.setCheckboxRow(records, true);
|
||||
}
|
||||
if (props.mode === 'single') {
|
||||
const records = rightTableApi.grid.getData();
|
||||
if (records.length === 1) {
|
||||
await tableApi.grid.setRadioRow(records[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return await userList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'userId',
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
gridEvents: {
|
||||
// 需要控制不同的事件 radio也会触发checkbox事件
|
||||
checkboxChange: checkBoxEvent,
|
||||
checkboxAll: checkBoxEvent,
|
||||
radioChange: radioEvent,
|
||||
},
|
||||
});
|
||||
|
||||
function checkBoxEvent() {
|
||||
if (props.mode !== 'multiple') {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* 给右边表格赋值
|
||||
* records拿到的是当前页的选中数据
|
||||
* reserveRecords拿到的是其他页选中的数据
|
||||
*/
|
||||
const records = tableApi.grid.getCheckboxRecords();
|
||||
const reserveRecords = tableApi.grid.getCheckboxReserveRecords();
|
||||
const realRecords = [...records, ...reserveRecords];
|
||||
rightTableApi.grid.loadData(realRecords);
|
||||
}
|
||||
|
||||
function radioEvent() {
|
||||
if (props.mode !== 'single') {
|
||||
return;
|
||||
}
|
||||
// 给右边表格赋值
|
||||
const records = tableApi.grid.getRadioRecord();
|
||||
rightTableApi.grid.loadData([records]);
|
||||
}
|
||||
|
||||
const rightGridOptions: VxeGridProps = {
|
||||
checkboxConfig: {},
|
||||
columns: [
|
||||
{
|
||||
field: 'nickName',
|
||||
title: '昵称',
|
||||
width: 200,
|
||||
resizable: false,
|
||||
slots: {
|
||||
default: 'user',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
title: '操作',
|
||||
width: 120,
|
||||
slots: { default: 'action' },
|
||||
},
|
||||
],
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
proxyConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'userId',
|
||||
},
|
||||
toolbarConfig: {
|
||||
enabled: false,
|
||||
},
|
||||
showOverflow: false,
|
||||
};
|
||||
|
||||
const [RightBasicTable, rightTableApi] = useVbenVxeGrid({
|
||||
gridOptions: rightGridOptions,
|
||||
});
|
||||
|
||||
async function handleRemoveItem(row: any) {
|
||||
if (props.mode === 'multiple') {
|
||||
await tableApi.grid.setCheckboxRow(row, false);
|
||||
}
|
||||
if (props.mode === 'single') {
|
||||
await tableApi.grid.clearRadioRow();
|
||||
}
|
||||
const data = rightTableApi.grid.getData();
|
||||
await rightTableApi.grid.loadData(data.filter((item) => item !== row));
|
||||
// 这个方法有问题
|
||||
// await rightTableApi.grid.remove(row);
|
||||
}
|
||||
|
||||
function handleRemoveAll() {
|
||||
if (props.mode === 'multiple') {
|
||||
tableApi.grid.clearCheckboxRow();
|
||||
tableApi.grid.clearCheckboxReserve();
|
||||
}
|
||||
if (props.mode === 'single') {
|
||||
tableApi.grid.clearRadioRow();
|
||||
}
|
||||
rightTableApi.grid.loadData([]);
|
||||
}
|
||||
|
||||
async function handleDeptQuery() {
|
||||
await tableApi.reload();
|
||||
// 重置后恢复 保存勾选的数据
|
||||
const records = rightTableApi.grid.getData();
|
||||
if (props.mode === 'multiple') {
|
||||
tableApi?.grid.setCheckboxRow(records, true);
|
||||
}
|
||||
if (props.mode === 'single' && records.length === 1) {
|
||||
tableApi.grid.setRadioRow(records[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
const records = rightTableApi.grid.getData();
|
||||
console.log(records);
|
||||
emit('finish', records);
|
||||
modalApi.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<div class="flex min-h-[600px]">
|
||||
<DeptTree
|
||||
v-model:select-dept-id="selectDeptId"
|
||||
:show-search="false"
|
||||
class="w-[230px]"
|
||||
@reload="() => tableApi.reload()"
|
||||
@select="handleDeptQuery"
|
||||
/>
|
||||
<div class="h-[600px] w-[450px]">
|
||||
<BasicTable>
|
||||
<template #user="{ row }">
|
||||
<div class="flex items-center gap-2">
|
||||
<VbenAvatar
|
||||
:alt="row.nickName"
|
||||
:src="row.avatar ?? ''"
|
||||
:class="{ 'bg-primary': !row.avatar }"
|
||||
class="size-[32px] rounded-full text-white"
|
||||
/>
|
||||
<div class="flex flex-col items-baseline text-[12px]">
|
||||
<div>{{ row.nickName }}</div>
|
||||
<div class="opacity-50">
|
||||
{{ row.phonenumber || '暂无手机号' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
<div class="flex h-[600px] flex-col">
|
||||
<div class="flex w-full px-4">
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<div>已选中人员</div>
|
||||
<div>
|
||||
<a-button size="small" @click="handleRemoveAll">
|
||||
清空选中
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<RightBasicTable id="user-select-right-table">
|
||||
<template #user="{ row }">
|
||||
<div class="flex items-center gap-2 overflow-hidden">
|
||||
<VbenAvatar
|
||||
:alt="row.nickName"
|
||||
:src="row.avatar ?? ''"
|
||||
:class="{ 'bg-primary': !row.avatar }"
|
||||
class="size-[32px] rounded-full text-white"
|
||||
/>
|
||||
<div class="flex flex-col items-baseline text-[12px]">
|
||||
<div class="overflow-ellipsis whitespace-nowrap">
|
||||
{{ row.nickName }}
|
||||
</div>
|
||||
<div class="opacity-50">
|
||||
{{ row.phonenumber || '暂无手机号' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<a-button size="small" @click="handleRemoveItem(row)">
|
||||
移除
|
||||
</a-button>
|
||||
</template>
|
||||
</RightBasicTable>
|
||||
</div>
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(div.vben-link) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.vxe-body--row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/**
|
||||
默认显示右边的滚动条 防止出现滚动条被挤压
|
||||
*/
|
||||
#user-select-right-table {
|
||||
div.vxe-table--body-wrapper.body--wrapper {
|
||||
overflow: scroll;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,9 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
62
apps/web-antd/src/views/workflow/leave/api/index.ts
Normal file
62
apps/web-antd/src/views/workflow/leave/api/index.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { LeaveForm, LeaveQuery, LeaveVO } from './model';
|
||||
|
||||
import type { ID, IDS, PageResult } from '#/api/common';
|
||||
|
||||
import { commonExport } from '#/api/helper';
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
/**
|
||||
* 查询请假申请列表
|
||||
* @param params
|
||||
* @returns 请假申请列表
|
||||
*/
|
||||
export function leaveList(params?: LeaveQuery) {
|
||||
return requestClient.get<PageResult<LeaveVO>>('/workflow/leave/list', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出请假申请列表
|
||||
* @param params
|
||||
* @returns 请假申请列表
|
||||
*/
|
||||
export function leaveExport(params?: LeaveQuery) {
|
||||
return commonExport('/workflow/leave/export', params ?? {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询请假申请详情
|
||||
* @param id id
|
||||
* @returns 请假申请详情
|
||||
*/
|
||||
export function leaveInfo(id: ID) {
|
||||
return requestClient.get<LeaveVO>(`/workflow/leave/${id}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增请假申请
|
||||
* @param data
|
||||
* @returns void
|
||||
*/
|
||||
export function leaveAdd(data: LeaveForm) {
|
||||
return requestClient.postWithMsg<LeaveVO>('/workflow/leave', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新请假申请
|
||||
* @param data
|
||||
* @returns void
|
||||
*/
|
||||
export function leaveUpdate(data: LeaveForm) {
|
||||
return requestClient.putWithMsg<LeaveVO>('/workflow/leave', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除请假申请
|
||||
* @param id id
|
||||
* @returns void
|
||||
*/
|
||||
export function leaveRemove(id: ID | IDS) {
|
||||
return requestClient.deleteWithMsg<void>(`/workflow/leave/${id}`);
|
||||
}
|
107
apps/web-antd/src/views/workflow/leave/api/model.d.ts
vendored
Normal file
107
apps/web-antd/src/views/workflow/leave/api/model.d.ts
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { BaseEntity, PageQuery } from '#/api/common';
|
||||
|
||||
export interface LeaveVO {
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
id: number | string;
|
||||
|
||||
/**
|
||||
* 请假类型
|
||||
*/
|
||||
leaveType: string;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
startDate: string;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
endDate: string;
|
||||
|
||||
/**
|
||||
* 请假天数
|
||||
*/
|
||||
leaveDays: number;
|
||||
|
||||
/**
|
||||
* 请假原因
|
||||
*/
|
||||
remark: string;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface LeaveForm extends BaseEntity {
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
id?: number | string;
|
||||
|
||||
/**
|
||||
* 请假类型
|
||||
*/
|
||||
leaveType?: string;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
startDate?: string;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
endDate?: string;
|
||||
|
||||
/**
|
||||
* 请假天数
|
||||
*/
|
||||
leaveDays?: number;
|
||||
|
||||
/**
|
||||
* 请假原因
|
||||
*/
|
||||
remark?: string;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface LeaveQuery extends PageQuery {
|
||||
/**
|
||||
* 请假类型
|
||||
*/
|
||||
leaveType?: string;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
startDate?: string;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
endDate?: string;
|
||||
|
||||
/**
|
||||
* 请假天数
|
||||
*/
|
||||
leaveDays?: number;
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* 日期范围参数
|
||||
*/
|
||||
params?: any;
|
||||
}
|
173
apps/web-antd/src/views/workflow/leave/data.tsx
Normal file
173
apps/web-antd/src/views/workflow/leave/data.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import type { FormSchemaGetter, VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { DictEnum } from '@vben/constants';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { OptionsTag } from '#/components/table';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
export const leaveTypeOptions = [
|
||||
{ label: '病假', value: '1' },
|
||||
{ label: '事假', value: '2' },
|
||||
{ label: '年假', value: '3' },
|
||||
{ label: '婚假', value: '4' },
|
||||
{ label: '产假', value: '5' },
|
||||
{ label: '其他', value: '7' },
|
||||
];
|
||||
|
||||
export const leaveFlowOptions = [
|
||||
{ label: '请假流程-普通', value: 'leave1' },
|
||||
{ label: '请假流程-排他网关', value: 'leave2' },
|
||||
{ label: '请假流程-并行网关', value: 'leave3' },
|
||||
{ label: '请假流程-会签', value: 'leave4' },
|
||||
{ label: '请假申请-并行会签网关', value: 'leave5' },
|
||||
];
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 1,
|
||||
},
|
||||
fieldName: 'startLeaveDays',
|
||||
label: '请假天数',
|
||||
},
|
||||
{
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
min: 1,
|
||||
},
|
||||
fieldName: 'endLeaveDays',
|
||||
label: '至',
|
||||
labelClass: 'justify-center',
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
title: '请假类型',
|
||||
field: 'leaveType',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return <OptionsTag options={leaveTypeOptions} value={row.leaveType} />;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
field: 'startDate',
|
||||
formatter: ({ cellValue }) => dayjs(cellValue).format('YYYY-MM-DD'),
|
||||
},
|
||||
{
|
||||
title: '结束时间',
|
||||
field: 'endDate',
|
||||
formatter: ({ cellValue }) => dayjs(cellValue).format('YYYY-MM-DD'),
|
||||
},
|
||||
{
|
||||
title: '请假天数',
|
||||
field: 'leaveDays',
|
||||
formatter: ({ cellValue }) => `${cellValue}天`,
|
||||
},
|
||||
{
|
||||
title: '请假原因',
|
||||
field: 'remark',
|
||||
},
|
||||
{
|
||||
title: '流程状态',
|
||||
field: 'status',
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderDict(row.status, DictEnum.WF_BUSINESS_STATUS);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
width: 210,
|
||||
},
|
||||
];
|
||||
|
||||
export const modalSchema: (isEdit: boolean) => VbenFormSchema[] = (
|
||||
isEdit: boolean,
|
||||
) => [
|
||||
{
|
||||
label: '主键',
|
||||
fieldName: 'id',
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '流程类型',
|
||||
fieldName: 'flowType',
|
||||
component: 'Select',
|
||||
help: '这里仅仅为了发起流程方便, 实际不应该包含此字段',
|
||||
componentProps: {
|
||||
options: leaveFlowOptions,
|
||||
getPopupContainer,
|
||||
},
|
||||
defaultValue: 'leave1',
|
||||
rules: 'selectRequired',
|
||||
dependencies: {
|
||||
show: () => isEdit,
|
||||
triggerFields: [''],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: '请假类型',
|
||||
fieldName: 'leaveType',
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
options: leaveTypeOptions,
|
||||
getPopupContainer,
|
||||
},
|
||||
rules: 'selectRequired',
|
||||
formItemClass: 'col-span-1',
|
||||
},
|
||||
{
|
||||
label: '开始时间',
|
||||
fieldName: 'dateRange',
|
||||
component: 'RangePicker',
|
||||
componentProps(model) {
|
||||
return {
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'YYYY-MM-DD HH:mm:ss',
|
||||
onChange: (dates: [string, string]) => {
|
||||
if (!dates) {
|
||||
model.leaveDays = null;
|
||||
return;
|
||||
}
|
||||
const [start, end] = dates;
|
||||
const leaveDays = dayjs(end).diff(dayjs(start), 'day') + 1;
|
||||
model.leaveDays = leaveDays;
|
||||
},
|
||||
};
|
||||
},
|
||||
rules: 'required',
|
||||
formItemClass: 'col-span-1',
|
||||
},
|
||||
{
|
||||
label: '请假天数',
|
||||
fieldName: 'leaveDays',
|
||||
component: 'Input',
|
||||
componentProps: {
|
||||
disabled: true,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
label: '请假原因',
|
||||
fieldName: 'remark',
|
||||
component: 'Textarea',
|
||||
formItemClass: 'items-baseline',
|
||||
},
|
||||
];
|
@@ -1,9 +1,198 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
|
||||
import type { LeaveForm } from './api/model';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||
import { cancelProcessApply } from '#/api/workflow/instance';
|
||||
import { commonDownloadExcel } from '#/utils/file/download';
|
||||
|
||||
import { flowInfoModal } from '../components';
|
||||
import { leaveExport, leaveList, leaveRemove } from './api';
|
||||
import { columns, querySchema } from './data';
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
schema: querySchema(),
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
// 点击行选中
|
||||
// trigger: 'row',
|
||||
},
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
return await leaveList({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
},
|
||||
// 表格全局唯一表示 保存列配置需要用到
|
||||
id: 'workflow-leave-index',
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
function handleAdd() {
|
||||
router.push('/workflow/leaveEdit/index');
|
||||
}
|
||||
|
||||
async function handleEdit(row: Required<LeaveForm>) {
|
||||
router.push({ path: '/workflow/leaveEdit/index', query: { id: row.id } });
|
||||
}
|
||||
|
||||
async function handleDelete(row: Required<LeaveForm>) {
|
||||
await leaveRemove(row.id);
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
async function handleRevoke(row: Required<LeaveForm>) {
|
||||
await cancelProcessApply({
|
||||
businessId: row.id,
|
||||
message: '申请人撤销流程!',
|
||||
});
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
function handleMultiDelete() {
|
||||
const rows = tableApi.grid.getCheckboxRecords();
|
||||
const ids = rows.map((row: Required<LeaveForm>) => row.id);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
okType: 'danger',
|
||||
content: `确认删除选中的${ids.length}条记录吗?`,
|
||||
onOk: async () => {
|
||||
await leaveRemove(ids);
|
||||
await tableApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleDownloadExcel() {
|
||||
commonDownloadExcel(
|
||||
leaveExport,
|
||||
'请假申请数据',
|
||||
tableApi.formApi.form.values,
|
||||
{
|
||||
fieldMappingTime: formOptions.fieldMappingTime,
|
||||
},
|
||||
);
|
||||
}
|
||||
const [FlowInfoModal, flowInfoModalApi] = useVbenModal({
|
||||
connectedComponent: flowInfoModal,
|
||||
});
|
||||
function handleInfo(row: Required<LeaveForm>) {
|
||||
flowInfoModalApi.setData({ businessId: row.id });
|
||||
flowInfoModalApi.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
<Page :auto-content-height="true">
|
||||
<BasicTable table-title="请假申请列表">
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
v-access:code="['workflow:leave:export']"
|
||||
@click="handleDownloadExcel"
|
||||
>
|
||||
{{ $t('pages.common.export') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
:disabled="!vxeCheckboxChecked(tableApi)"
|
||||
danger
|
||||
type="primary"
|
||||
v-access:code="['workflow:leave:remove']"
|
||||
@click="handleMultiDelete"
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
v-access:code="['workflow:leave:add']"
|
||||
@click="handleAdd"
|
||||
>
|
||||
{{ $t('pages.common.add') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<Space>
|
||||
<ghost-button
|
||||
v-if="['draft', 'cancel', 'back'].includes(row.status)"
|
||||
v-access:code="['workflow:leave:edit']"
|
||||
@click.stop="handleEdit(row)"
|
||||
>
|
||||
{{ $t('pages.common.edit') }}
|
||||
</ghost-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
title="确认撤销?"
|
||||
@confirm="handleRevoke(row)"
|
||||
>
|
||||
<ghost-button
|
||||
v-if="['waiting'].includes(row.status)"
|
||||
v-access:code="['workflow:leave:edit']"
|
||||
@click.stop=""
|
||||
>
|
||||
撤销
|
||||
</ghost-button>
|
||||
</Popconfirm>
|
||||
<ghost-button @click="handleInfo(row)">详情</ghost-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<ghost-button
|
||||
v-if="['draft', 'cancel', 'back'].includes(row.status)"
|
||||
danger
|
||||
v-access:code="['workflow:leave:remove']"
|
||||
@click.stop=""
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</ghost-button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<FlowInfoModal />
|
||||
</Page>
|
||||
</template>
|
||||
|
45
apps/web-antd/src/views/workflow/leave/leave-description.vue
Normal file
45
apps/web-antd/src/views/workflow/leave/leave-description.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { LeaveVO } from './api/model';
|
||||
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { Descriptions, DescriptionsItem } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { leaveTypeOptions } from './data';
|
||||
|
||||
defineOptions({
|
||||
name: 'LeaveDescription',
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = defineProps<{ data: LeaveVO }>();
|
||||
|
||||
const leaveType = computed(() => {
|
||||
return (
|
||||
leaveTypeOptions.find((item) => item.value === props.data.leaveType)
|
||||
?.label ?? '未知'
|
||||
);
|
||||
});
|
||||
|
||||
function formatDate(date: string) {
|
||||
return dayjs(date).format('YYYY-MM-DD');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Descriptions :column="1" size="middle">
|
||||
<DescriptionsItem label="请假类型">
|
||||
{{ leaveType }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="请假时间">
|
||||
{{ formatDate(data.startDate) }} - {{ formatDate(data.endDate) }}
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="请假时长">
|
||||
{{ data.leaveDays }}天
|
||||
</DescriptionsItem>
|
||||
<DescriptionsItem label="请假原因">
|
||||
{{ data.remark || '无' }}
|
||||
</DescriptionsItem>
|
||||
</Descriptions>
|
||||
</template>
|
199
apps/web-antd/src/views/workflow/leave/leave-form.vue
Normal file
199
apps/web-antd/src/views/workflow/leave/leave-form.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<script setup lang="ts">
|
||||
import type { LeaveVO } from './api/model';
|
||||
|
||||
import type { StartWorkFlowReqData } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { Card } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import { cloneDeep, omit } from 'lodash-es';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { startWorkFlow } from '#/api/workflow/task';
|
||||
|
||||
import { applyModal } from '../components';
|
||||
import { leaveAdd, leaveInfo, leaveUpdate } from './api';
|
||||
import { modalSchema } from './data';
|
||||
import LeaveDescription from './leave-description.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const readonly = route.query?.readonly === 'true';
|
||||
const id = route.query?.id as string;
|
||||
|
||||
/**
|
||||
* id存在&readonly时候
|
||||
*/
|
||||
const showActionBtn = computed(() => {
|
||||
return !readonly;
|
||||
});
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
// 默认占满两列
|
||||
formItemClass: 'col-span-2',
|
||||
// 默认label宽度 px
|
||||
labelWidth: 100,
|
||||
// 通用配置项 会影响到所有表单项
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
disabled: readonly,
|
||||
},
|
||||
},
|
||||
schema: modalSchema(!readonly),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
const leaveDescription = ref<LeaveVO>();
|
||||
const showDescription = computed(() => {
|
||||
return readonly && leaveDescription.value;
|
||||
});
|
||||
const cardRef = useTemplateRef('cardRef');
|
||||
onMounted(async () => {
|
||||
// 只读 获取信息赋值
|
||||
if (id) {
|
||||
const resp = await leaveInfo(id);
|
||||
leaveDescription.value = resp;
|
||||
await formApi.setValues(resp);
|
||||
const dateRange = [dayjs(resp.startDate), dayjs(resp.endDate)];
|
||||
await formApi.setFieldValue('dateRange', dateRange);
|
||||
|
||||
/**
|
||||
* window.parent(最近的上一级父页面)
|
||||
* 主要解决内嵌iframe卡顿的问题
|
||||
*/
|
||||
if (readonly) {
|
||||
// 渲染完毕才显示表单
|
||||
window.parent.postMessage({ type: 'mounted' }, '*');
|
||||
// 获取表单高度 内嵌时保持一致
|
||||
setTimeout(() => {
|
||||
const el = cardRef.value?.$el as HTMLDivElement;
|
||||
// 获取高度
|
||||
const height = el?.offsetHeight ?? 0;
|
||||
if (height) {
|
||||
window.parent.postMessage({ type: 'height', height }, '*');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
/**
|
||||
* 提取通用逻辑
|
||||
*/
|
||||
async function handleSaveOrUpdate() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
let data = cloneDeep(await formApi.getValues()) as any;
|
||||
data = omit(data, 'flowType');
|
||||
// 处理日期
|
||||
data.startDate = dayjs(data.dateRange[0]).format('YYYY-MM-DD HH:mm:ss');
|
||||
data.endDate = dayjs(data.dateRange[1]).format('YYYY-MM-DD HH:mm:ss');
|
||||
if (id) {
|
||||
data.id = id;
|
||||
return await leaveUpdate(data);
|
||||
} else {
|
||||
return await leaveAdd(data);
|
||||
}
|
||||
}
|
||||
|
||||
const [ApplyModal, applyModalApi] = useVbenModal({
|
||||
connectedComponent: applyModal,
|
||||
});
|
||||
/**
|
||||
* 暂存 草稿状态
|
||||
*/
|
||||
async function handleTempSave() {
|
||||
try {
|
||||
await handleSaveOrUpdate();
|
||||
router.push('/demo/leave');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存业务 & 发起流程
|
||||
*/
|
||||
async function handleStartWorkFlow() {
|
||||
try {
|
||||
// 保存业务
|
||||
const leaveResp = await handleSaveOrUpdate();
|
||||
// 启动流程
|
||||
const taskVariables = {
|
||||
leaveDays: leaveResp!.leaveDays,
|
||||
userList: ['1', '3', '4'],
|
||||
};
|
||||
const formValues = await formApi.getValues();
|
||||
const flowCode = formValues?.flowType ?? 'leave1';
|
||||
const startWorkFlowData: StartWorkFlowReqData = {
|
||||
businessId: leaveResp!.id,
|
||||
flowCode,
|
||||
variables: taskVariables,
|
||||
};
|
||||
const { taskId } = await startWorkFlow(startWorkFlowData);
|
||||
// 打开窗口
|
||||
applyModalApi.setData({
|
||||
taskId,
|
||||
taskVariables,
|
||||
variables: {},
|
||||
});
|
||||
applyModalApi.open();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleComplete() {
|
||||
formApi.resetForm();
|
||||
router.push('/demo/leave');
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示详情时 需要较小的padding
|
||||
*/
|
||||
const cardSize = computed(() => {
|
||||
return showDescription.value ? 'small' : 'default';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card ref="cardRef" :size="cardSize">
|
||||
<div id="leave-form">
|
||||
<!-- 使用v-if会影响生命周期 -->
|
||||
<BasicForm v-show="!showDescription" />
|
||||
<LeaveDescription v-if="showDescription" :data="leaveDescription!" />
|
||||
<div v-if="showActionBtn" class="flex justify-end gap-2">
|
||||
<a-button @click="handleTempSave">暂存</a-button>
|
||||
<a-button type="primary" @click="handleStartWorkFlow">提交</a-button>
|
||||
</div>
|
||||
<ApplyModal @complete="handleComplete" />
|
||||
</div>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
html:has(#leave-form) {
|
||||
/**
|
||||
去除 '菜单加载中' 主要是iframe内嵌使用
|
||||
*/
|
||||
.ant-message-notice-content:has(.ant-message-loading) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
去除顶部进度条样式
|
||||
*/
|
||||
#nprogress {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,9 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import type { CategoryTree } from '#/api/workflow/category/model';
|
||||
|
||||
import { onMounted, type PropType, ref } from 'vue';
|
||||
|
||||
import { SyncOutlined } from '@ant-design/icons-vue';
|
||||
import { InputSearch, Skeleton, Tree } from 'ant-design-vue';
|
||||
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
|
||||
defineOptions({ inheritAttrs: false });
|
||||
|
||||
const emit = defineEmits<{
|
||||
/**
|
||||
* 点击刷新按钮的事件
|
||||
*/
|
||||
reload: [];
|
||||
/**
|
||||
* 点击节点的事件
|
||||
*/
|
||||
select: [];
|
||||
}>();
|
||||
|
||||
const selectCode = defineModel('selectCode', {
|
||||
required: true,
|
||||
type: Array as PropType<number[] | string[]>,
|
||||
});
|
||||
|
||||
const searchValue = defineModel('searchValue', {
|
||||
type: String,
|
||||
default: '',
|
||||
});
|
||||
|
||||
const categoryTreeArray = ref<CategoryTree[]>([]);
|
||||
/** 骨架屏加载 */
|
||||
const showTreeSkeleton = ref<boolean>(true);
|
||||
|
||||
async function loadTree() {
|
||||
showTreeSkeleton.value = true;
|
||||
searchValue.value = '';
|
||||
selectCode.value = [];
|
||||
|
||||
const treeData = await categoryTree();
|
||||
|
||||
categoryTreeArray.value = treeData;
|
||||
showTreeSkeleton.value = false;
|
||||
}
|
||||
|
||||
async function handleReload() {
|
||||
await loadTree();
|
||||
emit('reload');
|
||||
}
|
||||
|
||||
onMounted(loadTree);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$attrs.class">
|
||||
<Skeleton
|
||||
:loading="showTreeSkeleton"
|
||||
:paragraph="{ rows: 8 }"
|
||||
active
|
||||
class="p-[8px]"
|
||||
>
|
||||
<div
|
||||
class="bg-background flex h-full flex-col overflow-y-auto rounded-lg"
|
||||
>
|
||||
<!-- 固定在顶部 必须加上bg-background背景色 否则会产生'穿透'效果 -->
|
||||
<div class="bg-background z-100 sticky left-0 top-0 p-[8px]">
|
||||
<InputSearch
|
||||
v-model:value="searchValue"
|
||||
:placeholder="$t('pages.common.search')"
|
||||
size="small"
|
||||
>
|
||||
<template #enterButton>
|
||||
<a-button @click="handleReload">
|
||||
<SyncOutlined class="text-primary" />
|
||||
</a-button>
|
||||
</template>
|
||||
</InputSearch>
|
||||
</div>
|
||||
<div class="h-full overflow-x-hidden px-[8px]">
|
||||
<Tree
|
||||
v-bind="$attrs"
|
||||
v-if="categoryTreeArray.length > 0"
|
||||
v-model:selected-keys="selectCode"
|
||||
:class="$attrs.class"
|
||||
:field-names="{ title: 'label', key: 'id' }"
|
||||
:show-line="{ showLeafIcon: false }"
|
||||
:tree-data="categoryTreeArray"
|
||||
:virtual="false"
|
||||
default-expand-all
|
||||
@select="$emit('select')"
|
||||
>
|
||||
<template #title="{ label }">
|
||||
<span v-if="label.indexOf(searchValue) > -1">
|
||||
{{ label.substring(0, label.indexOf(searchValue)) }}
|
||||
<span style="color: #f50">{{ searchValue }}</span>
|
||||
{{
|
||||
label.substring(
|
||||
label.indexOf(searchValue) + searchValue.length,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-else>{{ label }}</span>
|
||||
</template>
|
||||
</Tree>
|
||||
</div>
|
||||
</div>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</template>
|
@@ -0,0 +1,36 @@
|
||||
import { optionsToEnum } from '@vben/utils';
|
||||
|
||||
export const activityStatusOptions = [
|
||||
{
|
||||
label: '激活',
|
||||
value: 1,
|
||||
color: 'success',
|
||||
enumName: 'Active',
|
||||
},
|
||||
{
|
||||
label: '挂起',
|
||||
value: 0,
|
||||
color: 'error',
|
||||
enumName: 'Suspended',
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const ActivityStatusEnum = optionsToEnum(activityStatusOptions);
|
||||
|
||||
export const publishStatusOptions = [
|
||||
{
|
||||
label: '已发布',
|
||||
value: 1,
|
||||
color: 'success',
|
||||
},
|
||||
{
|
||||
label: '未发布',
|
||||
value: 0,
|
||||
color: 'warning',
|
||||
},
|
||||
{
|
||||
label: '失效',
|
||||
value: 9,
|
||||
color: 'error',
|
||||
},
|
||||
];
|
103
apps/web-antd/src/views/workflow/processDefinition/data.tsx
Normal file
103
apps/web-antd/src/views/workflow/processDefinition/data.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { OptionsTag } from '#/components/table';
|
||||
|
||||
import { publishStatusOptions } from './constant';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'flowName',
|
||||
label: '流程名称',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'flowCode',
|
||||
label: '流程code',
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
field: 'flowName',
|
||||
title: '流程名称',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'flowCode',
|
||||
title: '流程code',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'version',
|
||||
title: '版本号',
|
||||
minWidth: 80,
|
||||
formatter: ({ cellValue }) => `V${cellValue}.0`,
|
||||
},
|
||||
{
|
||||
field: 'activityStatus',
|
||||
title: '激活状态',
|
||||
minWidth: 100,
|
||||
slots: {
|
||||
default: 'activityStatus',
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'isPublish',
|
||||
title: '发布状态',
|
||||
minWidth: 100,
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
const cellValue = row.isPublish;
|
||||
return (
|
||||
<OptionsTag options={publishStatusOptions as any} value={cellValue} />
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
resizable: false,
|
||||
width: 200,
|
||||
},
|
||||
];
|
||||
|
||||
export const modalSchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
dependencies: {
|
||||
show: () => false,
|
||||
triggerFields: [''],
|
||||
},
|
||||
fieldName: 'id',
|
||||
},
|
||||
{
|
||||
component: 'TreeSelect',
|
||||
fieldName: 'category',
|
||||
label: '流程分类',
|
||||
rules: 'selectRequired',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'flowCode',
|
||||
label: '流程code',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'flowName',
|
||||
label: '流程名称',
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'formPath',
|
||||
label: '表单路径',
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
@@ -1,9 +1,373 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
import type { RadioChangeEvent } from 'ant-design-vue';
|
||||
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import {
|
||||
message,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
RadioGroup,
|
||||
Space,
|
||||
Switch,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||
import {
|
||||
unPublishList,
|
||||
workflowDefinitionActive,
|
||||
workflowDefinitionCopy,
|
||||
workflowDefinitionDelete,
|
||||
workflowDefinitionExport,
|
||||
workflowDefinitionList,
|
||||
workflowDefinitionPublish,
|
||||
} from '#/api/workflow/definition';
|
||||
import { downloadByData } from '#/utils/file/download';
|
||||
|
||||
import CategoryTree from './category-tree.vue';
|
||||
import { columns, querySchema } from './data';
|
||||
import processDefinitionDeployModal from './process-definition-deploy-modal.vue';
|
||||
import processDefinitionModal from './process-definition-modal.vue';
|
||||
|
||||
// 左边部门用
|
||||
const selectedCode = ref<number[] | string[]>([]);
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
schema: querySchema(),
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
handleReset: async () => {
|
||||
selectedCode.value = [];
|
||||
const { formApi, reload } = tableApi;
|
||||
await formApi.resetForm();
|
||||
const formValues = formApi.form.values;
|
||||
formApi.setLatestSubmissionValues(formValues);
|
||||
await reload(formValues);
|
||||
},
|
||||
};
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
// 点击行选中
|
||||
trigger: 'default',
|
||||
},
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 部门树选择处理
|
||||
if (selectedCode.value.length === 1) {
|
||||
formValues.category = selectedCode.value[0];
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'category');
|
||||
}
|
||||
|
||||
return await currentTableApi.value({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
height: 100,
|
||||
},
|
||||
id: 'workflow-definition-index',
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
// 左边的切换
|
||||
const statusOptions = [
|
||||
{ label: '已发布流程', value: 1 },
|
||||
{ label: '未发布流程', value: 0 },
|
||||
];
|
||||
const currentStatus = ref(1);
|
||||
const currentTableApi = computed(() => {
|
||||
if (currentStatus.value === 1) {
|
||||
return workflowDefinitionList;
|
||||
}
|
||||
return unPublishList;
|
||||
});
|
||||
async function handleStatusChange(e: RadioChangeEvent) {
|
||||
currentStatus.value = e.target.value as number;
|
||||
await tableApi.reload();
|
||||
}
|
||||
|
||||
async function handleDelete(row: Recordable<any>) {
|
||||
await workflowDefinitionDelete(row.id);
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
function handleMultiDelete() {
|
||||
const rows = tableApi.grid.getCheckboxRecords();
|
||||
const ids = rows.map((row: any) => row.id);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
okType: 'danger',
|
||||
content: `确认删除选中的${ids.length}条记录吗?`,
|
||||
onOk: async () => {
|
||||
await workflowDefinitionDelete(ids);
|
||||
await tableApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
/**
|
||||
* 流程设计/预览
|
||||
* @param row row
|
||||
* @param disabled true为预览,false为设计
|
||||
*/
|
||||
function handleDesign(row: any, disabled: boolean) {
|
||||
router.push({
|
||||
path: '/workflow/designer',
|
||||
query: { definitionId: row.id, disabled: String(disabled) },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活/挂起流程
|
||||
* @param row row
|
||||
*/
|
||||
async function handleActive(row: any, status: boolean | number | string) {
|
||||
const lastStatus = status === 1 ? 0 : 1;
|
||||
try {
|
||||
await workflowDefinitionActive(row.id, !!status);
|
||||
await tableApi.query();
|
||||
} catch (error) {
|
||||
row.activityStatus = lastStatus;
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布流程
|
||||
* @param row row
|
||||
*/
|
||||
async function handlePublish(row: any) {
|
||||
await workflowDefinitionPublish(row.id);
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制流程
|
||||
* @param row row
|
||||
*/
|
||||
async function handleCopy(row: any) {
|
||||
await workflowDefinitionCopy(row.id);
|
||||
// 跳转到未发布流程tab
|
||||
currentStatus.value = 0;
|
||||
await tableApi.reload();
|
||||
}
|
||||
|
||||
const [ProcessDefinitionModal, modalApi] = useVbenModal({
|
||||
connectedComponent: processDefinitionModal,
|
||||
});
|
||||
|
||||
/**
|
||||
* 新增流程
|
||||
*/
|
||||
function handleAdd() {
|
||||
modalApi.setData({});
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑流程
|
||||
*/
|
||||
function handleEdit(row: any) {
|
||||
modalApi.setData({ id: row.id });
|
||||
modalApi.open();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出xml
|
||||
* @param row row
|
||||
*/
|
||||
async function handleExportXml(row: any) {
|
||||
const hideLoading = message.loading($t('pages.common.downloadLoading'), 0);
|
||||
try {
|
||||
const blob = await workflowDefinitionExport(row.id);
|
||||
downloadByData(blob, `${row.flowName}-${Date.now()}.json`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
const [ProcessDefinitionDeployModal, deployModalApi] = useVbenModal({
|
||||
connectedComponent: processDefinitionDeployModal,
|
||||
});
|
||||
|
||||
/**
|
||||
* 部署流程xml
|
||||
*/
|
||||
function handleDeploy() {
|
||||
if (selectedCode.value.length === 0) {
|
||||
message.warning('请先选择流程分类');
|
||||
return;
|
||||
}
|
||||
const selectedCategory = selectedCode.value[0];
|
||||
if (selectedCategory === 0) {
|
||||
message.warning('不可选择根目录进行部署, 请选择子分类');
|
||||
return;
|
||||
}
|
||||
deployModalApi.setData({ category: selectedCategory });
|
||||
deployModalApi.open();
|
||||
}
|
||||
|
||||
// 部署流程json
|
||||
async function handleDeploySuccess() {
|
||||
// 跳转到未发布
|
||||
currentStatus.value = 0;
|
||||
await tableApi.reload();
|
||||
}
|
||||
|
||||
// 新增完成需要跳转到未发布
|
||||
async function handleReload(type: 'add' | 'update') {
|
||||
if (type === 'add') {
|
||||
currentStatus.value = 0;
|
||||
}
|
||||
await tableApi.reload();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-[8px]">
|
||||
<CategoryTree
|
||||
v-model:select-code="selectedCode"
|
||||
class="w-[260px]"
|
||||
@reload="() => tableApi.reload()"
|
||||
@select="() => tableApi.reload()"
|
||||
/>
|
||||
<BasicTable class="flex-1 overflow-hidden">
|
||||
<template #toolbar-actions>
|
||||
<RadioGroup
|
||||
v-model:value="currentStatus"
|
||||
:options="statusOptions"
|
||||
button-style="solid"
|
||||
option-type="button"
|
||||
@change="handleStatusChange"
|
||||
/>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
:disabled="!vxeCheckboxChecked(tableApi)"
|
||||
danger
|
||||
type="primary"
|
||||
v-access:code="['system:user:remove']"
|
||||
@click="handleMultiDelete"
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</a-button>
|
||||
<a-button v-access:code="['system:user:add']" @click="handleDeploy">
|
||||
部署
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
v-access:code="['system:user:add']"
|
||||
@click="handleAdd"
|
||||
>
|
||||
{{ $t('pages.common.add') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #activityStatus="{ row }">
|
||||
<Switch
|
||||
v-model:checked="row.activityStatus"
|
||||
:checked-value="1"
|
||||
:unchecked-value="0"
|
||||
checked-children="激活"
|
||||
un-checked-children="挂起"
|
||||
@change="(status) => handleActive(row, status)"
|
||||
/>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div>
|
||||
<a-button size="small" type="link" @click="handleEdit(row)">
|
||||
编辑信息
|
||||
</a-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<a-button danger size="small" type="link" @click.stop="">
|
||||
删除流程
|
||||
</a-button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
<div>
|
||||
<a-button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="handleDesign(row, !!row.isPublish)"
|
||||
>
|
||||
{{ row.isPublish ? '查看流程' : '设计流程' }}
|
||||
</a-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
:title="`确认发布流程[${row.flowName}]?`"
|
||||
placement="left"
|
||||
@confirm="handlePublish(row)"
|
||||
>
|
||||
<a-button v-if="!row.isPublish" size="small" type="link">
|
||||
发布流程
|
||||
</a-button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
<div>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
:title="`确认复制流程[${row.flowName}]?`"
|
||||
placement="left"
|
||||
@confirm="handleCopy(row)"
|
||||
>
|
||||
<a-button size="small" type="link"> 复制流程 </a-button>
|
||||
</Popconfirm>
|
||||
<a-button size="small" type="link" @click="handleExportXml(row)">
|
||||
导出流程
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
<ProcessDefinitionModal @reload="handleReload" />
|
||||
<ProcessDefinitionDeployModal @reload="handleDeploySuccess" />
|
||||
</Page>
|
||||
</template>
|
||||
|
@@ -0,0 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
import type { UploadFile } from 'ant-design-vue/es/upload/interface';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { InBoxIcon } from '@vben/icons';
|
||||
|
||||
import { Upload } from 'ant-design-vue';
|
||||
|
||||
import { workflowDefinitionImport } from '#/api/workflow/definition';
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
const UploadDragger = Upload.Dragger;
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
onCancel: handleCancel,
|
||||
onConfirm: handleSubmit,
|
||||
});
|
||||
|
||||
const fileList = ref<UploadFile[]>([]);
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
if (fileList.value.length !== 1) {
|
||||
handleCancel();
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
file: fileList.value[0]!.originFileObj as Blob,
|
||||
category: modalApi.getData().category,
|
||||
};
|
||||
await workflowDefinitionImport(data);
|
||||
emit('reload');
|
||||
handleCancel();
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
modalApi.close();
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
modalApi.close();
|
||||
fileList.value = [];
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal
|
||||
:close-on-click-modal="false"
|
||||
:fullscreen-button="false"
|
||||
title="流程部署"
|
||||
>
|
||||
<!-- z-index不设置会遮挡模板下载loading -->
|
||||
<!-- 手动处理 而不是放入文件就上传 -->
|
||||
<UploadDragger
|
||||
v-model:file-list="fileList"
|
||||
:before-upload="() => false"
|
||||
:max-count="1"
|
||||
:show-upload-list="true"
|
||||
accept="text/json"
|
||||
>
|
||||
<p class="ant-upload-drag-icon flex items-center justify-center">
|
||||
<InBoxIcon class="text-primary size-[48px]" />
|
||||
</p>
|
||||
<p class="ant-upload-text">点击或者拖拽到此处上传[json]文件</p>
|
||||
</UploadDragger>
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,131 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { addFullName, cloneDeep, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
import {
|
||||
workflowDefinitionAdd,
|
||||
workflowDefinitionInfo,
|
||||
workflowDefinitionUpdate,
|
||||
} from '#/api/workflow/definition';
|
||||
|
||||
import { modalSchema } from './data';
|
||||
|
||||
const emit = defineEmits<{ reload: [type: 'add' | 'update'] }>();
|
||||
|
||||
const isUpdate = ref(false);
|
||||
const title = computed(() => {
|
||||
return isUpdate.value ? $t('pages.common.edit') : $t('pages.common.add');
|
||||
});
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 90,
|
||||
},
|
||||
schema: modalSchema(),
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
async function setupCategorySelect() {
|
||||
// menu
|
||||
const tree = await categoryTree();
|
||||
addFullName(tree, 'label', ' / ');
|
||||
|
||||
formApi.updateSchema([
|
||||
{
|
||||
componentProps: {
|
||||
fieldNames: {
|
||||
label: 'label',
|
||||
value: 'id',
|
||||
},
|
||||
getPopupContainer,
|
||||
// 设置弹窗滚动高度 默认256
|
||||
listHeight: 300,
|
||||
showSearch: true,
|
||||
treeData: tree,
|
||||
treeDefaultExpandAll: true,
|
||||
// 默认展开的树节点
|
||||
// treeDefaultExpandedKeys: [0],
|
||||
treeLine: { showLeafIcon: false },
|
||||
// 筛选的字段
|
||||
treeNodeFilterProp: 'label',
|
||||
treeNodeLabelProp: 'fullName',
|
||||
},
|
||||
fieldName: 'category',
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
const [BasicDrawer, modalApi] = useVbenModal({
|
||||
onCancel: handleCancel,
|
||||
onConfirm: handleConfirm,
|
||||
async onOpenChange(isOpen) {
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
modalApi.modalLoading(true);
|
||||
|
||||
const { id } = modalApi.getData() as { id?: number | string };
|
||||
isUpdate.value = !!id;
|
||||
|
||||
// 加载分类树选择
|
||||
await setupCategorySelect();
|
||||
if (isUpdate.value && id) {
|
||||
const record = await workflowDefinitionInfo(id);
|
||||
await formApi.setValues(record);
|
||||
}
|
||||
|
||||
modalApi.modalLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
async function handleConfirm() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
if (isUpdate.value) {
|
||||
await workflowDefinitionUpdate(data);
|
||||
emit('reload', 'update');
|
||||
} else {
|
||||
await workflowDefinitionAdd(data);
|
||||
emit('reload', 'add');
|
||||
}
|
||||
await handleCancel();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancel() {
|
||||
modalApi.close();
|
||||
await formApi.resetForm();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicDrawer
|
||||
:close-on-click-modal="false"
|
||||
:fullscreen-button="false"
|
||||
:title="title"
|
||||
class="w-[550px]"
|
||||
>
|
||||
<div class="min-h-[400px]">
|
||||
<BasicForm />
|
||||
</div>
|
||||
</BasicDrawer>
|
||||
</template>
|
91
apps/web-antd/src/views/workflow/processInstance/data.tsx
Normal file
91
apps/web-antd/src/views/workflow/processInstance/data.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import type { FormSchemaGetter } from '#/adapter/form';
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { DictEnum } from '@vben/constants';
|
||||
|
||||
import { OptionsTag } from '#/components/table';
|
||||
import { renderDict } from '#/utils/render';
|
||||
|
||||
import { activityStatusOptions } from '../processDefinition/constant';
|
||||
|
||||
export const querySchema: FormSchemaGetter = () => [
|
||||
{
|
||||
component: 'Input',
|
||||
label: '任务名称',
|
||||
fieldName: 'nodeName',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
label: '流程名称',
|
||||
fieldName: 'flowName',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
label: '流程编码',
|
||||
fieldName: 'flowCode',
|
||||
},
|
||||
];
|
||||
|
||||
export const columns: VxeGridProps['columns'] = [
|
||||
{ type: 'checkbox', width: 60 },
|
||||
{
|
||||
field: 'flowName',
|
||||
title: '流程名称',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'nodeName',
|
||||
title: '任务名称',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'flowCode',
|
||||
title: '流程编码',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'createByName',
|
||||
title: '申请人',
|
||||
minWidth: 150,
|
||||
},
|
||||
{
|
||||
field: 'version',
|
||||
title: '版本号',
|
||||
minWidth: 150,
|
||||
formatter: ({ cellValue }) => `V${cellValue}.0`,
|
||||
},
|
||||
{
|
||||
field: 'activityStatus',
|
||||
title: '状态',
|
||||
minWidth: 100,
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
const cellValue = row.activityStatus;
|
||||
return (
|
||||
<OptionsTag
|
||||
options={activityStatusOptions as any}
|
||||
value={cellValue}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'flowStatus',
|
||||
title: '流程状态',
|
||||
minWidth: 100,
|
||||
slots: {
|
||||
default: ({ row }) => {
|
||||
return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'action',
|
||||
fixed: 'right',
|
||||
slots: { default: 'action' },
|
||||
title: '操作',
|
||||
resizable: false,
|
||||
width: 200,
|
||||
},
|
||||
];
|
@@ -1,9 +1,235 @@
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
import type { RadioChangeEvent } from 'ant-design-vue';
|
||||
|
||||
import type { VbenFormProps } from '@vben/common-ui';
|
||||
import type { Recordable } from '@vben/types';
|
||||
|
||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { Page, useVbenModal } from '@vben/common-ui';
|
||||
import { $t } from '@vben/locales';
|
||||
import { getVxePopupContainer } from '@vben/utils';
|
||||
|
||||
import { Modal, Popconfirm, RadioGroup, Space } from 'ant-design-vue';
|
||||
|
||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||
import {
|
||||
deleteByInstanceIds,
|
||||
pageByFinish,
|
||||
pageByRunning,
|
||||
} from '#/api/workflow/instance';
|
||||
import CategoryTree from '#/views/workflow/processDefinition/category-tree.vue';
|
||||
|
||||
import { flowInfoModal } from '../components';
|
||||
import { columns, querySchema } from './data';
|
||||
import instanceInvalidModal from './instance-invalid-modal.vue';
|
||||
import instanceVariableModal from './instance-variable-modal.vue';
|
||||
|
||||
// 左边分类用
|
||||
const selectedCode = ref<number[] | string[]>([]);
|
||||
|
||||
const formOptions: VbenFormProps = {
|
||||
schema: querySchema(),
|
||||
commonConfig: {
|
||||
labelWidth: 80,
|
||||
componentProps: {
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
||||
handleReset: async () => {
|
||||
selectedCode.value = [];
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
const { formApi, reload } = tableApi;
|
||||
await formApi.resetForm();
|
||||
const formValues = formApi.form.values;
|
||||
formApi.setLatestSubmissionValues(formValues);
|
||||
await reload(formValues);
|
||||
},
|
||||
};
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '运行中', value: 'process_running' },
|
||||
{ label: '已完成', value: 'process_completed' },
|
||||
];
|
||||
let currentTypeApi = pageByRunning;
|
||||
const currentType = ref('process_running');
|
||||
async function handleTypeChange(e: RadioChangeEvent) {
|
||||
const { value } = e.target;
|
||||
switch (value) {
|
||||
case 'process_completed': {
|
||||
currentTypeApi = pageByFinish;
|
||||
break;
|
||||
}
|
||||
case 'process_running': {
|
||||
currentTypeApi = pageByRunning;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
await tableApi.reload();
|
||||
}
|
||||
|
||||
const gridOptions: VxeGridProps = {
|
||||
checkboxConfig: {
|
||||
// 高亮
|
||||
highlight: true,
|
||||
// 翻页时保留选中状态
|
||||
reserve: true,
|
||||
// 点击行选中
|
||||
trigger: 'default',
|
||||
},
|
||||
columns,
|
||||
height: 'auto',
|
||||
keepSource: true,
|
||||
pagerConfig: {},
|
||||
proxyConfig: {
|
||||
ajax: {
|
||||
query: async ({ page }, formValues = {}) => {
|
||||
// 部门树选择处理
|
||||
if (selectedCode.value.length === 1) {
|
||||
formValues.category = selectedCode.value[0];
|
||||
} else {
|
||||
Reflect.deleteProperty(formValues, 'category');
|
||||
}
|
||||
|
||||
return await currentTypeApi({
|
||||
pageNum: page.currentPage,
|
||||
pageSize: page.pageSize,
|
||||
...formValues,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
rowConfig: {
|
||||
keyField: 'id',
|
||||
height: 66,
|
||||
},
|
||||
id: 'workflow-definition-index',
|
||||
};
|
||||
|
||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
||||
formOptions,
|
||||
gridOptions,
|
||||
});
|
||||
|
||||
const [InstanceInvalidModal, instanceInvalidModalApi] = useVbenModal({
|
||||
connectedComponent: instanceInvalidModal,
|
||||
});
|
||||
async function handleInvalid(row: Recordable<any>) {
|
||||
instanceInvalidModalApi.setData({ id: row.id });
|
||||
instanceInvalidModalApi.open();
|
||||
}
|
||||
|
||||
async function handleDelete(row: Recordable<any>) {
|
||||
await deleteByInstanceIds(row.id);
|
||||
await tableApi.query();
|
||||
}
|
||||
|
||||
function handleMultiDelete() {
|
||||
const rows = tableApi.grid.getCheckboxRecords();
|
||||
const ids = rows.map((row: any) => row.id);
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
okType: 'danger',
|
||||
content: `确认删除选中的${ids.length}条记录吗?`,
|
||||
onOk: async () => {
|
||||
await deleteByInstanceIds(ids);
|
||||
await tableApi.query();
|
||||
},
|
||||
});
|
||||
}
|
||||
const [InstanceVariableModal, instanceVariableModalApi] = useVbenModal({
|
||||
connectedComponent: instanceVariableModal,
|
||||
});
|
||||
function handleVariable(row: Recordable<any>) {
|
||||
instanceVariableModalApi.setData({ record: row.variable });
|
||||
instanceVariableModalApi.open();
|
||||
}
|
||||
const [FlowInfoModal, flowInfoModalApi] = useVbenModal({
|
||||
connectedComponent: flowInfoModal,
|
||||
});
|
||||
function handleInfo(row: any) {
|
||||
console.log(row);
|
||||
flowInfoModalApi.setData({ businessId: row.businessId });
|
||||
flowInfoModalApi.open();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-[8px]">
|
||||
<CategoryTree
|
||||
v-model:select-code="selectedCode"
|
||||
class="w-[260px]"
|
||||
@reload="() => tableApi.reload()"
|
||||
@select="() => tableApi.reload()"
|
||||
/>
|
||||
<BasicTable class="flex-1 overflow-hidden">
|
||||
<template #toolbar-actions>
|
||||
<RadioGroup
|
||||
v-model:value="currentType"
|
||||
:options="typeOptions"
|
||||
button-style="solid"
|
||||
option-type="button"
|
||||
@change="handleTypeChange"
|
||||
/>
|
||||
</template>
|
||||
<template #toolbar-tools>
|
||||
<Space>
|
||||
<a-button
|
||||
:disabled="!vxeCheckboxChecked(tableApi)"
|
||||
danger
|
||||
type="primary"
|
||||
v-access:code="['system:user:remove']"
|
||||
@click="handleMultiDelete"
|
||||
>
|
||||
{{ $t('pages.common.delete') }}
|
||||
</a-button>
|
||||
</Space>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<div class="flex flex-col">
|
||||
<div v-if="currentType === 'process_running'">
|
||||
<a-button
|
||||
danger
|
||||
size="small"
|
||||
type="link"
|
||||
@click.stop="handleInvalid(row)"
|
||||
>
|
||||
作废流程
|
||||
</a-button>
|
||||
<Popconfirm
|
||||
:get-popup-container="getVxePopupContainer"
|
||||
placement="left"
|
||||
title="确认删除?"
|
||||
@confirm="handleDelete(row)"
|
||||
>
|
||||
<a-button danger size="small" type="link" @click.stop="">
|
||||
删除流程
|
||||
</a-button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
<div>
|
||||
<a-button size="small" type="link" @click.stop="handleInfo(row)">
|
||||
流程预览
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
type="link"
|
||||
@click.stop="handleVariable(row)"
|
||||
>
|
||||
变量查看
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</div>
|
||||
<InstanceInvalidModal @reload="() => tableApi.reload()" />
|
||||
<InstanceVariableModal />
|
||||
<FlowInfoModal />
|
||||
</Page>
|
||||
</template>
|
||||
|
@@ -0,0 +1,67 @@
|
||||
<script setup lang="ts">
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { workflowInstanceInvalid } from '#/api/workflow/instance';
|
||||
|
||||
const emit = defineEmits<{ reload: [] }>();
|
||||
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
onConfirm: handleSubmit,
|
||||
onCancel: handleCancel,
|
||||
fullscreenButton: false,
|
||||
title: '作废原因',
|
||||
});
|
||||
|
||||
const [BasicForm, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
formItemClass: 'col-span-2',
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
labelWidth: 80,
|
||||
},
|
||||
layout: 'vertical',
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'comment',
|
||||
label: '作废原因',
|
||||
component: 'Textarea',
|
||||
},
|
||||
],
|
||||
showDefaultActions: false,
|
||||
wrapperClass: 'grid-cols-2',
|
||||
});
|
||||
|
||||
async function handleCancel() {
|
||||
modalApi.close();
|
||||
await formApi.resetForm();
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
modalApi.modalLoading(true);
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
const data = cloneDeep(await formApi.getValues());
|
||||
data.id = modalApi.getData().id;
|
||||
await workflowInstanceInvalid(data as any);
|
||||
emit('reload');
|
||||
handleCancel();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
modalApi.modalLoading(false);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<BasicForm />
|
||||
</BasicModal>
|
||||
</template>
|
@@ -0,0 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { JsonPreview, useVbenModal } from '@vben/common-ui';
|
||||
|
||||
const data = ref({});
|
||||
const [BasicModal, modalApi] = useVbenModal({
|
||||
title: '流程变量',
|
||||
fullscreenButton: false,
|
||||
footer: false,
|
||||
onOpenChange: (visible) => {
|
||||
if (!visible) {
|
||||
data.value = {};
|
||||
return null;
|
||||
}
|
||||
const recordString = modalApi.getData().record;
|
||||
data.value = JSON.parse(recordString);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BasicModal>
|
||||
<div class="min-h-[400px] overflow-y-auto">
|
||||
<JsonPreview :data="data" />
|
||||
</div>
|
||||
</BasicModal>
|
||||
</template>
|
@@ -1,9 +1,357 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Empty,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
Popover,
|
||||
Segmented,
|
||||
Spin,
|
||||
Tooltip,
|
||||
TreeSelect,
|
||||
} from 'ant-design-vue';
|
||||
import { cloneDeep, debounce, uniqueId } from 'lodash-es';
|
||||
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
import { pageByAllTaskFinish, pageByAllTaskWait } from '#/api/workflow/task';
|
||||
|
||||
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||
|
||||
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
/**
|
||||
* 流程监控 - 待办任务页面的id不唯一 改为前端处理
|
||||
*/
|
||||
interface TaskItem extends TaskInfo {
|
||||
active: boolean;
|
||||
randomId: string;
|
||||
}
|
||||
|
||||
const taskList = ref<TaskItem[]>([]);
|
||||
const taskTotal = ref(0);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
|
||||
const typeOptions = [
|
||||
{ label: '待办任务', value: 'todo' },
|
||||
{ label: '已办任务', value: 'done' },
|
||||
];
|
||||
const currentType = ref('todo');
|
||||
const currentApi = computed(() => {
|
||||
if (currentType.value === 'todo') {
|
||||
return pageByAllTaskWait;
|
||||
}
|
||||
return pageByAllTaskFinish;
|
||||
});
|
||||
const approvalType = computed(() => {
|
||||
if (currentType.value === 'done') {
|
||||
return 'readonly';
|
||||
}
|
||||
return 'admin';
|
||||
});
|
||||
async function handleTypeChange() {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
page.value = 1;
|
||||
|
||||
taskList.value = [];
|
||||
await nextTick();
|
||||
await reload(true);
|
||||
}
|
||||
|
||||
const defaultFormData = {
|
||||
flowName: '', // 流程定义名称
|
||||
nodeName: '', // 任务名称
|
||||
flowCode: '', // 流程定义编码
|
||||
createByIds: [] as string[], // 创建人
|
||||
category: null as null | number, // 流程分类
|
||||
};
|
||||
const formData = ref(cloneDeep(defaultFormData));
|
||||
|
||||
/**
|
||||
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||
*/
|
||||
const isLoadComplete = computed(
|
||||
() => taskList.value.length === taskTotal.value,
|
||||
);
|
||||
|
||||
// 卡片父容器的ref
|
||||
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||
|
||||
/**
|
||||
* @param resetFields 是否清空查询参数
|
||||
*/
|
||||
async function reload(resetFields: boolean = false) {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
|
||||
page.value = 1;
|
||||
currentTask.value = undefined;
|
||||
taskTotal.value = 0;
|
||||
lastSelectId.value = '';
|
||||
|
||||
if (resetFields) {
|
||||
formData.value = cloneDeep(defaultFormData);
|
||||
selectedUserList.value = [];
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const resp = await currentApi.value({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value = resp.rows.map((item) => ({
|
||||
...item,
|
||||
active: false,
|
||||
randomId: uniqueId(),
|
||||
}));
|
||||
taskTotal.value = resp.total;
|
||||
|
||||
loading.value = false;
|
||||
// 默认选中第一个
|
||||
if (taskList.value.length > 0) {
|
||||
const firstTask = taskList.value[0]!;
|
||||
currentTask.value = firstTask;
|
||||
handleCardClick(firstTask);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
const handleScroll = debounce(async (e: Event) => {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
// e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
|
||||
// e.target.clientHeight 是元素的可视高度。
|
||||
// e.target.scrollHeight 是元素的总高度。
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||
// 判断是否滚动到底部
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight;
|
||||
|
||||
// 滚动到底部且没有加载完成
|
||||
if (isBottom && !isLoadComplete.value) {
|
||||
loading.value = true;
|
||||
page.value += 1;
|
||||
const resp = await currentApi.value({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value.push(
|
||||
...resp.rows.map((item) => ({
|
||||
...item,
|
||||
active: false,
|
||||
randomId: uniqueId(),
|
||||
})),
|
||||
);
|
||||
loading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const lastSelectId = ref('');
|
||||
const currentTask = ref<TaskInfo>();
|
||||
async function handleCardClick(item: TaskItem) {
|
||||
const { randomId } = item;
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === randomId) {
|
||||
return;
|
||||
}
|
||||
currentTask.value = item;
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
taskList.value.forEach((item) => {
|
||||
item.active = item.randomId === randomId;
|
||||
});
|
||||
lastSelectId.value = randomId;
|
||||
}
|
||||
|
||||
const { refreshTab } = useTabs();
|
||||
|
||||
// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示
|
||||
const popoverOpen = ref(false);
|
||||
const selectedUserList = ref<User[]>([]);
|
||||
function handleFinish(userList: User[]) {
|
||||
popoverOpen.value = true;
|
||||
selectedUserList.value = userList;
|
||||
formData.value.createByIds = userList.map((item) => item.userId);
|
||||
}
|
||||
|
||||
const treeData = ref<any[]>([]);
|
||||
onMounted(async () => {
|
||||
// menu
|
||||
const tree = await categoryTree();
|
||||
addFullName(tree, 'label', ' / ');
|
||||
treeData.value = tree;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div
|
||||
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||
>
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<Segmented
|
||||
v-model:value="currentType"
|
||||
:options="typeOptions"
|
||||
block
|
||||
class="mb-2"
|
||||
@change="handleTypeChange"
|
||||
/>
|
||||
<div class="flex items-center gap-1">
|
||||
<InputSearch
|
||||
v-model:value="formData.flowName"
|
||||
placeholder="流程名称搜索"
|
||||
@search="reload(false)"
|
||||
/>
|
||||
<Tooltip placement="top" title="重置">
|
||||
<a-button @click="reload(true)">
|
||||
<RedoOutlined />
|
||||
</a-button>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
v-model:open="popoverOpen"
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="rightTop"
|
||||
trigger="click"
|
||||
>
|
||||
<template #title>
|
||||
<div class="w-full border-b pb-[12px] text-[16px]">搜索</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<Form
|
||||
:colon="false"
|
||||
:label-col="{ span: 6 }"
|
||||
:model="formData"
|
||||
autocomplete="off"
|
||||
class="w-[300px]"
|
||||
@finish="() => reload(false)"
|
||||
>
|
||||
<FormItem label="申请人">
|
||||
<!-- 弹窗关闭后仍然显示表单浮层 -->
|
||||
<CopyComponent
|
||||
v-model:user-list="selectedUserList"
|
||||
@cancel="() => (popoverOpen = true)"
|
||||
@finish="handleFinish"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程分类">
|
||||
<TreeSelect
|
||||
v-model:value="formData.category"
|
||||
:allow-clear="true"
|
||||
:field-names="{ label: 'label', value: 'id' }"
|
||||
:get-popup-container="getPopupContainer"
|
||||
:tree-data="treeData"
|
||||
:tree-default-expand-all="true"
|
||||
:tree-line="{ showLeafIcon: false }"
|
||||
placeholder="请选择"
|
||||
tree-node-filter-prop="label"
|
||||
tree-node-label-prop="fullName"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="任务名称">
|
||||
<Input
|
||||
v-model:value="formData.nodeName"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程编码">
|
||||
<Input
|
||||
v-model:value="formData.flowCode"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<div class="flex">
|
||||
<a-button block html-type="submit" type="primary">
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button block class="ml-2" @click="reload(true)">
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</template>
|
||||
<a-button>
|
||||
<FilterOutlined />
|
||||
</a-button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="cardContainerRef"
|
||||
class="thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<template v-if="taskList.length > 0">
|
||||
<ApprovalCard
|
||||
v-for="item in taskList"
|
||||
:key="item.randomId"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
row-key="randomId"
|
||||
@click="handleCardClick(item)"
|
||||
/>
|
||||
</template>
|
||||
<Empty v-else :image="emptyImage" />
|
||||
<div
|
||||
v-if="isLoadComplete && taskList.length > 0"
|
||||
class="flex items-center justify-center text-[14px] opacity-50"
|
||||
>
|
||||
没有更多数据了
|
||||
</div>
|
||||
<!-- 遮罩loading层 -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]"
|
||||
>
|
||||
<Spin tip="加载中..." />
|
||||
</div>
|
||||
</div>
|
||||
<!-- total显示 -->
|
||||
<div
|
||||
class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
共 {{ taskTotal }} 条记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ApprovalPanel
|
||||
:task="currentTask"
|
||||
:type="approvalType"
|
||||
@reload="refreshTab"
|
||||
/>
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.thin-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
@apply thin-scrollbar;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,9 +1,256 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
import { getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Empty,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
Popover,
|
||||
Spin,
|
||||
Tooltip,
|
||||
} from 'ant-design-vue';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
|
||||
import { pageByCurrent } from '#/api/workflow/instance';
|
||||
|
||||
import { ApprovalCard, ApprovalPanel } from '../components';
|
||||
|
||||
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
const taskList = ref<({ active: boolean } & TaskInfo)[]>([]);
|
||||
const taskTotal = ref(0);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
|
||||
const defaultFormData = {
|
||||
flowName: '', // 流程定义名称
|
||||
nodeName: '', // 任务名称
|
||||
flowCode: '', // 流程定义编码
|
||||
category: null as null | number, // 流程分类
|
||||
};
|
||||
const formData = ref(cloneDeep(defaultFormData));
|
||||
|
||||
/**
|
||||
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||
*/
|
||||
const isLoadComplete = computed(
|
||||
() => taskList.value.length === taskTotal.value,
|
||||
);
|
||||
|
||||
// 卡片父容器的ref
|
||||
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||
|
||||
/**
|
||||
* @param resetFields 是否清空查询参数
|
||||
*/
|
||||
async function reload(resetFields: boolean = false) {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
|
||||
page.value = 1;
|
||||
currentTask.value = undefined;
|
||||
taskTotal.value = 0;
|
||||
lastSelectId.value = '';
|
||||
|
||||
if (resetFields) {
|
||||
formData.value = cloneDeep(defaultFormData);
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const resp = await pageByCurrent({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
|
||||
taskTotal.value = resp.total;
|
||||
|
||||
loading.value = false;
|
||||
// 默认选中第一个
|
||||
if (taskList.value.length > 0) {
|
||||
const firstTask = taskList.value[0]!;
|
||||
currentTask.value = firstTask;
|
||||
handleCardClick(firstTask);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
const handleScroll = debounce(async (e: Event) => {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
// e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
|
||||
// e.target.clientHeight 是元素的可视高度。
|
||||
// e.target.scrollHeight 是元素的总高度。
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||
// 判断是否滚动到底部
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight;
|
||||
|
||||
// 滚动到底部且没有加载完成
|
||||
if (isBottom && !isLoadComplete.value) {
|
||||
loading.value = true;
|
||||
page.value += 1;
|
||||
const resp = await pageByCurrent({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value.push(
|
||||
...resp.rows.map((item) => ({ ...item, active: false })),
|
||||
);
|
||||
loading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const lastSelectId = ref('');
|
||||
const currentTask = ref<TaskInfo>();
|
||||
async function handleCardClick(item: TaskInfo) {
|
||||
const { id } = item;
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === id) {
|
||||
return;
|
||||
}
|
||||
currentTask.value = item;
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
taskList.value.forEach((item) => {
|
||||
item.active = item.id === id;
|
||||
});
|
||||
lastSelectId.value = id;
|
||||
}
|
||||
|
||||
const { refreshTab } = useTabs();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div
|
||||
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||
>
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<InputSearch
|
||||
v-model:value="formData.flowName"
|
||||
placeholder="流程名称搜索"
|
||||
@search="reload(false)"
|
||||
/>
|
||||
<Tooltip placement="top" title="重置">
|
||||
<a-button @click="reload(true)">
|
||||
<RedoOutlined />
|
||||
</a-button>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="rightTop"
|
||||
trigger="click"
|
||||
>
|
||||
<template #title>
|
||||
<div class="w-full border-b pb-[12px] text-[16px]">搜索</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<Form
|
||||
:colon="false"
|
||||
:label-col="{ span: 6 }"
|
||||
:model="formData"
|
||||
autocomplete="off"
|
||||
class="w-[300px]"
|
||||
@finish="() => reload(false)"
|
||||
>
|
||||
<FormItem label="任务名称">
|
||||
<Input
|
||||
v-model:value="formData.nodeName"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程编码">
|
||||
<Input
|
||||
v-model:value="formData.flowCode"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<div class="flex">
|
||||
<a-button block html-type="submit" type="primary">
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button block class="ml-2" @click="reload(true)">
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</template>
|
||||
<a-button>
|
||||
<FilterOutlined />
|
||||
</a-button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="cardContainerRef"
|
||||
class="thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<template v-if="taskList.length > 0">
|
||||
<ApprovalCard
|
||||
v-for="item in taskList"
|
||||
:key="item.id"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
@click="handleCardClick(item)"
|
||||
/>
|
||||
</template>
|
||||
<Empty v-else :image="emptyImage" />
|
||||
<div
|
||||
v-if="isLoadComplete && taskList.length > 0"
|
||||
class="flex items-center justify-center text-[14px] opacity-50"
|
||||
>
|
||||
没有更多数据了
|
||||
</div>
|
||||
<!-- 遮罩loading层 -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]"
|
||||
>
|
||||
<Spin tip="加载中..." />
|
||||
</div>
|
||||
</div>
|
||||
<!-- total显示 -->
|
||||
<div
|
||||
class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
共 {{ taskTotal }} 条记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ApprovalPanel :task="currentTask" type="myself" @reload="refreshTab" />
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.thin-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
@apply thin-scrollbar;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,9 +1,298 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Empty,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
Popover,
|
||||
Spin,
|
||||
Tooltip,
|
||||
TreeSelect,
|
||||
} from 'ant-design-vue';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
import { pageByTaskCopy } from '#/api/workflow/task';
|
||||
|
||||
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||
|
||||
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
const taskList = ref<({ active: boolean } & TaskInfo)[]>([]);
|
||||
const taskTotal = ref(0);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
|
||||
const defaultFormData = {
|
||||
flowName: '', // 流程定义名称
|
||||
nodeName: '', // 任务名称
|
||||
flowCode: '', // 流程定义编码
|
||||
createByIds: [] as string[], // 创建人
|
||||
category: null as null | number, // 流程分类
|
||||
};
|
||||
const formData = ref(cloneDeep(defaultFormData));
|
||||
|
||||
/**
|
||||
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||
*/
|
||||
const isLoadComplete = computed(
|
||||
() => taskList.value.length === taskTotal.value,
|
||||
);
|
||||
|
||||
// 卡片父容器的ref
|
||||
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||
|
||||
/**
|
||||
* @param resetFields 是否清空查询参数
|
||||
*/
|
||||
async function reload(resetFields: boolean = false) {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
|
||||
page.value = 1;
|
||||
currentTask.value = undefined;
|
||||
taskTotal.value = 0;
|
||||
lastSelectId.value = '';
|
||||
|
||||
if (resetFields) {
|
||||
formData.value = cloneDeep(defaultFormData);
|
||||
selectedUserList.value = [];
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const resp = await pageByTaskCopy({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
|
||||
taskTotal.value = resp.total;
|
||||
|
||||
loading.value = false;
|
||||
// 默认选中第一个
|
||||
if (taskList.value.length > 0) {
|
||||
const firstTask = taskList.value[0]!;
|
||||
currentTask.value = firstTask;
|
||||
handleCardClick(firstTask);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
const handleScroll = debounce(async (e: Event) => {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
// e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
|
||||
// e.target.clientHeight 是元素的可视高度。
|
||||
// e.target.scrollHeight 是元素的总高度。
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||
// 判断是否滚动到底部
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight;
|
||||
|
||||
// 滚动到底部且没有加载完成
|
||||
if (isBottom && !isLoadComplete.value) {
|
||||
loading.value = true;
|
||||
page.value += 1;
|
||||
const resp = await pageByTaskCopy({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value.push(
|
||||
...resp.rows.map((item) => ({ ...item, active: false })),
|
||||
);
|
||||
loading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const lastSelectId = ref('');
|
||||
const currentTask = ref<TaskInfo>();
|
||||
async function handleCardClick(item: TaskInfo) {
|
||||
const { id } = item;
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === id) {
|
||||
return;
|
||||
}
|
||||
currentTask.value = item;
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
taskList.value.forEach((item) => {
|
||||
item.active = item.id === id;
|
||||
});
|
||||
lastSelectId.value = id;
|
||||
}
|
||||
|
||||
// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示
|
||||
const popoverOpen = ref(false);
|
||||
const selectedUserList = ref<User[]>([]);
|
||||
function handleFinish(userList: User[]) {
|
||||
popoverOpen.value = true;
|
||||
selectedUserList.value = userList;
|
||||
formData.value.createByIds = userList.map((item) => item.userId);
|
||||
}
|
||||
|
||||
const treeData = ref<any[]>([]);
|
||||
onMounted(async () => {
|
||||
// menu
|
||||
const tree = await categoryTree();
|
||||
addFullName(tree, 'label', ' / ');
|
||||
treeData.value = tree;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div
|
||||
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||
>
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<InputSearch
|
||||
v-model:value="formData.flowName"
|
||||
placeholder="流程名称搜索"
|
||||
@search="reload(false)"
|
||||
/>
|
||||
<Tooltip placement="top" title="重置">
|
||||
<a-button @click="reload(true)">
|
||||
<RedoOutlined />
|
||||
</a-button>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
v-model:open="popoverOpen"
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="rightTop"
|
||||
trigger="click"
|
||||
>
|
||||
<template #title>
|
||||
<div class="w-full border-b pb-[12px] text-[16px]">搜索</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<Form
|
||||
:colon="false"
|
||||
:label-col="{ span: 6 }"
|
||||
:model="formData"
|
||||
autocomplete="off"
|
||||
class="w-[300px]"
|
||||
@finish="() => reload(false)"
|
||||
>
|
||||
<FormItem label="申请人">
|
||||
<!-- 弹窗关闭后仍然显示表单浮层 -->
|
||||
<CopyComponent
|
||||
v-model:user-list="selectedUserList"
|
||||
@cancel="() => (popoverOpen = true)"
|
||||
@finish="handleFinish"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程分类">
|
||||
<TreeSelect
|
||||
v-model:value="formData.category"
|
||||
:allow-clear="true"
|
||||
:field-names="{ label: 'label', value: 'id' }"
|
||||
:get-popup-container="getPopupContainer"
|
||||
:tree-data="treeData"
|
||||
:tree-default-expand-all="true"
|
||||
:tree-line="{ showLeafIcon: false }"
|
||||
placeholder="请选择"
|
||||
tree-node-filter-prop="label"
|
||||
tree-node-label-prop="fullName"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="任务名称">
|
||||
<Input
|
||||
v-model:value="formData.nodeName"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程编码">
|
||||
<Input
|
||||
v-model:value="formData.flowCode"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<div class="flex">
|
||||
<a-button block html-type="submit" type="primary">
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button block class="ml-2" @click="reload(true)">
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</template>
|
||||
<a-button>
|
||||
<FilterOutlined />
|
||||
</a-button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="cardContainerRef"
|
||||
class="thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<template v-if="taskList.length > 0">
|
||||
<ApprovalCard
|
||||
v-for="item in taskList"
|
||||
:key="item.id"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
@click="handleCardClick(item)"
|
||||
/>
|
||||
</template>
|
||||
<Empty v-else :image="emptyImage" />
|
||||
<div
|
||||
v-if="isLoadComplete && taskList.length > 0"
|
||||
class="flex items-center justify-center text-[14px] opacity-50"
|
||||
>
|
||||
没有更多数据了
|
||||
</div>
|
||||
<!-- 遮罩loading层 -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]"
|
||||
>
|
||||
<Spin tip="加载中..." />
|
||||
</div>
|
||||
</div>
|
||||
<!-- total显示 -->
|
||||
<div
|
||||
class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
共 {{ taskTotal }} 条记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ApprovalPanel :task="currentTask" type="readonly" />
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.thin-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
@apply thin-scrollbar;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,9 +1,298 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import CommonSkeleton from '#/views/common';
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Empty,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
Popover,
|
||||
Spin,
|
||||
Tooltip,
|
||||
TreeSelect,
|
||||
} from 'ant-design-vue';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
import { pageByTaskFinish } from '#/api/workflow/task';
|
||||
|
||||
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||
|
||||
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
const taskList = ref<({ active: boolean } & TaskInfo)[]>([]);
|
||||
const taskTotal = ref(0);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
|
||||
const defaultFormData = {
|
||||
flowName: '', // 流程定义名称
|
||||
nodeName: '', // 任务名称
|
||||
flowCode: '', // 流程定义编码
|
||||
createByIds: [] as string[], // 创建人
|
||||
category: null as null | number, // 流程分类
|
||||
};
|
||||
const formData = ref(cloneDeep(defaultFormData));
|
||||
|
||||
/**
|
||||
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||
*/
|
||||
const isLoadComplete = computed(
|
||||
() => taskList.value.length === taskTotal.value,
|
||||
);
|
||||
|
||||
// 卡片父容器的ref
|
||||
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||
|
||||
/**
|
||||
* @param resetFields 是否清空查询参数
|
||||
*/
|
||||
async function reload(resetFields: boolean = false) {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
|
||||
page.value = 1;
|
||||
currentTask.value = undefined;
|
||||
taskTotal.value = 0;
|
||||
lastSelectId.value = '';
|
||||
|
||||
if (resetFields) {
|
||||
formData.value = cloneDeep(defaultFormData);
|
||||
selectedUserList.value = [];
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const resp = await pageByTaskFinish({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
|
||||
taskTotal.value = resp.total;
|
||||
|
||||
loading.value = false;
|
||||
// 默认选中第一个
|
||||
if (taskList.value.length > 0) {
|
||||
const firstTask = taskList.value[0]!;
|
||||
currentTask.value = firstTask;
|
||||
handleCardClick(firstTask);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
const handleScroll = debounce(async (e: Event) => {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
// e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
|
||||
// e.target.clientHeight 是元素的可视高度。
|
||||
// e.target.scrollHeight 是元素的总高度。
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||
// 判断是否滚动到底部
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight;
|
||||
|
||||
// 滚动到底部且没有加载完成
|
||||
if (isBottom && !isLoadComplete.value) {
|
||||
loading.value = true;
|
||||
page.value += 1;
|
||||
const resp = await pageByTaskFinish({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value.push(
|
||||
...resp.rows.map((item) => ({ ...item, active: false })),
|
||||
);
|
||||
loading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const lastSelectId = ref('');
|
||||
const currentTask = ref<TaskInfo>();
|
||||
async function handleCardClick(item: TaskInfo) {
|
||||
const { id } = item;
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === id) {
|
||||
return;
|
||||
}
|
||||
currentTask.value = item;
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
taskList.value.forEach((item) => {
|
||||
item.active = item.id === id;
|
||||
});
|
||||
lastSelectId.value = id;
|
||||
}
|
||||
|
||||
// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示
|
||||
const popoverOpen = ref(false);
|
||||
const selectedUserList = ref<User[]>([]);
|
||||
function handleFinish(userList: User[]) {
|
||||
popoverOpen.value = true;
|
||||
selectedUserList.value = userList;
|
||||
formData.value.createByIds = userList.map((item) => item.userId);
|
||||
}
|
||||
|
||||
const treeData = ref<any[]>([]);
|
||||
onMounted(async () => {
|
||||
// menu
|
||||
const tree = await categoryTree();
|
||||
addFullName(tree, 'label', ' / ');
|
||||
treeData.value = tree;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<CommonSkeleton />
|
||||
</div>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div
|
||||
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||
>
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<div class="flex items-center gap-1">
|
||||
<InputSearch
|
||||
v-model:value="formData.flowName"
|
||||
placeholder="流程名称搜索"
|
||||
@search="reload(false)"
|
||||
/>
|
||||
<Tooltip placement="top" title="重置">
|
||||
<a-button @click="reload(true)">
|
||||
<RedoOutlined />
|
||||
</a-button>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
v-model:open="popoverOpen"
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="rightTop"
|
||||
trigger="click"
|
||||
>
|
||||
<template #title>
|
||||
<div class="w-full border-b pb-[12px] text-[16px]">搜索</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<Form
|
||||
:colon="false"
|
||||
:label-col="{ span: 6 }"
|
||||
:model="formData"
|
||||
autocomplete="off"
|
||||
class="w-[300px]"
|
||||
@finish="() => reload(false)"
|
||||
>
|
||||
<FormItem label="申请人">
|
||||
<!-- 弹窗关闭后仍然显示表单浮层 -->
|
||||
<CopyComponent
|
||||
v-model:user-list="selectedUserList"
|
||||
@cancel="() => (popoverOpen = true)"
|
||||
@finish="handleFinish"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程分类">
|
||||
<TreeSelect
|
||||
v-model:value="formData.category"
|
||||
:allow-clear="true"
|
||||
:field-names="{ label: 'label', value: 'id' }"
|
||||
:get-popup-container="getPopupContainer"
|
||||
:tree-data="treeData"
|
||||
:tree-default-expand-all="true"
|
||||
:tree-line="{ showLeafIcon: false }"
|
||||
placeholder="请选择"
|
||||
tree-node-filter-prop="label"
|
||||
tree-node-label-prop="fullName"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="任务名称">
|
||||
<Input
|
||||
v-model:value="formData.nodeName"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程编码">
|
||||
<Input
|
||||
v-model:value="formData.flowCode"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<div class="flex">
|
||||
<a-button block html-type="submit" type="primary">
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button block class="ml-2" @click="reload(true)">
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</template>
|
||||
<a-button>
|
||||
<FilterOutlined />
|
||||
</a-button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="cardContainerRef"
|
||||
class="thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<template v-if="taskList.length > 0">
|
||||
<ApprovalCard
|
||||
v-for="item in taskList"
|
||||
:key="item.id"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
@click="handleCardClick(item)"
|
||||
/>
|
||||
</template>
|
||||
<Empty v-else :image="emptyImage" />
|
||||
<div
|
||||
v-if="isLoadComplete && taskList.length > 0"
|
||||
class="flex items-center justify-center text-[14px] opacity-50"
|
||||
>
|
||||
没有更多数据了
|
||||
</div>
|
||||
<!-- 遮罩loading层 -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]"
|
||||
>
|
||||
<Spin tip="加载中..." />
|
||||
</div>
|
||||
</div>
|
||||
<!-- total显示 -->
|
||||
<div
|
||||
class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
共 {{ taskTotal }} 条记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ApprovalPanel :task="currentTask" type="readonly" />
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.thin-scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
@apply thin-scrollbar;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,25 +1,97 @@
|
||||
<!-- eslint-disable no-use-before-define -->
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue';
|
||||
import type { User } from '#/api/system/user/model';
|
||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||
|
||||
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
import { useTabs } from '@vben/hooks';
|
||||
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||
|
||||
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Card,
|
||||
Divider,
|
||||
Empty,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
Space,
|
||||
TabPane,
|
||||
Tabs,
|
||||
Tag,
|
||||
Popover,
|
||||
Spin,
|
||||
Tooltip,
|
||||
TreeSelect,
|
||||
} from 'ant-design-vue';
|
||||
import { debounce, uniqueId } from 'lodash-es';
|
||||
import { cloneDeep, debounce } from 'lodash-es';
|
||||
|
||||
import { ApprovalCard, ApprovalTimeline } from '../components';
|
||||
import RejectionPng from '../components/rejection.png';
|
||||
import { categoryTree } from '#/api/workflow/category';
|
||||
import { pageByTaskWait } from '#/api/workflow/task';
|
||||
|
||||
const handleScroll = debounce((e: Event) => {
|
||||
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||
|
||||
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||
|
||||
const taskList = ref<({ active: boolean } & TaskInfo)[]>([]);
|
||||
const taskTotal = ref(0);
|
||||
const page = ref(1);
|
||||
const loading = ref(false);
|
||||
|
||||
const defaultFormData = {
|
||||
flowName: '', // 流程定义名称
|
||||
nodeName: '', // 任务名称
|
||||
flowCode: '', // 流程定义编码
|
||||
createByIds: [] as string[], // 创建人
|
||||
category: null as null | number, // 流程分类
|
||||
};
|
||||
const formData = ref(cloneDeep(defaultFormData));
|
||||
|
||||
/**
|
||||
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||
*/
|
||||
const isLoadComplete = computed(
|
||||
() => taskList.value.length === taskTotal.value,
|
||||
);
|
||||
|
||||
// 卡片父容器的ref
|
||||
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||
|
||||
/**
|
||||
* @param resetFields 是否清空查询参数
|
||||
*/
|
||||
async function reload(resetFields: boolean = false) {
|
||||
// 需要先滚动到顶部
|
||||
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||
|
||||
page.value = 1;
|
||||
currentTask.value = undefined;
|
||||
taskTotal.value = 0;
|
||||
lastSelectId.value = '';
|
||||
|
||||
if (resetFields) {
|
||||
formData.value = cloneDeep(defaultFormData);
|
||||
selectedUserList.value = [];
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
const resp = await pageByTaskWait({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value = resp.rows.map((item) => ({ ...item, active: false }));
|
||||
taskTotal.value = resp.total;
|
||||
|
||||
loading.value = false;
|
||||
// 默认选中第一个
|
||||
if (taskList.value.length > 0) {
|
||||
const firstTask = taskList.value[0]!;
|
||||
currentTask.value = firstTask;
|
||||
handleCardClick(firstTask);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(reload);
|
||||
|
||||
const handleScroll = debounce(async (e: Event) => {
|
||||
if (!e.target) {
|
||||
return;
|
||||
}
|
||||
@@ -29,132 +101,189 @@ const handleScroll = debounce((e: Event) => {
|
||||
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||
// 判断是否滚动到底部
|
||||
const isBottom = scrollTop + clientHeight >= scrollHeight;
|
||||
console.log(isBottom);
|
||||
// console.log(scrollTop + clientHeight);
|
||||
// console.log(scrollHeight);
|
||||
|
||||
// 滚动到底部且没有加载完成
|
||||
if (isBottom && !isLoadComplete.value) {
|
||||
loading.value = true;
|
||||
page.value += 1;
|
||||
const resp = await pageByTaskWait({
|
||||
pageSize: 10,
|
||||
pageNum: page.value,
|
||||
...formData.value,
|
||||
});
|
||||
taskList.value.push(
|
||||
...resp.rows.map((item) => ({ ...item, active: false })),
|
||||
);
|
||||
loading.value = false;
|
||||
}
|
||||
}, 200);
|
||||
|
||||
const data = reactive(
|
||||
Array.from({ length: 10 }).map(() => ({
|
||||
id: uniqueId(),
|
||||
startTime: '2022-01-01',
|
||||
endTime: '2022-01-02',
|
||||
title: '审批任务',
|
||||
desc: '审批任务描述',
|
||||
status: '审批中',
|
||||
active: false,
|
||||
})),
|
||||
);
|
||||
|
||||
const timeLine = Array.from({ length: 5 }).map(() => ({
|
||||
id: uniqueId(),
|
||||
name: '张三',
|
||||
status: '审批中',
|
||||
remark: '审批任务描述',
|
||||
time: '2022-01-01',
|
||||
}));
|
||||
|
||||
const lastSelectId = ref('');
|
||||
function handleCardClick(id: string) {
|
||||
const currentTask = ref<TaskInfo>();
|
||||
async function handleCardClick(item: TaskInfo) {
|
||||
const { id } = item;
|
||||
// 点击的是同一个
|
||||
if (lastSelectId.value === id) {
|
||||
return;
|
||||
}
|
||||
currentTask.value = item;
|
||||
// 反选状态 & 如果已经点击了 不变 & 保持只能有一个选中
|
||||
data.forEach((item) => {
|
||||
taskList.value.forEach((item) => {
|
||||
item.active = item.id === id;
|
||||
});
|
||||
lastSelectId.value = id;
|
||||
}
|
||||
|
||||
const { refreshTab } = useTabs();
|
||||
|
||||
// 由于失去焦点浮层会消失 使用v-model选择人员完毕后强制显示
|
||||
const popoverOpen = ref(false);
|
||||
const selectedUserList = ref<User[]>([]);
|
||||
function handleFinish(userList: User[]) {
|
||||
popoverOpen.value = true;
|
||||
selectedUserList.value = userList;
|
||||
formData.value.createByIds = userList.map((item) => item.userId);
|
||||
}
|
||||
|
||||
const treeData = ref<any[]>([]);
|
||||
onMounted(async () => {
|
||||
// menu
|
||||
const tree = await categoryTree();
|
||||
addFullName(tree, 'label', ' / ');
|
||||
treeData.value = tree;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page :auto-content-height="true">
|
||||
<div class="flex h-full gap-2">
|
||||
<div class="bg-background flex h-full w-[320px] flex-col rounded-lg">
|
||||
<div
|
||||
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||
>
|
||||
<!-- 搜索条件 -->
|
||||
<div
|
||||
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||
>
|
||||
<InputSearch placeholder="搜索" />
|
||||
<div class="flex items-center gap-1">
|
||||
<InputSearch
|
||||
v-model:value="formData.flowName"
|
||||
placeholder="流程名称搜索"
|
||||
@search="reload(false)"
|
||||
/>
|
||||
<Tooltip placement="top" title="重置">
|
||||
<a-button @click="reload(true)">
|
||||
<RedoOutlined />
|
||||
</a-button>
|
||||
</Tooltip>
|
||||
<Popover
|
||||
v-model:open="popoverOpen"
|
||||
:get-popup-container="getPopupContainer"
|
||||
placement="rightTop"
|
||||
trigger="click"
|
||||
>
|
||||
<template #title>
|
||||
<div class="w-full border-b pb-[12px] text-[16px]">搜索</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<Form
|
||||
:colon="false"
|
||||
:label-col="{ span: 6 }"
|
||||
:model="formData"
|
||||
autocomplete="off"
|
||||
class="w-[300px]"
|
||||
@finish="() => reload(false)"
|
||||
>
|
||||
<FormItem label="申请人">
|
||||
<!-- 弹窗关闭后仍然显示表单浮层 -->
|
||||
<CopyComponent
|
||||
v-model:user-list="selectedUserList"
|
||||
@cancel="() => (popoverOpen = true)"
|
||||
@finish="handleFinish"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程分类">
|
||||
<TreeSelect
|
||||
v-model:value="formData.category"
|
||||
:allow-clear="true"
|
||||
:field-names="{ label: 'label', value: 'id' }"
|
||||
:get-popup-container="getPopupContainer"
|
||||
:tree-data="treeData"
|
||||
:tree-default-expand-all="true"
|
||||
:tree-line="{ showLeafIcon: false }"
|
||||
placeholder="请选择"
|
||||
tree-node-filter-prop="label"
|
||||
tree-node-label-prop="fullName"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="任务名称">
|
||||
<Input
|
||||
v-model:value="formData.nodeName"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="流程编码">
|
||||
<Input
|
||||
v-model:value="formData.flowCode"
|
||||
placeholder="请输入"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<div class="flex">
|
||||
<a-button block html-type="submit" type="primary">
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button block class="ml-2" @click="reload(true)">
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</template>
|
||||
<a-button>
|
||||
<FilterOutlined />
|
||||
</a-button>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="cardContainerRef"
|
||||
class="thin-scrollbar flex flex-1 flex-col gap-2 overflow-y-auto py-3"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<ApprovalCard
|
||||
v-for="item in data"
|
||||
:key="item.id"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
@click="handleCardClick"
|
||||
/>
|
||||
<template v-if="taskList.length > 0">
|
||||
<ApprovalCard
|
||||
v-for="item in taskList"
|
||||
:key="item.id"
|
||||
:info="item"
|
||||
class="mx-2"
|
||||
@click="handleCardClick(item)"
|
||||
/>
|
||||
</template>
|
||||
<Empty v-else :image="emptyImage" />
|
||||
<div
|
||||
v-if="isLoadComplete && taskList.length > 0"
|
||||
class="flex items-center justify-center text-[14px] opacity-50"
|
||||
>
|
||||
没有更多数据了
|
||||
</div>
|
||||
<!-- 遮罩loading层 -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="absolute left-0 top-0 flex h-full w-full items-center justify-center bg-[rgba(0,0,0,0.1)]"
|
||||
>
|
||||
<Spin tip="加载中..." />
|
||||
</div>
|
||||
</div>
|
||||
<!-- total显示 -->
|
||||
<div
|
||||
class="bg-background sticky bottom-0 w-full rounded-b-lg border-t-[1px] py-2"
|
||||
>
|
||||
<div class="flex items-center justify-center">
|
||||
共 {{ data.length }} 条记录
|
||||
共 {{ taskTotal }} 条记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Card
|
||||
:body-style="{ overflowY: 'auto', height: '100%' }"
|
||||
class="thin-scrollbar flex-1 overflow-y-hidden"
|
||||
size="small"
|
||||
title="编号: 1234567890123456789012"
|
||||
>
|
||||
<div class="flex flex-col gap-5 p-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-2xl font-bold">报销申请</div>
|
||||
<div>
|
||||
<Tag color="warning">申请中</Tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar
|
||||
size="small"
|
||||
src="https://plus.dapdap.top/minio-server/plus/2024/11/21/925ed278e2d441beb7f695b41e13c4dd.jpg"
|
||||
/>
|
||||
<span>疯狂的牛子Li</span>
|
||||
<div class="flex items-center opacity-50">
|
||||
<span>XXXX有限公司</span>
|
||||
<Divider type="vertical" />
|
||||
<span>提交于: 2022-01-01 12:00:00</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧图标 -->
|
||||
<div class="z-100 absolute right-3 top-3">
|
||||
<img :src="RejectionPng" class="size-[96px]" />
|
||||
</div>
|
||||
</div>
|
||||
<Tabs class="flex-1">
|
||||
<TabPane key="1" tab="审批详情">
|
||||
<div class="h-fulloverflow-y-auto">
|
||||
<Alert message="该页面仅为静态页 后期可能会用到!" type="info" />
|
||||
<Divider />
|
||||
<ApprovalTimeline :list="timeLine" />
|
||||
</div>
|
||||
</TabPane>
|
||||
<TabPane key="2" tab="审批记录">审批记录</TabPane>
|
||||
<TabPane key="3" tab="全文评论(999+)">全文评论</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
<!-- 固定底部 -->
|
||||
<div
|
||||
class="border-t-solid bg-background absolute bottom-0 left-0 w-full border-t-[1px] p-3"
|
||||
>
|
||||
<div class="flex justify-end">
|
||||
<Space>
|
||||
<a-button type="primary">通过</a-button>
|
||||
<a-button danger type="primary">驳回</a-button>
|
||||
<a-button>其他</a-button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<ApprovalPanel :task="currentTask" type="approve" @reload="refreshTab" />
|
||||
</div>
|
||||
</Page>
|
||||
</template>
|
||||
|
Reference in New Issue
Block a user