mirror of
https://gitee.com/dapppp/ruoyi-plus-vben5.git
synced 2026-04-19 22:58:33 +08:00
Compare commits
19 Commits
flow-refac
...
1.5.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c39bef181 | ||
|
|
86bcceaa84 | ||
|
|
1980a2482d | ||
|
|
a38cf80ea4 | ||
|
|
a986e1a2ab | ||
|
|
9822d2af8a | ||
|
|
b51f5d1fa6 | ||
|
|
56104b2abf | ||
|
|
b4ca3f43a9 | ||
|
|
0666483c58 | ||
|
|
77c45d855b | ||
|
|
8ce52eef51 | ||
|
|
738a918df6 | ||
|
|
d9131cbe22 | ||
|
|
968a2eb7b6 | ||
|
|
9b59a8acdb | ||
|
|
ab756b3434 | ||
|
|
e23e5cd5a8 | ||
|
|
c3033d66bd |
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,3 +1,24 @@
|
|||||||
|
# 1.5.2
|
||||||
|
|
||||||
|
对应后端版本 单体/微服务: 5.5.1/2.5.1
|
||||||
|
|
||||||
|
该版本后端功能值包含一个`同步租户参数配置`功能 旧版本也能升级(使用)
|
||||||
|
|
||||||
|
**REFACTOR**
|
||||||
|
|
||||||
|
- 流程相关代码重构 移除之前的历史代码
|
||||||
|
|
||||||
|
**FEATURES**
|
||||||
|
|
||||||
|
- 修改流程变量
|
||||||
|
- 租户管理 同步租户参数配置
|
||||||
|
|
||||||
|
**BUG FIX**
|
||||||
|
|
||||||
|
- 菜单管理 新增没有加载下拉选择api
|
||||||
|
- v-access:role指令错误判断code而非role
|
||||||
|
- modal/drawer里使用列配置 重置列弹窗被遮挡
|
||||||
|
|
||||||
# 1.5.1
|
# 1.5.1
|
||||||
|
|
||||||
对应后端版本 单体/微服务: 5.5.0/2.5.0
|
对应后端版本 单体/微服务: 5.5.0/2.5.0
|
||||||
@@ -6,6 +27,10 @@
|
|||||||
|
|
||||||
- 拖拽列宽时的颜色与primary保持一致
|
- 拖拽列宽时的颜色与primary保持一致
|
||||||
|
|
||||||
|
**OTHERS**
|
||||||
|
|
||||||
|
- 调整不同环境打包命令 兼容windows系统
|
||||||
|
|
||||||
# 1.5.0
|
# 1.5.0
|
||||||
|
|
||||||
对应后端版本 单体/微服务: 5.5.0/2.5.0
|
对应后端版本 单体/微服务: 5.5.0/2.5.0
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
v5版本采用分仓(包)目录结构, 具体开发路径为: `根目录/apps/web-antd`
|
v5版本采用分仓(包)目录结构, 具体开发路径为: `根目录/apps/web-antd`
|
||||||
|
|
||||||
目前对应后端版本: **分布式5.5.0/微服务2.5.0**
|
目前对应后端版本: **分布式5.5.1/微服务2.5.1**
|
||||||
|
|
||||||
V1.1.0版本已支持离线图标
|
V1.1.0版本已支持离线图标
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@vben/web-antd",
|
"name": "@vben/web-antd",
|
||||||
"version": "1.5.0",
|
"version": "1.5.2",
|
||||||
"homepage": "https://vben.pro",
|
"homepage": "https://vben.pro",
|
||||||
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
"bugs": "https://github.com/vbenjs/vue-vben-admin/issues",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -16,7 +16,8 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm vite build",
|
"build:prod": "pnpm vite build",
|
||||||
|
"build:test": "pnpm vite build --mode test",
|
||||||
"build:analyze": "pnpm vite build --mode analyze",
|
"build:analyze": "pnpm vite build --mode analyze",
|
||||||
"dev": "pnpm vite --mode development",
|
"dev": "pnpm vite --mode development",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
|||||||
@@ -125,3 +125,13 @@ export function dictSyncTenant(tenantId?: string) {
|
|||||||
successMessageMode: 'message',
|
successMessageMode: 'message',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步租户配置
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
export function syncTenantConfig() {
|
||||||
|
return requestClient.get<void>('/system/tenant/syncTenantConfig', {
|
||||||
|
successMessageMode: 'message',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
27
apps/web-antd/src/components/global/slot.ts
Normal file
27
apps/web-antd/src/components/global/slot.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { defineComponent, h } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用默认插槽来自定义组件
|
||||||
|
* 给vbenForm的components使用
|
||||||
|
*/
|
||||||
|
export const DefaultSlot = defineComponent({
|
||||||
|
name: 'DefaultSlot',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* 绑定到根节点的div上的属性
|
||||||
|
*/
|
||||||
|
rootDivAttrs: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
/**
|
||||||
|
* 获取属性 传递给作用域插槽供外部使用
|
||||||
|
*/
|
||||||
|
const attrs = this.$attrs;
|
||||||
|
|
||||||
|
return h('div', { ...this.rootDivAttrs }, this.$slots.default?.(attrs));
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -133,6 +133,11 @@ const initOptions = computed((): InitOptions => {
|
|||||||
toolbar_mode: 'sliding',
|
toolbar_mode: 'sliding',
|
||||||
// 隐藏下面的 按xxx获取帮助
|
// 隐藏下面的 按xxx获取帮助
|
||||||
help_accessibility: false,
|
help_accessibility: false,
|
||||||
|
// https://blog.csdn.net/qq_46380656/article/details/122171418
|
||||||
|
// 避免图片地址和链接地址转换成相对路径
|
||||||
|
relative_urls: false,
|
||||||
|
remove_script_host: false,
|
||||||
|
convert_urls: false,
|
||||||
...options,
|
...options,
|
||||||
/**
|
/**
|
||||||
* 覆盖默认的base64行为
|
* 覆盖默认的base64行为
|
||||||
|
|||||||
@@ -1,215 +1,170 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface';
|
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface';
|
||||||
import type { DataNode } from 'ant-design-vue/es/tree';
|
import type { DataNode } from 'ant-design-vue/es/tree';
|
||||||
import type { CheckInfo } from 'ant-design-vue/es/vc-tree/props';
|
|
||||||
|
|
||||||
import type { PropType, SetupContext } from 'vue';
|
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { computed, nextTick, onMounted, ref, useSlots, watch } from 'vue';
|
import { treeToList } from '@vben/utils';
|
||||||
|
|
||||||
import { findGroupParentIds, treeToList } from '@vben/utils';
|
|
||||||
|
|
||||||
import { Checkbox, Tree } from 'ant-design-vue';
|
import { Checkbox, Tree } from 'ant-design-vue';
|
||||||
import { uniq } from 'lodash-es';
|
|
||||||
|
|
||||||
/** 需要禁止透传 */
|
/** 需要禁止透传 */
|
||||||
defineOptions({ inheritAttrs: false });
|
defineOptions({ inheritAttrs: false });
|
||||||
|
|
||||||
const props = defineProps({
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
checkStrictly: {
|
expandAllOnInit: false,
|
||||||
default: true,
|
fieldNames: () => ({ key: 'id', title: 'label' }),
|
||||||
type: Boolean,
|
resetOnStrictlyChange: true,
|
||||||
},
|
treeData: () => [],
|
||||||
expandAllOnInit: {
|
|
||||||
default: false,
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
fieldNames: {
|
|
||||||
default: () => ({ key: 'id', title: 'label' }),
|
|
||||||
type: Object as PropType<{ key: string; title: string }>,
|
|
||||||
},
|
|
||||||
/** 点击节点关联/独立时 清空已勾选的节点 */
|
|
||||||
resetOnStrictlyChange: {
|
|
||||||
default: true,
|
|
||||||
type: Boolean,
|
|
||||||
},
|
|
||||||
treeData: {
|
|
||||||
default: () => [],
|
|
||||||
type: Array as PropType<DataNode[]>,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const emit = defineEmits<{ checkStrictlyChange: [boolean] }>();
|
|
||||||
|
|
||||||
const expandStatus = ref(false);
|
interface Props {
|
||||||
const selectAllStatus = ref(false);
|
/**
|
||||||
|
* 是否展开所有节点 mount
|
||||||
|
*/
|
||||||
|
expandAllOnInit?: boolean;
|
||||||
|
/**
|
||||||
|
* 自定义字段
|
||||||
|
*/
|
||||||
|
fieldNames?: { key: string; title: string };
|
||||||
|
/**
|
||||||
|
* 点击节点关联/独立时 清空已勾选的节点
|
||||||
|
*/
|
||||||
|
resetOnStrictlyChange?: boolean;
|
||||||
|
/**
|
||||||
|
* 树结构数据
|
||||||
|
*/
|
||||||
|
treeData?: DataNode[];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 后台的这个字段跟antd/ele是反的
|
* 展开的状态
|
||||||
* 组件库这个字段代表不关联
|
|
||||||
* 后台这个代表关联
|
|
||||||
*/
|
*/
|
||||||
const innerCheckedStrictly = computed(() => {
|
const expandStatus = ref(false);
|
||||||
return !props.checkStrictly;
|
/**
|
||||||
});
|
* 全选状态
|
||||||
|
*/
|
||||||
|
const selectAllStatus = ref(false);
|
||||||
|
|
||||||
const associationText = computed(() => {
|
const associationText = computed(() => {
|
||||||
return props.checkStrictly ? '父子节点关联' : '父子节点独立';
|
return checkStrictly.value ? '父子节点关联' : '父子节点独立';
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 这个只用于界面显示
|
* 这个只用于界面显示
|
||||||
* 关联情况下 只会有最末尾的节点被选中
|
* 关联情况下 只会有最末尾的节点被选中
|
||||||
*/
|
*/
|
||||||
const checkedKeys = defineModel('value', {
|
const checkedKeys = defineModel<(number | string)[]>('value', {
|
||||||
default: () => [],
|
default: () => [],
|
||||||
type: Array as PropType<(number | string)[]>,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否节点关联 后端字段跟前端字段是反的
|
||||||
|
*/
|
||||||
|
const checkStrictly = defineModel<boolean>('checkStrictly', {
|
||||||
|
default: () => true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const computedCheckedKeys = computed<any>({
|
||||||
|
get() {
|
||||||
|
/**
|
||||||
|
* 严格模式(节点不关联) 需要返回{checked: string[] | number[], halfChecked: string[]}
|
||||||
|
* @see https://www.antdv.com/components/tree-cn#tree-props
|
||||||
|
*/
|
||||||
|
if (!checkStrictly.value) {
|
||||||
|
return {
|
||||||
|
checked: [...checkedKeys.value],
|
||||||
|
halfChecked: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return checkedKeys.value;
|
||||||
|
},
|
||||||
|
set(v) {
|
||||||
|
if (!checkStrictly.value) {
|
||||||
|
checkedKeys.value = [...v.checked, ...v.halfChecked];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
checkedKeys.value = v;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// 所有节点的ID
|
// 所有节点的ID
|
||||||
const allKeys = computed(() => {
|
const allKeys = computed(() => {
|
||||||
const idField = props.fieldNames.key;
|
const idField = props.fieldNames.key;
|
||||||
return treeToList(props.treeData).map((item: any) => item[idField]);
|
return treeToList(props.treeData).map((item: any) => item[idField]);
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 已经选择的所有节点 包括子/父节点 用于提交 */
|
function handleCheckedAllChange(e: CheckboxChangeEvent) {
|
||||||
const checkedRealKeys = ref<(number | string)[]>([]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取第一次的menuTree id 设置到checkedMenuKeys
|
|
||||||
* 主要为了解决没有任何修改 直接点击保存的情况
|
|
||||||
*
|
|
||||||
* length为0情况(即新增时候没有勾选节点) 勾选这里会延迟触发 节点会拼接上父节点 导致ID重复
|
|
||||||
*/
|
|
||||||
const stop = watch([checkedKeys, () => props.treeData], () => {
|
|
||||||
if (
|
|
||||||
props.checkStrictly &&
|
|
||||||
checkedKeys.value.length > 0 &&
|
|
||||||
props.treeData.length > 0
|
|
||||||
) {
|
|
||||||
/** 找到父节点 添加上 */
|
|
||||||
const parentIds = findGroupParentIds(
|
|
||||||
props.treeData,
|
|
||||||
checkedKeys.value as any,
|
|
||||||
{ id: props.fieldNames.key },
|
|
||||||
);
|
|
||||||
/**
|
|
||||||
* uniq 解决上面的id重复问题
|
|
||||||
*/
|
|
||||||
checkedRealKeys.value = uniq([...parentIds, ...checkedKeys.value]);
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
if (!props.checkStrictly && checkedKeys.value.length > 0) {
|
|
||||||
/** 节点独立 这里是全部的节点 */
|
|
||||||
checkedRealKeys.value = checkedKeys.value;
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param checkedStateKeys 已经选中的子节点的ID
|
|
||||||
* @param info info.halfCheckedKeys为父节点的ID
|
|
||||||
*/
|
|
||||||
type CheckedState<T = number | string> =
|
|
||||||
| T[]
|
|
||||||
| { checked: T[]; halfChecked: T[] };
|
|
||||||
function handleChecked(checkedStateKeys: CheckedState, info: CheckInfo) {
|
|
||||||
// 数组的话为节点关联
|
|
||||||
if (Array.isArray(checkedStateKeys)) {
|
|
||||||
const halfCheckedKeys: number[] = (info.halfCheckedKeys || []) as number[];
|
|
||||||
checkedRealKeys.value = [...halfCheckedKeys, ...checkedStateKeys];
|
|
||||||
} else {
|
|
||||||
checkedRealKeys.value = [...checkedStateKeys.checked];
|
|
||||||
// fix: Invalid prop: type check failed for prop "value". Expected Array, got Object
|
|
||||||
checkedKeys.value = [...checkedStateKeys.checked];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleExpandChange(e: CheckboxChangeEvent) {
|
|
||||||
// 这个用于展示
|
// 这个用于展示
|
||||||
checkedKeys.value = e.target.checked ? allKeys.value : [];
|
checkedKeys.value = e.target.checked ? allKeys.value : [];
|
||||||
// 这个用于提交
|
|
||||||
checkedRealKeys.value = e.target.checked ? allKeys.value : [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const expandedKeys = ref<string[]>([]);
|
const expandedKeys = ref<string[]>([]);
|
||||||
function handleExpandOrCollapseAll(e: CheckboxChangeEvent) {
|
function handleExpandOrCollapseAll() {
|
||||||
const expand = e.target.checked;
|
expandStatus.value = !expandStatus.value;
|
||||||
expandedKeys.value = expand ? allKeys.value : [];
|
expandedKeys.value = expandStatus.value ? allKeys.value : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCheckStrictlyChange(e: CheckboxChangeEvent) {
|
function handleCheckStrictlyChange() {
|
||||||
emit('checkStrictlyChange', e.target.checked);
|
|
||||||
if (props.resetOnStrictlyChange) {
|
if (props.resetOnStrictlyChange) {
|
||||||
checkedKeys.value = [];
|
checkedKeys.value = [];
|
||||||
checkedRealKeys.value = [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 暴露方法来获取用于提交的全部节点
|
|
||||||
* uniq去重(保险方案)
|
|
||||||
*/
|
|
||||||
defineExpose({
|
|
||||||
getCheckedKeys: () => uniq(checkedRealKeys.value),
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (props.expandAllOnInit) {
|
if (props.expandAllOnInit) {
|
||||||
await nextTick();
|
await nextTick();
|
||||||
expandedKeys.value = allKeys.value;
|
expandedKeys.value = allKeys.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const slots = useSlots() as SetupContext['slots'];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-background w-full rounded-lg border-[1px] p-[12px]">
|
<div class="bg-background w-full rounded-lg border-[1px] p-[12px]">
|
||||||
|
<!-- <div class="flex flex-col gap-6 text-[13px]">
|
||||||
|
<div>computedCheckedKeys {{ computedCheckedKeys }}</div>
|
||||||
|
<div>checkedKeys {{ checkedKeys }}</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
<div class="flex items-center justify-between gap-2 border-b-[1px] pb-2">
|
<div class="flex items-center justify-between gap-2 border-b-[1px] pb-2">
|
||||||
<div>
|
<div class="opacity-75">
|
||||||
<span>节点状态: </span>
|
<span>节点状态: </span>
|
||||||
<span :class="[props.checkStrictly ? 'text-primary' : 'text-red-500']">
|
<span :class="[checkStrictly ? 'text-primary' : 'text-red-500']">
|
||||||
{{ associationText }}
|
{{ associationText }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
已选中
|
|
||||||
<span class="text-primary mx-1 font-semibold">
|
|
||||||
{{ checkedRealKeys.length }}
|
|
||||||
</span>
|
|
||||||
个节点
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-wrap items-center justify-between border-b-[1px] py-2"
|
class="flex flex-wrap items-center justify-between border-b-[1px] py-2"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<a-button size="small" @click="handleExpandOrCollapseAll">
|
||||||
v-model:checked="expandStatus"
|
|
||||||
@change="handleExpandOrCollapseAll"
|
|
||||||
>
|
|
||||||
展开/折叠全部
|
展开/折叠全部
|
||||||
</Checkbox>
|
</a-button>
|
||||||
<Checkbox v-model:checked="selectAllStatus" @change="handleExpandChange">
|
<Checkbox
|
||||||
|
v-model:checked="selectAllStatus"
|
||||||
|
@change="handleCheckedAllChange"
|
||||||
|
>
|
||||||
全选/取消全选
|
全选/取消全选
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
<Checkbox :checked="checkStrictly" @change="handleCheckStrictlyChange">
|
<Checkbox
|
||||||
|
v-model:checked="checkStrictly"
|
||||||
|
@change="handleCheckStrictlyChange"
|
||||||
|
>
|
||||||
父子节点关联
|
父子节点关联
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<Tree
|
<Tree
|
||||||
v-if="treeData.length > 0"
|
:check-strictly="!checkStrictly"
|
||||||
v-model:check-strictly="innerCheckedStrictly"
|
v-model:checked-keys="computedCheckedKeys"
|
||||||
v-model:checked-keys="checkedKeys"
|
|
||||||
v-model:expanded-keys="expandedKeys"
|
v-model:expanded-keys="expandedKeys"
|
||||||
:checkable="true"
|
:checkable="true"
|
||||||
:field-names="fieldNames"
|
:field-names="fieldNames"
|
||||||
:selectable="false"
|
:selectable="false"
|
||||||
:tree-data="treeData"
|
:tree-data="treeData"
|
||||||
@check="handleChecked"
|
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
v-for="slotName in Object.keys(slots)"
|
v-for="slotName in Object.keys($slots)"
|
||||||
:key="slotName"
|
:key="slotName"
|
||||||
#[slotName]="data"
|
#[slotName]="data"
|
||||||
>
|
>
|
||||||
@@ -219,3 +174,20 @@ const slots = useSlots() as SetupContext['slots'];
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
:deep(.ant-tree) {
|
||||||
|
// 勾选框居中
|
||||||
|
& .ant-tree-checkbox {
|
||||||
|
margin: 0;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 展开图标居中
|
||||||
|
& .ant-tree-switcher {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -115,9 +115,14 @@ const [BasicDrawer, drawerApi] = useVbenDrawer({
|
|||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
await formApi.setFieldValue('parentId', id);
|
await formApi.setFieldValue('parentId', id);
|
||||||
if (update) {
|
// 创建元组(不是数组 元素位置固定)
|
||||||
// 没有依赖关系 同时加载
|
const promise = [
|
||||||
const [record] = await Promise.all([menuInfo(id), setupMenuSelect()]);
|
update ? menuInfo(id) : null,
|
||||||
|
setupMenuSelect(),
|
||||||
|
] as const;
|
||||||
|
// 并行获取菜单树选择和菜单信息
|
||||||
|
const [record] = await Promise.all(promise);
|
||||||
|
if (record) {
|
||||||
await formApi.setValues(record);
|
await formApi.setValues(record);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -97,11 +97,19 @@ async function handleClosed() {
|
|||||||
<BasicForm>
|
<BasicForm>
|
||||||
<template #tip>
|
<template #tip>
|
||||||
<div class="ml-7 w-full">
|
<div class="ml-7 w-full">
|
||||||
<Alert
|
<Alert show-icon type="warning">
|
||||||
message="私有桶使用自定义域名无法预览, 但可以正常上传/下载"
|
<template #message>
|
||||||
show-icon
|
私有桶(minio)使用自定义域名需要参考
|
||||||
type="warning"
|
<a
|
||||||
/>
|
href="https://gitee.com/dromara/RuoYi-Vue-Plus/issues/IBQIKC"
|
||||||
|
target="_blank"
|
||||||
|
class="text-primary"
|
||||||
|
>
|
||||||
|
支持minio预览私有桶
|
||||||
|
</a>
|
||||||
|
, 否则无法预览
|
||||||
|
</template>
|
||||||
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</BasicForm>
|
</BasicForm>
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import type { FormSchemaGetter } from '#/adapter/form';
|
import type { FormSchemaGetter } from '#/adapter/form';
|
||||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
|
import { markRaw } from 'vue';
|
||||||
|
|
||||||
import { DictEnum } from '@vben/constants';
|
import { DictEnum } from '@vben/constants';
|
||||||
import { getPopupContainer } from '@vben/utils';
|
import { getPopupContainer } from '@vben/utils';
|
||||||
|
|
||||||
import { Tag } from 'ant-design-vue';
|
import { Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { DefaultSlot } from '#/components/global/slot';
|
||||||
|
import { TreeSelectPanel } from '#/components/tree';
|
||||||
import { getDictOptions } from '#/utils/dict';
|
import { getDictOptions } from '#/utils/dict';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -177,15 +181,6 @@ export const authModalSchemas: FormSchemaGetter = () => [
|
|||||||
fieldName: 'roleId',
|
fieldName: 'roleId',
|
||||||
label: '角色ID',
|
label: '角色ID',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
component: 'Radio',
|
|
||||||
dependencies: {
|
|
||||||
show: () => false,
|
|
||||||
triggerFields: [''],
|
|
||||||
},
|
|
||||||
fieldName: 'deptCheckStrictly',
|
|
||||||
label: 'deptCheckStrictly',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
componentProps: {
|
componentProps: {
|
||||||
@@ -214,12 +209,39 @@ export const authModalSchemas: FormSchemaGetter = () => [
|
|||||||
label: '权限范围',
|
label: '权限范围',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'TreeSelect',
|
component: 'Radio',
|
||||||
|
dependencies: {
|
||||||
|
show: () => false,
|
||||||
|
triggerFields: [''],
|
||||||
|
},
|
||||||
|
fieldName: 'deptCheckStrictly',
|
||||||
|
label: 'deptCheckStrictly',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 这种的场景基本上是一个组件需要绑定两个或以上的场景
|
||||||
|
component: markRaw(DefaultSlot),
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
|
componentProps: {
|
||||||
|
rootDivAttrs: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
},
|
||||||
dependencies: {
|
dependencies: {
|
||||||
show: (values) => values.dataScope === '2',
|
show: (values) => values.dataScope === '2',
|
||||||
triggerFields: ['dataScope'],
|
triggerFields: ['dataScope'],
|
||||||
},
|
},
|
||||||
|
renderComponentContent: (model) => ({
|
||||||
|
default: (attrs: any) => {
|
||||||
|
return (
|
||||||
|
<TreeSelectPanel
|
||||||
|
expand-all-on-init={true}
|
||||||
|
treeData={attrs.treeData}
|
||||||
|
v-model:checkStrictly={model.deptCheckStrictly}
|
||||||
|
v-model:value={model.deptIds}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
fieldName: 'deptIds',
|
fieldName: 'deptIds',
|
||||||
help: '更改后立即生效',
|
help: '更改后立即生效',
|
||||||
label: '部门权限',
|
label: '部门权限',
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DeptOption } from '#/api/system/role/model';
|
import type { DeptOption } from '#/api/system/role/model';
|
||||||
|
|
||||||
import { ref } from 'vue';
|
|
||||||
|
|
||||||
import { useVbenModal } from '@vben/common-ui';
|
import { useVbenModal } from '@vben/common-ui';
|
||||||
import { cloneDeep } from '@vben/utils';
|
import { cloneDeep, findGroupParentIds } from '@vben/utils';
|
||||||
|
|
||||||
|
import { uniq } from 'lodash-es';
|
||||||
|
|
||||||
import { useVbenForm } from '#/adapter/form';
|
import { useVbenForm } from '#/adapter/form';
|
||||||
import { roleDataScope, roleDeptTree, roleInfo } from '#/api/system/role';
|
import { roleDataScope, roleDeptTree, roleInfo } from '#/api/system/role';
|
||||||
import { TreeSelectPanel } from '#/components/tree';
|
|
||||||
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
|
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
|
||||||
|
|
||||||
import { authModalSchemas } from './data';
|
import { authModalSchemas } from './data';
|
||||||
@@ -26,26 +25,32 @@ const [BasicForm, formApi] = useVbenForm({
|
|||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const deptTree = ref<DeptOption[]>([]);
|
/**
|
||||||
|
* 保存部门数据 用于获取祖先节点
|
||||||
|
*/
|
||||||
|
let treeData: DeptOption[] = [];
|
||||||
async function setupDeptTree(id: number | string) {
|
async function setupDeptTree(id: number | string) {
|
||||||
const resp = await roleDeptTree(id);
|
const resp = await roleDeptTree(id);
|
||||||
formApi.setFieldValue('deptIds', resp.checkedKeys);
|
const { checkedKeys, depts } = resp;
|
||||||
// 设置菜单信息
|
|
||||||
deptTree.value = resp.depts;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function customFormValueGetter() {
|
/**
|
||||||
const v = await defaultFormValueGetter(formApi)();
|
* 设置部门树数据
|
||||||
// 获取勾选信息
|
*/
|
||||||
const menuIds = deptSelectRef.value?.[0]?.getCheckedKeys() ?? [];
|
formApi.updateSchema([
|
||||||
const mixStr = v + menuIds.join(',');
|
{ fieldName: 'deptIds', componentProps: { treeData: depts } },
|
||||||
return mixStr;
|
]);
|
||||||
|
/**
|
||||||
|
* 设置选中 必须先传递treeData
|
||||||
|
* Note: Tree missing follow keys: '1981565541727186945'
|
||||||
|
*/
|
||||||
|
await formApi.setFieldValue('deptIds', checkedKeys);
|
||||||
|
treeData = depts;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
|
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
|
||||||
{
|
{
|
||||||
initializedGetter: customFormValueGetter,
|
initializedGetter: defaultFormValueGetter(formApi),
|
||||||
currentGetter: customFormValueGetter,
|
currentGetter: defaultFormValueGetter(formApi),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -56,14 +61,14 @@ const [BasicModal, modalApi] = useVbenModal({
|
|||||||
onConfirm: handleConfirm,
|
onConfirm: handleConfirm,
|
||||||
onOpenChange: async (isOpen) => {
|
onOpenChange: async (isOpen) => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
|
treeData = [];
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
modalApi.modalLoading(true);
|
modalApi.modalLoading(true);
|
||||||
|
|
||||||
const { id } = modalApi.getData() as { id: number | string };
|
const { id } = modalApi.getData() as { id: number | string };
|
||||||
|
|
||||||
setupDeptTree(id);
|
const [record] = await Promise.all([roleInfo(id), setupDeptTree(id)]);
|
||||||
const record = await roleInfo(id);
|
|
||||||
await formApi.setValues(record);
|
await formApi.setValues(record);
|
||||||
markInitialized();
|
markInitialized();
|
||||||
|
|
||||||
@@ -71,11 +76,6 @@ const [BasicModal, modalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 这里拿到的是一个数组ref
|
|
||||||
*/
|
|
||||||
const deptSelectRef = ref();
|
|
||||||
|
|
||||||
async function handleConfirm() {
|
async function handleConfirm() {
|
||||||
try {
|
try {
|
||||||
modalApi.lock(true);
|
modalApi.lock(true);
|
||||||
@@ -87,7 +87,15 @@ async function handleConfirm() {
|
|||||||
const data = cloneDeep(await formApi.getValues());
|
const data = cloneDeep(await formApi.getValues());
|
||||||
// 不为自定义权限的话 删除部门id
|
// 不为自定义权限的话 删除部门id
|
||||||
if (data.dataScope === '2') {
|
if (data.dataScope === '2') {
|
||||||
const deptIds = deptSelectRef.value?.[0]?.getCheckedKeys() ?? [];
|
let { deptIds, deptCheckStrictly } = data;
|
||||||
|
// 节点关联 需要拼接上祖级ID(获取的是不带的)
|
||||||
|
if (deptCheckStrictly) {
|
||||||
|
// 找到所有父级ID
|
||||||
|
const parentIds = findGroupParentIds(treeData, deptIds, { id: 'id' });
|
||||||
|
// 去重
|
||||||
|
deptIds = uniq([...parentIds, ...deptIds]);
|
||||||
|
}
|
||||||
|
// 赋值
|
||||||
data.deptIds = deptIds;
|
data.deptIds = deptIds;
|
||||||
} else {
|
} else {
|
||||||
data.deptIds = [];
|
data.deptIds = [];
|
||||||
@@ -107,29 +115,10 @@ async function handleClosed() {
|
|||||||
await formApi.resetForm();
|
await formApi.resetForm();
|
||||||
resetInitialized();
|
resetInitialized();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过回调更新 无法通过v-model
|
|
||||||
* @param value 菜单选择是否严格模式
|
|
||||||
*/
|
|
||||||
function handleCheckStrictlyChange(value: boolean) {
|
|
||||||
formApi.setFieldValue('deptCheckStrictly', value);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<BasicModal class="min-h-[600px] w-[550px]" title="分配权限">
|
<BasicModal class="min-h-[600px] w-[550px]" title="分配权限">
|
||||||
<BasicForm>
|
<BasicForm />
|
||||||
<template #deptIds="slotProps">
|
|
||||||
<TreeSelectPanel
|
|
||||||
ref="deptSelectRef"
|
|
||||||
v-bind="slotProps"
|
|
||||||
:check-strictly="formApi.form.values.deptCheckStrictly"
|
|
||||||
:expand-all-on-init="true"
|
|
||||||
:tree-data="deptTree"
|
|
||||||
@check-strictly-change="handleCheckStrictlyChange"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</BasicForm>
|
|
||||||
</BasicModal>
|
</BasicModal>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Modal, Popconfirm, Space } from 'ant-design-vue';
|
|||||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||||
import {
|
import {
|
||||||
dictSyncTenant,
|
dictSyncTenant,
|
||||||
|
syncTenantConfig,
|
||||||
tenantExport,
|
tenantExport,
|
||||||
tenantList,
|
tenantList,
|
||||||
tenantRemove,
|
tenantRemove,
|
||||||
@@ -144,6 +145,18 @@ function handleSyncTenantDict() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSyncTenantConfig() {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '提示',
|
||||||
|
iconType: 'warning',
|
||||||
|
content: '确认同步租户参数配置?',
|
||||||
|
onOk: async () => {
|
||||||
|
await syncTenantConfig();
|
||||||
|
await tableApi.query();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -157,6 +170,12 @@ function handleSyncTenantDict() {
|
|||||||
>
|
>
|
||||||
同步租户字典
|
同步租户字典
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
v-access:code="['system:tenant:edit']"
|
||||||
|
@click="handleSyncTenantConfig"
|
||||||
|
>
|
||||||
|
同步租户参数配置
|
||||||
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
v-access:code="['system:tenant:export']"
|
v-access:code="['system:tenant:export']"
|
||||||
@click="handleDownloadExcel"
|
@click="handleDownloadExcel"
|
||||||
|
|||||||
@@ -38,8 +38,18 @@ import { approveWithReasonModal } from '../helper';
|
|||||||
import userSelectModal from '../user-select-modal.vue';
|
import userSelectModal from '../user-select-modal.vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
/**
|
||||||
|
* 行数据的taskInfo?
|
||||||
|
*/
|
||||||
task?: TaskInfo;
|
task?: TaskInfo;
|
||||||
|
/**
|
||||||
|
* 审批类型 根据不同类型显示按钮
|
||||||
|
*/
|
||||||
type: ApprovalType;
|
type: ApprovalType;
|
||||||
|
/**
|
||||||
|
* 为审批类型时候 显示的按钮(按钮权限)
|
||||||
|
*/
|
||||||
|
buttonPermissions: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
@@ -47,25 +57,11 @@ const emit = defineEmits<{
|
|||||||
reload: [];
|
reload: [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
/**
|
|
||||||
* 按钮权限
|
|
||||||
*/
|
|
||||||
const buttonPermissions = computed(() => {
|
|
||||||
const record: Record<string, boolean> = {};
|
|
||||||
if (!props.task) {
|
|
||||||
return record;
|
|
||||||
}
|
|
||||||
props.task.buttonList.forEach((item) => {
|
|
||||||
record[item.code] = item.show;
|
|
||||||
});
|
|
||||||
return record;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 是否显示 `其他` 按钮
|
// 是否显示 `其他` 按钮
|
||||||
const showButtonOther = computed(() => {
|
const showButtonOther = computed(() => {
|
||||||
const moreCollections = new Set(['addSign', 'subSign', 'transfer', 'trust']);
|
const moreCollections = new Set(['addSign', 'subSign', 'transfer', 'trust']);
|
||||||
return Object.keys(buttonPermissions.value).some(
|
return Object.keys(props.buttonPermissions).some(
|
||||||
(key) => moreCollections.has(key) && buttonPermissions.value[key],
|
(key) => moreCollections.has(key) && props.buttonPermissions[key],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -154,10 +150,11 @@ const [ApprovalModal, approvalModalApi] = useVbenModal({
|
|||||||
connectedComponent: approvalModal,
|
connectedComponent: approvalModal,
|
||||||
});
|
});
|
||||||
function handleApproval() {
|
function handleApproval() {
|
||||||
|
const { buttonPermissions } = props;
|
||||||
// 是否具有抄送权限
|
// 是否具有抄送权限
|
||||||
const copyPermission = buttonPermissions.value?.copy ?? false;
|
const copyPermission = buttonPermissions?.copy ?? false;
|
||||||
// 是否具有选人权限
|
// 是否具有选人权限
|
||||||
const assignPermission = buttonPermissions.value?.pop ?? false;
|
const assignPermission = buttonPermissions?.pop ?? false;
|
||||||
approvalModalApi.setData({
|
approvalModalApi.setData({
|
||||||
taskId: props.task?.id,
|
taskId: props.task?.id,
|
||||||
copyPermission,
|
copyPermission,
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ const [BasicModal, modalApi] = useVbenModal({
|
|||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
modalApi.modalLoading(true);
|
||||||
|
|
||||||
const { taskId } = modalApi.getData() as ModalProps;
|
const { taskId } = modalApi.getData() as ModalProps;
|
||||||
|
|
||||||
// 查询是否有按钮权限
|
// 查询是否有按钮权限
|
||||||
@@ -63,6 +65,8 @@ const [BasicModal, modalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
modalApi.modalLoading(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,6 +112,11 @@ const [BasicForm, formApi] = useVbenForm({
|
|||||||
component: 'Input',
|
component: 'Input',
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
label: '抄送人',
|
label: '抄送人',
|
||||||
|
// 默认不显示
|
||||||
|
dependencies: {
|
||||||
|
if: false,
|
||||||
|
triggerFields: [''],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!--
|
<!--
|
||||||
审批详情
|
审批详情
|
||||||
动态渲染要显示的内容 需要在flowDescripionsMap先定义好组件
|
动态渲染要显示的内容 需要再flowDescripionsMap先定义好组件
|
||||||
-->
|
-->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FlowComponentsMapMapKey } from '../register';
|
import type { FlowComponentsMapMapKey } from '../register';
|
||||||
|
|||||||
@@ -1,169 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { ApprovalType } from './type';
|
|
||||||
|
|
||||||
import type { FlowInfoResponse } from '#/api/workflow/instance/model';
|
|
||||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
|
|
||||||
import { useVbenDrawer, VbenAvatar } from '@vben/common-ui';
|
|
||||||
import { DictEnum } from '@vben/constants';
|
|
||||||
import { cloneDeep, cn } from '@vben/utils';
|
|
||||||
|
|
||||||
import { Divider, Skeleton, TabPane, Tabs } from 'ant-design-vue';
|
|
||||||
|
|
||||||
import { flowInfo } from '#/api/workflow/instance';
|
|
||||||
import { getTaskByTaskId } from '#/api/workflow/task';
|
|
||||||
import { renderDict } from '#/utils/render';
|
|
||||||
|
|
||||||
import { FlowActions } from './actions';
|
|
||||||
import ApprovalDetails from './approval-details.vue';
|
|
||||||
import FlowPreview from './flow-preview.vue';
|
|
||||||
|
|
||||||
const emit = defineEmits<{ reload: [] }>();
|
|
||||||
|
|
||||||
interface DrawerProps {
|
|
||||||
task: TaskInfo;
|
|
||||||
type: ApprovalType;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentTask = ref<null | TaskInfo>(null);
|
|
||||||
const currentFlowInfo = ref<FlowInfoResponse | null>(null);
|
|
||||||
const currentType = ref<DrawerProps['type'] | null>(null);
|
|
||||||
|
|
||||||
const [BasicDrawer, drawerApi] = useVbenDrawer({
|
|
||||||
title: '流程',
|
|
||||||
onOpenChange: async (isOpen) => {
|
|
||||||
if (!isOpen) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
const { task, type } = drawerApi.getData() as DrawerProps;
|
|
||||||
const { businessId, id: taskId } = task;
|
|
||||||
|
|
||||||
const flowResp = await flowInfo(businessId);
|
|
||||||
currentFlowInfo.value = flowResp;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 审批需要查询按钮权限 通过drawer传递的task是空的
|
|
||||||
* TODO: promise.all
|
|
||||||
*/
|
|
||||||
if (type === 'approve') {
|
|
||||||
const taskResp = await getTaskByTaskId(taskId);
|
|
||||||
const cloneTask = cloneDeep(task);
|
|
||||||
// 赋值 按钮权限
|
|
||||||
cloneTask.buttonList = taskResp.buttonList;
|
|
||||||
currentTask.value = cloneTask;
|
|
||||||
} else {
|
|
||||||
// default逻辑
|
|
||||||
currentTask.value = task;
|
|
||||||
}
|
|
||||||
// 最后赋值type
|
|
||||||
currentType.value = type;
|
|
||||||
// // 设置是否显示footer
|
|
||||||
// drawerApi.setState({
|
|
||||||
// footer: type !== 'readonly',
|
|
||||||
// });
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClosed() {
|
|
||||||
currentTask.value = null;
|
|
||||||
currentType.value = null;
|
|
||||||
currentFlowInfo.value = null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 底部按钮操作后
|
|
||||||
*/
|
|
||||||
async function handleAfterAction() {
|
|
||||||
emit('reload');
|
|
||||||
drawerApi.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
const showFooter = computed(() => {
|
|
||||||
return ![null, 'readonly'].includes(currentType.value);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<BasicDrawer class="w-[900px]" content-class="flex w-full">
|
|
||||||
<div :class="cn('flex-1 text-[#323639E0]')">
|
|
||||||
<Skeleton active :loading="loading">
|
|
||||||
<div v-if="currentTask" 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">
|
|
||||||
{{ currentTask.businessTitle ?? currentTask.flowName }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<component
|
|
||||||
:is="
|
|
||||||
renderDict(
|
|
||||||
currentTask.flowStatus,
|
|
||||||
DictEnum.WF_BUSINESS_STATUS,
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-2 text-sm">
|
|
||||||
<VbenAvatar
|
|
||||||
:alt="currentTask.createByName ?? ''"
|
|
||||||
class="bg-primary size-[28px] rounded-full text-white"
|
|
||||||
src=""
|
|
||||||
/>
|
|
||||||
<span>{{ currentTask.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>
|
|
||||||
流程分类: {{ currentTask.categoryName }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Divider type="vertical" />
|
|
||||||
|
|
||||||
<div class="flex items-center gap-1">
|
|
||||||
<span class="icon-[mdi--clock-outline] size-[16px]"></span>
|
|
||||||
提交时间: {{ currentTask.createTime }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Tabs v-if="currentFlowInfo" class="flex-1">
|
|
||||||
<TabPane key="1" tab="审批详情">
|
|
||||||
<ApprovalDetails
|
|
||||||
:current-flow-info="currentFlowInfo"
|
|
||||||
:task="currentTask"
|
|
||||||
/>
|
|
||||||
</TabPane>
|
|
||||||
|
|
||||||
<TabPane key="2" tab="审批流程图">
|
|
||||||
<FlowPreview :instance-id="currentFlowInfo.instanceId" />
|
|
||||||
</TabPane>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
</Skeleton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<!-- TODO: 暂时只能这样处理 footer常驻但不显示内容 这个插槽有点迷 -->
|
|
||||||
<FlowActions
|
|
||||||
v-if="showFooter && currentTask && currentType"
|
|
||||||
:task="currentTask"
|
|
||||||
:type="currentType"
|
|
||||||
@reload="handleAfterAction"
|
|
||||||
/>
|
|
||||||
<div v-else></div>
|
|
||||||
</template>
|
|
||||||
</BasicDrawer>
|
|
||||||
</template>
|
|
||||||
@@ -1,117 +1,72 @@
|
|||||||
<!-- 该文件需要重构 但我没空 -->
|
<!--
|
||||||
|
TODO: 优化项
|
||||||
|
会先加载流程信息 再加载业务表单信息
|
||||||
|
-->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { User } from '#/api/core/user';
|
import type { ApprovalType } from './type';
|
||||||
|
|
||||||
import type { FlowInfoResponse } from '#/api/workflow/instance/model';
|
import type { FlowInfoResponse } from '#/api/workflow/instance/model';
|
||||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||||
|
|
||||||
import { computed, h, onUnmounted, ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
import { Fallback, useVbenModal, VbenAvatar } from '@vben/common-ui';
|
import { Fallback, VbenAvatar } from '@vben/common-ui';
|
||||||
import { DictEnum } from '@vben/constants';
|
import { DictEnum } from '@vben/constants';
|
||||||
import { cn, getPopupContainer } from '@vben/utils';
|
import { cn } from '@vben/utils';
|
||||||
|
|
||||||
import {
|
import { CopyOutlined } from '@ant-design/icons-vue';
|
||||||
ArrowLeftOutlined,
|
import { useClipboard } from '@vueuse/core';
|
||||||
CheckOutlined,
|
import { Card, Divider, message, TabPane, Tabs } from 'ant-design-vue';
|
||||||
CopyOutlined,
|
|
||||||
EditOutlined,
|
|
||||||
ExclamationCircleOutlined,
|
|
||||||
MenuOutlined,
|
|
||||||
RollbackOutlined,
|
|
||||||
UsergroupAddOutlined,
|
|
||||||
UsergroupDeleteOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
} from '@ant-design/icons-vue';
|
|
||||||
import { useClipboard, useEventListener } from '@vueuse/core';
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
Divider,
|
|
||||||
Dropdown,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
message,
|
|
||||||
Modal,
|
|
||||||
Space,
|
|
||||||
TabPane,
|
|
||||||
Tabs,
|
|
||||||
} from 'ant-design-vue';
|
|
||||||
import { isObject } from 'lodash-es';
|
|
||||||
|
|
||||||
import {
|
import { flowInfo } from '#/api/workflow/instance';
|
||||||
cancelProcessApply,
|
import { getTaskByTaskId } from '#/api/workflow/task';
|
||||||
deleteByInstanceIds,
|
|
||||||
flowInfo,
|
|
||||||
} from '#/api/workflow/instance';
|
|
||||||
import {
|
|
||||||
getTaskByTaskId,
|
|
||||||
taskOperation,
|
|
||||||
terminationTask,
|
|
||||||
updateAssignee,
|
|
||||||
} from '#/api/workflow/task';
|
|
||||||
import { renderDict } from '#/utils/render';
|
import { renderDict } from '#/utils/render';
|
||||||
|
|
||||||
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '.';
|
import { FlowActions } from './actions';
|
||||||
import ApprovalDetails from './approval-details.vue';
|
import ApprovalDetails from './approval-details.vue';
|
||||||
import FlowPreview from './flow-preview.vue';
|
import FlowPreview from './flow-preview.vue';
|
||||||
import { approveWithReasonModal } from './helper';
|
|
||||||
import userSelectModal from './user-select-modal.vue';
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'ApprovalPanel',
|
name: 'ApprovalPanel',
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const props = defineProps<{ task?: TaskInfo; type: ApprovalType }>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下面按钮点击后会触发的事件
|
* 下面按钮点击后会触发的事件
|
||||||
*/
|
*/
|
||||||
const emit = defineEmits<{ reload: [] }>();
|
defineEmits<{ reload: [] }>();
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/**
|
||||||
|
* 行数据(list)的info
|
||||||
|
*/
|
||||||
|
task?: TaskInfo;
|
||||||
|
/**
|
||||||
|
* 审批类型
|
||||||
|
*/
|
||||||
|
type: ApprovalType;
|
||||||
|
}
|
||||||
|
|
||||||
const currentTask = ref<TaskInfo>();
|
|
||||||
/**
|
/**
|
||||||
* 是否显示 加签/减签操作
|
* 目前的作用只为了获取按钮权限 因为list接口(行数据)获取为空
|
||||||
*/
|
*/
|
||||||
const showMultiActions = computed(() => {
|
const onlyForBtnPermissionTask = ref<TaskInfo>();
|
||||||
if (!currentTask.value) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (Number(currentTask.value.nodeRatio) > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按钮权限
|
* 按钮权限
|
||||||
*/
|
*/
|
||||||
const buttonPermissions = computed(() => {
|
const buttonPermissions = computed(() => {
|
||||||
const record: Record<string, boolean> = {};
|
const record: Record<string, boolean> = {};
|
||||||
if (!currentTask.value) {
|
if (!onlyForBtnPermissionTask.value) {
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
currentTask.value.buttonList.forEach((item) => {
|
onlyForBtnPermissionTask.value.buttonList.forEach((item) => {
|
||||||
record[item.code] = item.show;
|
record[item.code] = item.show;
|
||||||
});
|
});
|
||||||
return record;
|
return record;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 是否显示 `其他` 按钮
|
|
||||||
const showButtonOther = computed(() => {
|
|
||||||
const moreCollections = new Set(['addSign', 'subSign', 'transfer', 'trust']);
|
|
||||||
return Object.keys(buttonPermissions.value).some(
|
|
||||||
(key) => moreCollections.has(key) && buttonPermissions.value[key],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* myself 我发起的
|
|
||||||
* readonly 只读 只用于查看
|
|
||||||
* approve 审批
|
|
||||||
* admin 流程监控 - 待办任务使用
|
|
||||||
*/
|
|
||||||
type ApprovalType = 'admin' | 'approve' | 'myself' | 'readonly';
|
|
||||||
const showFooter = computed(() => {
|
const showFooter = computed(() => {
|
||||||
if (props.type === 'readonly') {
|
if (props.type === 'readonly') {
|
||||||
return false;
|
return false;
|
||||||
@@ -131,36 +86,34 @@ const currentFlowInfo = ref<FlowInfoResponse>();
|
|||||||
* card的loading状态
|
* card的loading状态
|
||||||
*/
|
*/
|
||||||
const loading = ref(false);
|
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) {
|
async function handleLoadInfo(task: TaskInfo | undefined) {
|
||||||
|
if (!task) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (!task) return null;
|
|
||||||
loading.value = true;
|
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;
|
* 不为审批不需要调用`getTaskByTaskId`接口
|
||||||
|
*/
|
||||||
|
if (props.type !== 'approve') {
|
||||||
|
const flowResp = await flowInfo(task.businessId);
|
||||||
|
currentFlowInfo.value = flowResp;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getTaskByTaskId主要为了获取按钮权限 目前没有其他功能
|
||||||
|
* 行数据(即props.task)获取的是没有按钮权限的
|
||||||
|
*/
|
||||||
|
const [flowResp, taskResp] = await Promise.all([
|
||||||
|
flowInfo(task.businessId),
|
||||||
|
getTaskByTaskId(task.id),
|
||||||
|
]);
|
||||||
|
|
||||||
|
currentFlowInfo.value = flowResp;
|
||||||
|
onlyForBtnPermissionTask.value = taskResp;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -170,217 +123,6 @@ async function handleLoadInfo(task: TaskInfo | undefined) {
|
|||||||
|
|
||||||
watch(() => props.task, handleLoadInfo);
|
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() {
|
|
||||||
// 是否具有抄送权限
|
|
||||||
const copyPermission = buttonPermissions.value?.copy ?? false;
|
|
||||||
// 是否具有选人权限
|
|
||||||
const assignPermission = buttonPermissions.value?.pop ?? false;
|
|
||||||
approvalModalApi.setData({
|
|
||||||
taskId: props.task?.id,
|
|
||||||
copyPermission,
|
|
||||||
assignPermission,
|
|
||||||
});
|
|
||||||
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在本地开发没有问题
|
||||||
* 打包后在一些设备会无法复制 使用legacy来保证兼容性
|
* 打包后在一些设备会无法复制 使用legacy来保证兼容性
|
||||||
@@ -407,6 +149,7 @@ async function handleCopy(text: string) {
|
|||||||
<CopyOutlined class="cursor-pointer" @click="handleCopy(task.id)" />
|
<CopyOutlined class="cursor-pointer" @click="handleCopy(task.id)" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<a-button size="small" @click="() => handleLoadInfo(task)">
|
<a-button size="small" @click="() => handleLoadInfo(task)">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
@@ -414,6 +157,7 @@ async function handleCopy(text: string) {
|
|||||||
</div>
|
</div>
|
||||||
</a-button>
|
</a-button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="flex flex-col gap-5 p-4">
|
<div class="flex flex-col gap-5 p-4">
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -426,19 +170,24 @@ async function handleCopy(text: string) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<VbenAvatar
|
<VbenAvatar
|
||||||
:alt="task?.createByName ?? ''"
|
:alt="task?.createByName ?? ''"
|
||||||
class="bg-primary size-[28px] rounded-full text-white"
|
class="bg-primary size-[28px] rounded-full text-white"
|
||||||
src=""
|
src=""
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span>{{ task.createByName }}</span>
|
<span>{{ task.createByName }}</span>
|
||||||
|
|
||||||
<div class="flex items-center opacity-50">
|
<div class="flex items-center opacity-50">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<span class="icon-[bxs--category-alt] size-[16px]"></span>
|
<span class="icon-[bxs--category-alt] size-[16px]"></span>
|
||||||
流程分类: {{ task.categoryName }}
|
流程分类: {{ task.categoryName }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Divider type="vertical" />
|
<Divider type="vertical" />
|
||||||
|
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<span class="icon-[mdi--clock-outline] size-[16px]"></span>
|
<span class="icon-[mdi--clock-outline] size-[16px]"></span>
|
||||||
提交时间: {{ task.createTime }}
|
提交时间: {{ task.createTime }}
|
||||||
@@ -446,154 +195,32 @@ async function handleCopy(text: string) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs v-if="currentFlowInfo" class="flex-1">
|
<Tabs v-if="currentFlowInfo" class="flex-1">
|
||||||
<TabPane key="1" tab="审批详情">
|
<TabPane key="1" tab="审批详情">
|
||||||
<ApprovalDetails
|
<ApprovalDetails
|
||||||
:current-flow-info="currentFlowInfo"
|
:current-flow-info="currentFlowInfo"
|
||||||
:iframe-loaded="iframeLoaded"
|
|
||||||
:iframe-height="iframeHeight"
|
|
||||||
:task="task"
|
:task="task"
|
||||||
/>
|
/>
|
||||||
</TabPane>
|
</TabPane>
|
||||||
|
|
||||||
<TabPane key="2" tab="审批流程图">
|
<TabPane key="2" tab="审批流程图">
|
||||||
<FlowPreview :instance-id="currentFlowInfo.instanceId" />
|
<FlowPreview :instance-id="currentFlowInfo.instanceId" />
|
||||||
</TabPane>
|
</TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
<!-- 固定底部 -->
|
|
||||||
|
<!-- 固定底部 占位高度 -->
|
||||||
<div class="h-[58px]"></div>
|
<div class="h-[58px]"></div>
|
||||||
<div
|
<FlowActions
|
||||||
v-if="showFooter"
|
v-if="showFooter"
|
||||||
:class="
|
:type="type"
|
||||||
cn(
|
:task="task"
|
||||||
'absolute bottom-0 left-0',
|
:button-permissions="buttonPermissions"
|
||||||
'border-t-solid border-t-[1px]',
|
@reload="$emit('reload')"
|
||||||
'bg-background w-full p-3',
|
/>
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<Space v-if="type === 'myself'">
|
|
||||||
<a-button
|
|
||||||
v-if="revocable"
|
|
||||||
danger
|
|
||||||
ghost
|
|
||||||
type="primary"
|
|
||||||
:icon="h(RollbackOutlined)"
|
|
||||||
@click="handleCancel"
|
|
||||||
>
|
|
||||||
撤销申请
|
|
||||||
</a-button>
|
|
||||||
<a-button
|
|
||||||
type="primary"
|
|
||||||
ghost
|
|
||||||
v-if="editableAndRemoveable"
|
|
||||||
:icon="h(EditOutlined)"
|
|
||||||
@click="handleEdit"
|
|
||||||
>
|
|
||||||
重新编辑
|
|
||||||
</a-button>
|
|
||||||
<a-button
|
|
||||||
v-if="editableAndRemoveable"
|
|
||||||
danger
|
|
||||||
ghost
|
|
||||||
type="primary"
|
|
||||||
:icon="h(EditOutlined)"
|
|
||||||
@click="handleRemove"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</a-button>
|
|
||||||
</Space>
|
|
||||||
<Space v-if="type === 'approve'">
|
|
||||||
<a-button
|
|
||||||
type="primary"
|
|
||||||
ghost
|
|
||||||
:icon="h(CheckOutlined)"
|
|
||||||
@click="handleApproval"
|
|
||||||
>
|
|
||||||
通过
|
|
||||||
</a-button>
|
|
||||||
<a-button
|
|
||||||
v-if="buttonPermissions?.termination"
|
|
||||||
danger
|
|
||||||
ghost
|
|
||||||
type="primary"
|
|
||||||
:icon="h(ExclamationCircleOutlined)"
|
|
||||||
@click="handleTermination"
|
|
||||||
>
|
|
||||||
终止
|
|
||||||
</a-button>
|
|
||||||
<a-button
|
|
||||||
v-if="buttonPermissions?.back"
|
|
||||||
danger
|
|
||||||
ghost
|
|
||||||
type="primary"
|
|
||||||
:icon="h(ArrowLeftOutlined)"
|
|
||||||
@click="handleRejection"
|
|
||||||
>
|
|
||||||
驳回
|
|
||||||
</a-button>
|
|
||||||
<Dropdown
|
|
||||||
:get-popup-container="getPopupContainer"
|
|
||||||
placement="bottomRight"
|
|
||||||
>
|
|
||||||
<template #overlay>
|
|
||||||
<Menu>
|
|
||||||
<MenuItem
|
|
||||||
v-if="buttonPermissions?.trust"
|
|
||||||
key="1"
|
|
||||||
@click="() => delegationModalApi.open()"
|
|
||||||
>
|
|
||||||
<UserOutlined class="mr-2" />委托
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
v-if="buttonPermissions?.transfer"
|
|
||||||
key="2"
|
|
||||||
@click="() => transferModalApi.open()"
|
|
||||||
>
|
|
||||||
<RollbackOutlined class="mr-2" /> 转办
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
v-if="showMultiActions && buttonPermissions?.addSign"
|
|
||||||
key="3"
|
|
||||||
@click="() => addSignatureModalApi.open()"
|
|
||||||
>
|
|
||||||
<UsergroupAddOutlined class="mr-2" /> 加签
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
v-if="showMultiActions && buttonPermissions?.subSign"
|
|
||||||
key="4"
|
|
||||||
@click="() => reductionSignatureModalApi.open()"
|
|
||||||
>
|
|
||||||
<UsergroupDeleteOutlined class="mr-2" /> 减签
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</template>
|
|
||||||
<a-button v-if="showButtonOther" :icon="h(MenuOutlined)">
|
|
||||||
其他
|
|
||||||
</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>
|
</Card>
|
||||||
|
|
||||||
<slot v-else name="empty">
|
<slot v-else name="empty">
|
||||||
<Fallback title="点击左侧选择" />
|
<Fallback title="点击左侧选择" />
|
||||||
</slot>
|
</slot>
|
||||||
|
|||||||
@@ -45,11 +45,8 @@ onMounted(async () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 这里无法处理昵称中带,的情况
|
|
||||||
*/
|
|
||||||
const isMultiplePerson = computed(
|
const isMultiplePerson = computed(
|
||||||
() => props.item.approveName?.split(',').length > 1,
|
() => props.item.approver?.split(',').length > 1,
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -87,6 +84,7 @@ const isMultiplePerson = computed(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="cn('mt-2 flex flex-wrap gap-2')" v-if="isMultiplePerson">
|
<div :class="cn('mt-2 flex flex-wrap gap-2')" v-if="isMultiplePerson">
|
||||||
|
<!-- 如果昵称中带, 这里的处理是不准确的 -->
|
||||||
<div
|
<div
|
||||||
:class="cn('bg-foreground/5 flex items-center rounded-full', 'p-1')"
|
:class="cn('bg-foreground/5 flex items-center rounded-full', 'p-1')"
|
||||||
v-for="(name, index) in item.approveName.split(',')"
|
v-for="(name, index) in item.approveName.split(',')"
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Flow } from '#/api/workflow/instance/model';
|
import type { Flow } from '#/api/workflow/instance/model';
|
||||||
|
|
||||||
import { Timeline } from 'ant-design-vue';
|
import { Empty, Timeline } from 'ant-design-vue';
|
||||||
|
|
||||||
import ApprovalTimelineItem from './approval-timeline-item.vue';
|
import ApprovalTimelineItem from './approval-timeline-item.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
interface Props {
|
||||||
list: Flow[];
|
list: Flow[];
|
||||||
}>();
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Timeline v-if="props.list.length > 0">
|
<Timeline v-if="list.length > 0">
|
||||||
<ApprovalTimelineItem
|
<ApprovalTimelineItem v-for="item in list" :key="item.id" :item="item" />
|
||||||
v-for="item in props.list"
|
|
||||||
:key="item.id"
|
|
||||||
:item="item"
|
|
||||||
/>
|
|
||||||
</Timeline>
|
</Timeline>
|
||||||
|
<Empty v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<!--抄送组件-->
|
<!--抄送组件-->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CSSProperties, PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
|
|
||||||
import type { User } from '#/api/system/user/model';
|
import type { User } from '#/api/system/user/model';
|
||||||
|
|
||||||
@@ -18,11 +18,7 @@ defineOptions({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{ allowUserIds?: string; ellipseNumber?: number }>(),
|
||||||
allowUserIds?: string;
|
|
||||||
avatarSize?: number;
|
|
||||||
ellipseNumber?: number;
|
|
||||||
}>(),
|
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 最大显示的头像数量 超过显示为省略号头像
|
* 最大显示的头像数量 超过显示为省略号头像
|
||||||
@@ -32,10 +28,6 @@ const props = withDefaults(
|
|||||||
* 允许选择允许选择的人员ID 会当做参数拼接在uselist接口
|
* 允许选择允许选择的人员ID 会当做参数拼接在uselist接口
|
||||||
*/
|
*/
|
||||||
allowUserIds: '',
|
allowUserIds: '',
|
||||||
/**
|
|
||||||
* 头像大小
|
|
||||||
*/
|
|
||||||
avatarSize: 36,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -65,14 +57,6 @@ function handleFinish(userList: User[]) {
|
|||||||
const displayedList = computed(() => {
|
const displayedList = computed(() => {
|
||||||
return userListModel.value.slice(0, props.ellipseNumber);
|
return userListModel.value.slice(0, props.ellipseNumber);
|
||||||
});
|
});
|
||||||
|
|
||||||
const avatarStyle = computed<CSSProperties>(() => {
|
|
||||||
const { avatarSize } = props;
|
|
||||||
return {
|
|
||||||
width: `${avatarSize}px`,
|
|
||||||
height: `${avatarSize}px`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -87,9 +71,8 @@ const avatarStyle = computed<CSSProperties>(() => {
|
|||||||
<div>
|
<div>
|
||||||
<VbenAvatar
|
<VbenAvatar
|
||||||
:alt="user?.nickName ?? ''"
|
:alt="user?.nickName ?? ''"
|
||||||
class="bg-primary cursor-pointer rounded-full border text-white"
|
class="bg-primary size-[36px] cursor-pointer rounded-full border text-white"
|
||||||
src=""
|
src=""
|
||||||
:size="avatarSize"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -99,14 +82,13 @@ const avatarStyle = computed<CSSProperties>(() => {
|
|||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
v-if="userListModel.length > ellipseNumber"
|
v-if="userListModel.length > ellipseNumber"
|
||||||
class="flex cursor-pointer items-center justify-center rounded-full border bg-[gray] text-white"
|
class="flex size-[36px] cursor-pointer items-center justify-center rounded-full border bg-[gray] text-white"
|
||||||
:style="avatarStyle"
|
|
||||||
>
|
>
|
||||||
+{{ userListModel.length - props.ellipseNumber }}
|
+{{ userListModel.length - props.ellipseNumber }}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
<a-button class="ml-1" size="small" @click="handleOpen">选择人员</a-button>
|
<a-button size="small" @click="handleOpen">选择人员</a-button>
|
||||||
<UserSelectModal
|
<UserSelectModal
|
||||||
:allow-user-ids="allowUserIds"
|
:allow-user-ids="allowUserIds"
|
||||||
@cancel="$emit('cancel')"
|
@cancel="$emit('cancel')"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- 流程图预览组件 -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAppConfig } from '@vben/hooks';
|
import { useAppConfig } from '@vben/hooks';
|
||||||
import { stringify } from '@vben/request';
|
import { stringify } from '@vben/request';
|
||||||
@@ -7,7 +9,14 @@ import { useWarmflowIframe } from './hook';
|
|||||||
|
|
||||||
defineOptions({ name: 'FlowPreview' });
|
defineOptions({ name: 'FlowPreview' });
|
||||||
|
|
||||||
const props = defineProps<{ instanceId: string }>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/**
|
||||||
|
* 流程实例ID
|
||||||
|
*/
|
||||||
|
instanceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||||
|
|
||||||
@@ -21,6 +30,7 @@ const params = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* iframe地址
|
* iframe地址
|
||||||
|
* 后端地址 + 固定flow地址拼接
|
||||||
*/
|
*/
|
||||||
const url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;
|
const url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;
|
||||||
|
|
||||||
@@ -28,5 +38,9 @@ const { iframeRef } = useWarmflowIframe();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<iframe ref="iframeRef" :src="url" class="h-[600px] w-full border"></iframe>
|
<iframe
|
||||||
|
ref="iframeRef"
|
||||||
|
:src="url"
|
||||||
|
class="h-[600px] w-full rounded-[6px] border"
|
||||||
|
></iframe>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable vue/one-component-per-file */
|
|
||||||
import { defineComponent, h, ref } from 'vue';
|
import { defineComponent, h, ref } from 'vue';
|
||||||
|
|
||||||
import { Modal } from 'ant-design-vue';
|
import { Modal } from 'ant-design-vue';
|
||||||
@@ -59,13 +58,3 @@ export function getDiffTimeString(dateTime: string) {
|
|||||||
const diffText = dayjs.duration(diffSeconds, 'seconds').humanize();
|
const diffText = dayjs.duration(diffSeconds, 'seconds').humanize();
|
||||||
return diffText;
|
return diffText;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用默认插槽来定义schema
|
|
||||||
*/
|
|
||||||
export const DefaultSlot = defineComponent({
|
|
||||||
name: 'DefaultSlot',
|
|
||||||
render() {
|
|
||||||
return <div>{this.$slots.default?.()}</div>;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -10,20 +10,26 @@ export function useWarmflowIframe() {
|
|||||||
const iframeRef = useTemplateRef<HTMLIFrameElement>('iframeRef');
|
const iframeRef = useTemplateRef<HTMLIFrameElement>('iframeRef');
|
||||||
const { isDark } = usePreferences();
|
const { isDark } = usePreferences();
|
||||||
|
|
||||||
|
async function iframeLoadEvent() {
|
||||||
|
/**
|
||||||
|
* TODO: 这里可以优化 因为拿不到内部vue的mount状态
|
||||||
|
*/
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
const theme = isDark.value ? 'theme-dark' : 'theme-light';
|
||||||
|
iframeRef.value?.contentWindow?.postMessage({ type: theme });
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
/**
|
/**
|
||||||
* load只是iframe加载完 而非vue加载完
|
* load只是iframe加载完 而非vue加载完
|
||||||
*/
|
*/
|
||||||
iframeRef.value?.addEventListener('load', async () => {
|
iframeRef.value?.addEventListener('load', iframeLoadEvent);
|
||||||
/**
|
|
||||||
* TODO: 这里可以优化 因为拿不到内部vue的mount状态
|
|
||||||
*/
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
const theme = isDark.value ? 'theme-dark' : 'theme-light';
|
|
||||||
iframeRef.value?.contentWindow?.postMessage({ type: theme });
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// onBeforeUnmount(() => {
|
||||||
|
// iframeRef.value?.removeEventListener('load', iframeLoadEvent);
|
||||||
|
// });
|
||||||
|
|
||||||
// 监听主题切换 通知iframe切换
|
// 监听主题切换 通知iframe切换
|
||||||
watch(isDark, (dark) => {
|
watch(isDark, (dark) => {
|
||||||
if (!iframeRef.value) {
|
if (!iframeRef.value) {
|
||||||
|
|||||||
@@ -4,23 +4,21 @@ export { default as ApprovalCard } from './approval-card.vue';
|
|||||||
* 审批同意
|
* 审批同意
|
||||||
*/
|
*/
|
||||||
export { default as approvalModal } from './approval-modal.vue';
|
export { default as approvalModal } from './approval-modal.vue';
|
||||||
export { default as ApprovalPanelDrawerComp } from './approval-panel-drawer.vue';
|
|
||||||
export { default as ApprovalPanel } from './approval-panel.vue';
|
export { default as ApprovalPanel } from './approval-panel.vue';
|
||||||
/**
|
/**
|
||||||
* 审批驳回
|
* 审批驳回
|
||||||
*/
|
*/
|
||||||
export { default as approvalRejectionModal } from './approval-rejection-modal.vue';
|
export { default as approvalRejectionModal } from './approval-rejection-modal.vue';
|
||||||
export { default as ApprovalTimeline } from './approval-timeline.vue';
|
export { default as ApprovalTimeline } from './approval-timeline.vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 选择抄送人
|
* 选择抄送人
|
||||||
*/
|
*/
|
||||||
export { default as CopyComponent } from './copy-component.vue';
|
export { default as CopyComponent } from './copy-component.vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 详情信息 modal
|
* 详情信息 modal
|
||||||
*/
|
*/
|
||||||
export { default as flowInfoModal } from './flow-info-modal.vue';
|
export { default as flowInfoModal } from './flow-info-modal.vue';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流程干预 modal
|
* 流程干预 modal
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import type { FormSchemaGetter } from '#/adapter/form';
|
|
||||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
|
||||||
|
|
||||||
import { DictEnum } from '@vben/constants';
|
|
||||||
import { getPopupContainer } from '@vben/utils';
|
|
||||||
|
|
||||||
import { getDictOptions } from '#/utils/dict';
|
|
||||||
import { renderDict } from '#/utils/render';
|
|
||||||
|
|
||||||
export const querySchema: FormSchemaGetter = () => [
|
|
||||||
{
|
|
||||||
component: 'Input',
|
|
||||||
fieldName: 'configName',
|
|
||||||
label: '参数名称',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'Input',
|
|
||||||
fieldName: 'configKey',
|
|
||||||
label: '参数键名',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'Select',
|
|
||||||
componentProps: {
|
|
||||||
getPopupContainer,
|
|
||||||
options: getDictOptions(DictEnum.SYS_YES_NO),
|
|
||||||
},
|
|
||||||
fieldName: 'configType',
|
|
||||||
label: '系统内置',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
component: 'RangePicker',
|
|
||||||
fieldName: 'createTime',
|
|
||||||
label: '创建时间',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const columns: VxeGridProps['columns'] = [
|
|
||||||
{
|
|
||||||
field: 'id',
|
|
||||||
title: 'ID',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'businessTitle',
|
|
||||||
title: '业务标题',
|
|
||||||
formatter: ({ cellValue }) => cellValue ?? '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'flowName',
|
|
||||||
title: '流程名称',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'flowCode',
|
|
||||||
title: '流程编码',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'nodeName',
|
|
||||||
title: '当前任务',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'processedByName',
|
|
||||||
title: '办理人',
|
|
||||||
formatter: ({ cellValue }) => cellValue?.split?.(',') ?? cellValue,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'flowStatus',
|
|
||||||
title: '流程状态',
|
|
||||||
slots: {
|
|
||||||
default: ({ row }) => {
|
|
||||||
return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'createTime',
|
|
||||||
title: '创建时间',
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// field: 'action',
|
|
||||||
// fixed: 'right',
|
|
||||||
// slots: { default: 'action' },
|
|
||||||
// title: '操作',
|
|
||||||
// resizable: false,
|
|
||||||
// width: 'auto',
|
|
||||||
// },
|
|
||||||
];
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { default as TaskPage } from './task.vue';
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { VbenFormProps } from '@vben/common-ui';
|
|
||||||
|
|
||||||
import type { TaskPageProps } from './types';
|
|
||||||
|
|
||||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
|
||||||
|
|
||||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
|
||||||
|
|
||||||
import { ApprovalPanelDrawerComp } from '..';
|
|
||||||
import { columns, querySchema } from './data';
|
|
||||||
|
|
||||||
const props = defineProps<TaskPageProps>();
|
|
||||||
|
|
||||||
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',
|
|
||||||
// 日期选择格式化
|
|
||||||
fieldMappingTime: [
|
|
||||||
[
|
|
||||||
'createTime',
|
|
||||||
['params[beginTime]', 'params[endTime]'],
|
|
||||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const gridOptions: VxeGridProps = {
|
|
||||||
checkboxConfig: {
|
|
||||||
// 高亮
|
|
||||||
highlight: true,
|
|
||||||
// 翻页时保留选中状态
|
|
||||||
reserve: true,
|
|
||||||
},
|
|
||||||
columns,
|
|
||||||
height: 'auto',
|
|
||||||
keepSource: true,
|
|
||||||
pagerConfig: {},
|
|
||||||
proxyConfig: {
|
|
||||||
ajax: {
|
|
||||||
query: async ({ page }, formValues = {}) => {
|
|
||||||
return await props.listApi({
|
|
||||||
pageNum: page.currentPage,
|
|
||||||
pageSize: page.pageSize,
|
|
||||||
...formValues,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rowConfig: {
|
|
||||||
keyField: 'id',
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* TODO: id
|
|
||||||
*/
|
|
||||||
id: 'workflow-task',
|
|
||||||
rowClassName: 'cursor-pointer',
|
|
||||||
};
|
|
||||||
|
|
||||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
|
||||||
formOptions,
|
|
||||||
gridOptions,
|
|
||||||
gridEvents: {
|
|
||||||
cellClick: ({ row }) => {
|
|
||||||
handleOpen(row);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [ApprovalPanelDrawer, drawerApi] = useVbenDrawer({
|
|
||||||
connectedComponent: ApprovalPanelDrawerComp,
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleOpen(row: any) {
|
|
||||||
drawerApi.setData({ task: row, type: props.type }).open();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Page :auto-content-height="true">
|
|
||||||
<BasicTable :table-title="tableTitle" />
|
|
||||||
<ApprovalPanelDrawer @reload="() => tableApi.query()" />
|
|
||||||
</Page>
|
|
||||||
</template>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import type { ApprovalType } from '../type';
|
|
||||||
|
|
||||||
import type { PageResult } from '#/api/common';
|
|
||||||
import type { TaskInfo } from '#/api/workflow/task/model';
|
|
||||||
|
|
||||||
export {};
|
|
||||||
|
|
||||||
export interface TaskPageProps {
|
|
||||||
/**
|
|
||||||
* 表格显示的标题
|
|
||||||
*/
|
|
||||||
tableTitle?: string;
|
|
||||||
/**
|
|
||||||
* 类型
|
|
||||||
*/
|
|
||||||
type: ApprovalType;
|
|
||||||
listApi: (params?: any) => Promise<PageResult<TaskInfo>>;
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
export {};
|
export {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* myself 我发起的
|
* myself 我发起的
|
||||||
* readonly 只读 只用于查看
|
* readonly 只读 只用于查看
|
||||||
* approve 审批
|
* approve 审批(我的待办)
|
||||||
* admin 流程监控 - 待办任务使用
|
* admin 流程监控 - 待办任务使用
|
||||||
*/
|
*/
|
||||||
export type ApprovalType = 'admin' | 'approve' | 'myself' | 'readonly';
|
export type ApprovalType = 'admin' | 'approve' | 'myself' | 'readonly';
|
||||||
|
|||||||
@@ -1,27 +1,18 @@
|
|||||||
import type { FormSchemaGetter } from '#/adapter/form';
|
import type { FormSchemaGetter } from '#/adapter/form';
|
||||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
import type { VxeGridProps } from '#/adapter/vxe-table';
|
||||||
|
|
||||||
import { markRaw } from 'vue';
|
|
||||||
|
|
||||||
import { DictEnum } from '@vben/constants';
|
import { DictEnum } from '@vben/constants';
|
||||||
|
|
||||||
import { OptionsTag } from '#/components/table';
|
import { OptionsTag } from '#/components/table';
|
||||||
import { renderDict } from '#/utils/render';
|
import { renderDict } from '#/utils/render';
|
||||||
|
|
||||||
import { CopyComponent } from '../components';
|
|
||||||
import { DefaultSlot } from '../components/helper';
|
|
||||||
import { activityStatusOptions } from '../processDefinition/constant';
|
import { activityStatusOptions } from '../processDefinition/constant';
|
||||||
|
|
||||||
export const querySchema: FormSchemaGetter = () => [
|
export const querySchema: FormSchemaGetter = () => [
|
||||||
/**
|
|
||||||
* https://github.com/facebook/react/issues/6284
|
|
||||||
* https://github.com/ant-design/ant-design/issues/2950#issuecomment-245852795
|
|
||||||
* nodeName算一种`关键字` 会导致报错
|
|
||||||
*/
|
|
||||||
{
|
{
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
label: '任务名称',
|
label: '任务名称',
|
||||||
fieldName: '_nodeName',
|
fieldName: 'nodeName',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
@@ -33,17 +24,6 @@ export const querySchema: FormSchemaGetter = () => [
|
|||||||
label: '流程编码',
|
label: '流程编码',
|
||||||
fieldName: 'flowCode',
|
fieldName: 'flowCode',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
fieldName: 'createByIds',
|
|
||||||
defaultValue: [],
|
|
||||||
component: markRaw(DefaultSlot),
|
|
||||||
label: '申请人',
|
|
||||||
renderComponentContent: (model) => ({
|
|
||||||
default: () => (
|
|
||||||
<CopyComponent avatarSize={30} v-model:userList={model.createByIds} />
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const columns: VxeGridProps['columns'] = [
|
export const columns: VxeGridProps['columns'] = [
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { $t } from '@vben/locales';
|
|||||||
import { getVxePopupContainer } from '@vben/utils';
|
import { getVxePopupContainer } from '@vben/utils';
|
||||||
|
|
||||||
import { Modal, Popconfirm, RadioGroup, Space } from 'ant-design-vue';
|
import { Modal, Popconfirm, RadioGroup, Space } from 'ant-design-vue';
|
||||||
import { isArray } from 'lodash-es';
|
|
||||||
|
|
||||||
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
import { useVbenVxeGrid, vxeCheckboxChecked } from '#/adapter/vxe-table';
|
||||||
import {
|
import {
|
||||||
@@ -96,18 +95,6 @@ const gridOptions: VxeGridProps = {
|
|||||||
Reflect.deleteProperty(formValues, 'category');
|
Reflect.deleteProperty(formValues, 'category');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换数据
|
|
||||||
if (isArray(formValues.createByIds)) {
|
|
||||||
formValues.createByIds = (formValues.createByIds as Array<any>).map(
|
|
||||||
(item) => item.userId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// 使用nodeName会导致选人组件hover报错
|
|
||||||
if (formValues._nodeName) {
|
|
||||||
formValues.nodeName = formValues._nodeName;
|
|
||||||
Reflect.deleteProperty(formValues, '_nodeName');
|
|
||||||
}
|
|
||||||
|
|
||||||
return await currentTypeApi({
|
return await currentTypeApi({
|
||||||
pageNum: page.currentPage,
|
pageNum: page.currentPage,
|
||||||
pageSize: page.pageSize,
|
pageSize: page.pageSize,
|
||||||
@@ -163,11 +150,10 @@ const [InstanceVariableModal, instanceVariableModalApi] = useVbenModal({
|
|||||||
connectedComponent: instanceVariableModal,
|
connectedComponent: instanceVariableModal,
|
||||||
});
|
});
|
||||||
function handleVariable(row: Recordable<any>) {
|
function handleVariable(row: Recordable<any>) {
|
||||||
instanceVariableModalApi.setData({
|
instanceVariableModalApi.setData({ instanceId: row.id });
|
||||||
instanceId: row.id,
|
|
||||||
});
|
|
||||||
instanceVariableModalApi.open();
|
instanceVariableModalApi.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
const [FlowInfoModal, flowInfoModalApi] = useVbenModal({
|
const [FlowInfoModal, flowInfoModalApi] = useVbenModal({
|
||||||
connectedComponent: flowInfoModal,
|
connectedComponent: flowInfoModal,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ref } from 'vue';
|
|||||||
import { JsonPreview, useVbenModal } from '@vben/common-ui';
|
import { JsonPreview, useVbenModal } from '@vben/common-ui';
|
||||||
import { cn, getPopupContainer } from '@vben/utils';
|
import { cn, getPopupContainer } from '@vben/utils';
|
||||||
|
|
||||||
import { Modal, Tag } from 'ant-design-vue';
|
import { message, Modal, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
import { useVbenForm } from '#/adapter/form';
|
import { useVbenForm } from '#/adapter/form';
|
||||||
import { instanceVariable, updateFlowVariable } from '#/api/workflow/instance';
|
import { instanceVariable, updateFlowVariable } from '#/api/workflow/instance';
|
||||||
@@ -107,11 +107,30 @@ const [Form, formApi] = useVbenForm({
|
|||||||
getPopupContainer,
|
getPopupContainer,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'valueType',
|
||||||
|
component: 'Select',
|
||||||
|
label: '变量类型',
|
||||||
|
rules: 'selectRequired',
|
||||||
|
componentProps: {
|
||||||
|
getPopupContainer,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'string',
|
||||||
|
value: 'string',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'boolean | number | object (使用JSON.parse)',
|
||||||
|
value: 'object',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'value',
|
fieldName: 'value',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
label: '变量值',
|
label: '变量值',
|
||||||
rules: 'selectRequired',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
resetButtonOptions: {
|
resetButtonOptions: {
|
||||||
@@ -142,11 +161,24 @@ async function handleSubmit(values: any) {
|
|||||||
|
|
||||||
const { instanceId } = modalApi.getData() as ModalData;
|
const { instanceId } = modalApi.getData() as ModalData;
|
||||||
|
|
||||||
|
let transformValue = values.value;
|
||||||
|
if (values.valueType !== 'string') {
|
||||||
|
try {
|
||||||
|
transformValue = JSON.parse(values.value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
message.error(error.message);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 修改
|
// 修改
|
||||||
const requestData = {
|
const requestData = {
|
||||||
instanceId,
|
instanceId,
|
||||||
key: values.key,
|
key: values.key,
|
||||||
value: values.value,
|
value: transformValue,
|
||||||
};
|
};
|
||||||
await updateFlowVariable(requestData);
|
await updateFlowVariable(requestData);
|
||||||
await formApi.resetForm();
|
await formApi.resetForm();
|
||||||
@@ -170,8 +202,12 @@ async function handleSubmit(values: any) {
|
|||||||
>
|
>
|
||||||
<JsonPreview :data="data" />
|
<JsonPreview :data="data" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 text-sm font-medium text-red-500">
|
<div class="mt-2 break-all text-sm font-medium text-orange-500">
|
||||||
由于限制 只能变更字段为string类型
|
需要支持变量类型需要更改后端代码(原版只支持string类型)
|
||||||
|
<div>
|
||||||
|
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowVariableBo.java
|
||||||
|
</div>
|
||||||
|
将value的类型改为Object才能使用
|
||||||
</div>
|
</div>
|
||||||
<Form class="mt-2" />
|
<Form class="mt-2" />
|
||||||
</BasicModal>
|
</BasicModal>
|
||||||
|
|||||||
@@ -1,235 +1,360 @@
|
|||||||
<script setup lang="tsx">
|
<!-- eslint-disable no-use-before-define -->
|
||||||
import type { RadioChangeEvent } from 'ant-design-vue';
|
<script setup lang="ts">
|
||||||
|
import type { User } from '#/api/system/user/model';
|
||||||
|
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||||
|
|
||||||
import type { VbenFormProps } from '@vben/common-ui';
|
import { computed, nextTick, onMounted, ref, useTemplateRef } from 'vue';
|
||||||
|
|
||||||
import type { ComponentType } from '#/adapter/component';
|
import { Page } from '@vben/common-ui';
|
||||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
import { useTabs } from '@vben/hooks';
|
||||||
|
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||||
|
|
||||||
import { markRaw, onMounted, ref } from 'vue';
|
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 { Page, useVbenDrawer } from '@vben/common-ui';
|
|
||||||
import { DictEnum } from '@vben/constants';
|
|
||||||
|
|
||||||
import { RadioGroup } from 'ant-design-vue';
|
|
||||||
import { isArray } from 'lodash-es';
|
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
|
||||||
import { categoryTree } from '#/api/workflow/category';
|
import { categoryTree } from '#/api/workflow/category';
|
||||||
import { pageByAllTaskFinish, pageByAllTaskWait } from '#/api/workflow/task';
|
import { pageByAllTaskFinish, pageByAllTaskWait } from '#/api/workflow/task';
|
||||||
import { renderDict } from '#/utils/render';
|
|
||||||
|
|
||||||
import { ApprovalPanelDrawerComp, CopyComponent } from '../components';
|
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||||
import { DefaultSlot } from '../components/helper';
|
import { bottomOffset } from './constant';
|
||||||
|
|
||||||
const formOptions: VbenFormProps<ComponentType> = {
|
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||||
commonConfig: {
|
|
||||||
labelWidth: 80,
|
|
||||||
componentProps: {
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
schema: [
|
|
||||||
{
|
|
||||||
fieldName: 'category',
|
|
||||||
component: 'TreeSelect',
|
|
||||||
label: '流程分类',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'flowCode',
|
|
||||||
component: 'Input',
|
|
||||||
label: '流程编码',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'createByIds',
|
|
||||||
defaultValue: [],
|
|
||||||
component: markRaw(DefaultSlot),
|
|
||||||
label: '申请人',
|
|
||||||
renderComponentContent: (model) => ({
|
|
||||||
default: () => (
|
|
||||||
<CopyComponent avatarSize={30} v-model:userList={model.createByIds} />
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
|
||||||
// 日期选择格式化
|
|
||||||
fieldMappingTime: [
|
|
||||||
[
|
|
||||||
'createTime',
|
|
||||||
['params[beginTime]', 'params[endTime]'],
|
|
||||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
/**
|
||||||
const tree = await categoryTree();
|
* 流程监控 - 待办任务页面的id不唯一 改为前端处理
|
||||||
tableApi.formApi.updateSchema([
|
*/
|
||||||
{
|
interface TaskItem extends TaskInfo {
|
||||||
fieldName: 'category',
|
active: boolean;
|
||||||
componentProps: {
|
randomId: string;
|
||||||
fieldNames: { label: 'label', value: 'id' },
|
}
|
||||||
showSearch: true,
|
|
||||||
treeData: tree,
|
const taskList = ref<TaskItem[]>([]);
|
||||||
treeDefaultExpandAll: true,
|
const taskTotal = ref(0);
|
||||||
treeLine: { showLeafIcon: false },
|
const page = ref(1);
|
||||||
// 筛选的字段
|
const loading = ref(false);
|
||||||
treeNodeFilterProp: 'label',
|
|
||||||
// 选中后显示在输入框的值
|
|
||||||
treeNodeLabelProp: 'label',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
{ label: '运行中', value: 'running' },
|
{ label: '待办任务', value: 'todo' },
|
||||||
{ label: '已完成', value: 'finish' },
|
{ label: '已办任务', value: 'done' },
|
||||||
] as const;
|
];
|
||||||
type OptionValue = (typeof typeOptions)[number]['value'];
|
const currentType = ref('todo');
|
||||||
let currentTypeApi = pageByAllTaskWait;
|
const currentApi = computed(() => {
|
||||||
const currentType = ref<OptionValue>('running');
|
if (currentType.value === 'todo') {
|
||||||
async function handleTypeChange(e: RadioChangeEvent) {
|
return pageByAllTaskWait;
|
||||||
const { value } = e.target;
|
|
||||||
switch (value) {
|
|
||||||
case 'finish': {
|
|
||||||
currentTypeApi = pageByAllTaskFinish;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'running': {
|
|
||||||
currentTypeApi = pageByAllTaskWait;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
|
||||||
await tableApi.reload();
|
taskList.value = [];
|
||||||
|
await nextTick();
|
||||||
|
await reload(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const gridOptions: VxeGridProps = {
|
const defaultFormData = {
|
||||||
checkboxConfig: {
|
flowName: '', // 流程定义名称
|
||||||
// 高亮
|
nodeName: '', // 任务名称
|
||||||
highlight: true,
|
flowCode: '', // 流程定义编码
|
||||||
// 翻页时保留选中状态
|
createByIds: [] as string[], // 创建人
|
||||||
reserve: true,
|
category: null as null | number, // 流程分类
|
||||||
},
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'id',
|
|
||||||
title: 'ID',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'businessTitle',
|
|
||||||
title: '业务标题',
|
|
||||||
formatter: ({ cellValue }) => cellValue ?? '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'flowName',
|
|
||||||
title: '流程名称',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'categoryName',
|
|
||||||
title: '流程分类',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'flowCode',
|
|
||||||
title: '流程编码',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'nodeName',
|
|
||||||
title: '当前任务',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'processedByName',
|
|
||||||
title: '办理人',
|
|
||||||
formatter: ({ cellValue }) => cellValue?.split?.(',') ?? cellValue,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'flowStatus',
|
|
||||||
title: '流程状态',
|
|
||||||
slots: {
|
|
||||||
default: ({ row }) => {
|
|
||||||
return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'createTime',
|
|
||||||
title: '创建时间',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
height: 'auto',
|
|
||||||
keepSource: true,
|
|
||||||
pagerConfig: {},
|
|
||||||
proxyConfig: {
|
|
||||||
ajax: {
|
|
||||||
query: async ({ page }, formValues = {}) => {
|
|
||||||
// 转换数据
|
|
||||||
if (isArray(formValues.createByIds)) {
|
|
||||||
formValues.createByIds = (formValues.createByIds as Array<any>).map(
|
|
||||||
(item) => item.userId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await currentTypeApi({
|
|
||||||
pageNum: page.currentPage,
|
|
||||||
pageSize: page.pageSize,
|
|
||||||
...formValues,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rowConfig: {
|
|
||||||
keyField: 'id',
|
|
||||||
isCurrent: true,
|
|
||||||
},
|
|
||||||
id: 'workflow-task-myself',
|
|
||||||
rowClassName: 'cursor-pointer',
|
|
||||||
};
|
};
|
||||||
|
const formData = ref(cloneDeep(defaultFormData));
|
||||||
|
|
||||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
/**
|
||||||
formOptions,
|
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||||
gridOptions,
|
*/
|
||||||
gridEvents: {
|
const isLoadComplete = computed(
|
||||||
cellClick: ({ row }) => {
|
() => taskList.value.length === taskTotal.value,
|
||||||
handleOpen(row);
|
);
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [ApprovalPanelDrawer, drawerApi] = useVbenDrawer({
|
// 卡片父容器的ref
|
||||||
connectedComponent: ApprovalPanelDrawerComp,
|
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||||
});
|
|
||||||
|
|
||||||
function handleOpen(row: any) {
|
/**
|
||||||
let type = '';
|
* @param resetFields 是否清空查询参数
|
||||||
switch (currentType.value) {
|
*/
|
||||||
case 'finish': {
|
async function reload(resetFields: boolean = false) {
|
||||||
type = 'readonly';
|
// 需要先滚动到顶部
|
||||||
break;
|
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||||
}
|
|
||||||
case 'running': {
|
page.value = 1;
|
||||||
type = 'admin';
|
currentTask.value = undefined;
|
||||||
break;
|
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);
|
||||||
}
|
}
|
||||||
drawerApi.setData({ task: row, type }).open();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 - bottomOffset;
|
||||||
|
console.log('scrollTop + clientHeight', scrollTop + clientHeight);
|
||||||
|
console.log('scrollHeight', 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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page :auto-content-height="true">
|
<Page :auto-content-height="true">
|
||||||
<BasicTable>
|
<div class="flex h-full gap-2">
|
||||||
<template #toolbar-actions>
|
<div
|
||||||
<RadioGroup
|
class="bg-background relative flex h-full min-w-[320px] max-w-[320px] flex-col rounded-lg"
|
||||||
v-model:value="currentType"
|
>
|
||||||
:options="typeOptions"
|
<!-- 搜索条件 -->
|
||||||
button-style="solid"
|
<div
|
||||||
option-type="button"
|
class="bg-background z-100 sticky left-0 top-0 w-full rounded-t-lg border-b-[1px] border-solid p-2"
|
||||||
@change="handleTypeChange"
|
>
|
||||||
/>
|
<Segmented
|
||||||
</template>
|
v-model:value="currentType"
|
||||||
</BasicTable>
|
:options="typeOptions"
|
||||||
<ApprovalPanelDrawer @reload="() => tableApi.query()" />
|
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>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.thin-scrollbar {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
@apply thin-scrollbar;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,162 +1,257 @@
|
|||||||
|
<!-- eslint-disable no-use-before-define -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { VbenFormProps } from '@vben/common-ui';
|
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||||
|
|
||||||
import type { ComponentType } from '#/adapter/component';
|
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
|
||||||
|
|
||||||
import { onMounted } from 'vue';
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { useTabs } from '@vben/hooks';
|
||||||
|
import { getPopupContainer } from '@vben/utils';
|
||||||
|
|
||||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||||
import { DictEnum } from '@vben/constants';
|
import {
|
||||||
|
Empty,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
InputSearch,
|
||||||
|
Popover,
|
||||||
|
Spin,
|
||||||
|
Tooltip,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import { cloneDeep, debounce } from 'lodash-es';
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
|
||||||
import { categoryTree } from '#/api/workflow/category';
|
|
||||||
import { pageByCurrent } from '#/api/workflow/instance';
|
import { pageByCurrent } from '#/api/workflow/instance';
|
||||||
import { renderDict } from '#/utils/render';
|
|
||||||
|
|
||||||
import { ApprovalPanelDrawerComp } from '../components';
|
import { ApprovalCard, ApprovalPanel } from '../components';
|
||||||
|
import { bottomOffset } from './constant';
|
||||||
|
|
||||||
const formOptions: VbenFormProps<ComponentType> = {
|
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||||
commonConfig: {
|
|
||||||
labelWidth: 80,
|
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
|
||||||
componentProps: {
|
const taskTotal = ref(0);
|
||||||
allowClear: true,
|
const page = ref(1);
|
||||||
},
|
const loading = ref(false);
|
||||||
},
|
|
||||||
schema: [
|
const defaultFormData = {
|
||||||
{
|
flowName: '', // 流程定义名称
|
||||||
fieldName: 'category',
|
nodeName: '', // 任务名称
|
||||||
component: 'TreeSelect',
|
flowCode: '', // 流程定义编码
|
||||||
label: '流程分类',
|
category: null as null | number, // 流程分类
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'flowCode',
|
|
||||||
component: 'Input',
|
|
||||||
label: '流程编码',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
|
||||||
// 日期选择格式化
|
|
||||||
fieldMappingTime: [
|
|
||||||
[
|
|
||||||
'createTime',
|
|
||||||
['params[beginTime]', 'params[endTime]'],
|
|
||||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
const formData = ref(cloneDeep(defaultFormData));
|
||||||
|
|
||||||
onMounted(async () => {
|
/**
|
||||||
const tree = await categoryTree();
|
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||||
tableApi.formApi.updateSchema([
|
*/
|
||||||
{
|
const isLoadComplete = computed(
|
||||||
fieldName: 'category',
|
() => taskList.value.length === taskTotal.value,
|
||||||
componentProps: {
|
);
|
||||||
fieldNames: { label: 'label', value: 'id' },
|
|
||||||
showSearch: true,
|
|
||||||
treeData: tree,
|
|
||||||
treeDefaultExpandAll: true,
|
|
||||||
treeLine: { showLeafIcon: false },
|
|
||||||
// 筛选的字段
|
|
||||||
treeNodeFilterProp: 'label',
|
|
||||||
// 选中后显示在输入框的值
|
|
||||||
treeNodeLabelProp: 'label',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const gridOptions: VxeGridProps = {
|
// 卡片父容器的ref
|
||||||
checkboxConfig: {
|
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||||
// 高亮
|
|
||||||
highlight: true,
|
|
||||||
// 翻页时保留选中状态
|
|
||||||
reserve: true,
|
|
||||||
},
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'id',
|
|
||||||
title: 'ID',
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// field: 'businessTitle',
|
|
||||||
// title: '业务标题',
|
|
||||||
// formatter: ({ cellValue }) => cellValue ?? '-',
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
field: 'flowName',
|
|
||||||
title: '流程名称',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'categoryName',
|
|
||||||
title: '流程分类',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'flowCode',
|
|
||||||
title: '流程编码',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'nodeName',
|
|
||||||
title: '当前任务',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'flowStatus',
|
|
||||||
title: '流程状态',
|
|
||||||
slots: {
|
|
||||||
default: ({ row }) => {
|
|
||||||
return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'createTime',
|
|
||||||
title: '创建时间',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
height: 'auto',
|
|
||||||
keepSource: true,
|
|
||||||
pagerConfig: {},
|
|
||||||
proxyConfig: {
|
|
||||||
ajax: {
|
|
||||||
query: async ({ page }, formValues = {}) => {
|
|
||||||
return await pageByCurrent({
|
|
||||||
pageNum: page.currentPage,
|
|
||||||
pageSize: page.pageSize,
|
|
||||||
...formValues,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rowConfig: {
|
|
||||||
keyField: 'id',
|
|
||||||
isCurrent: true,
|
|
||||||
},
|
|
||||||
id: 'workflow-task-myself',
|
|
||||||
rowClassName: 'cursor-pointer',
|
|
||||||
};
|
|
||||||
|
|
||||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
/**
|
||||||
formOptions,
|
* @param resetFields 是否清空查询参数
|
||||||
gridOptions,
|
*/
|
||||||
gridEvents: {
|
async function reload(resetFields: boolean = false) {
|
||||||
cellClick: ({ row }) => {
|
// 需要先滚动到顶部
|
||||||
handleOpen(row);
|
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [ApprovalPanelDrawer, drawerApi] = useVbenDrawer({
|
page.value = 1;
|
||||||
connectedComponent: ApprovalPanelDrawerComp,
|
currentTask.value = undefined;
|
||||||
});
|
taskTotal.value = 0;
|
||||||
|
lastSelectId.value = '';
|
||||||
|
|
||||||
function handleOpen(row: any) {
|
if (resetFields) {
|
||||||
drawerApi.setData({ task: row, type: 'myself' }).open();
|
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 - bottomOffset;
|
||||||
|
|
||||||
|
// 滚动到底部且没有加载完成
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page :auto-content-height="true">
|
<Page :auto-content-height="true">
|
||||||
<BasicTable table-title="我发起的" />
|
<div class="flex h-full gap-2">
|
||||||
<ApprovalPanelDrawer @reload="() => tableApi.query()" />
|
<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>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.thin-scrollbar {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
@apply thin-scrollbar;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,183 +1,299 @@
|
|||||||
<script setup lang="tsx">
|
<!-- eslint-disable no-use-before-define -->
|
||||||
import type { VbenFormProps } from '@vben/common-ui';
|
<script setup lang="ts">
|
||||||
|
import type { User } from '#/api/system/user/model';
|
||||||
|
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||||
|
|
||||||
import type { ComponentType } from '#/adapter/component';
|
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
|
||||||
|
|
||||||
import { markRaw, onMounted } from 'vue';
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||||
|
|
||||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||||
import { DictEnum } from '@vben/constants';
|
import {
|
||||||
|
Empty,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
InputSearch,
|
||||||
|
Popover,
|
||||||
|
Spin,
|
||||||
|
Tooltip,
|
||||||
|
TreeSelect,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import { cloneDeep, debounce } from 'lodash-es';
|
||||||
|
|
||||||
import { isArray } from 'lodash-es';
|
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
|
||||||
import { categoryTree } from '#/api/workflow/category';
|
import { categoryTree } from '#/api/workflow/category';
|
||||||
import { pageByTaskCopy } from '#/api/workflow/task';
|
import { pageByTaskCopy } from '#/api/workflow/task';
|
||||||
import { renderDict } from '#/utils/render';
|
|
||||||
|
|
||||||
import { ApprovalPanelDrawerComp, CopyComponent } from '../components';
|
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||||
import { DefaultSlot } from '../components/helper';
|
import { bottomOffset } from './constant';
|
||||||
|
|
||||||
const formOptions: VbenFormProps<ComponentType> = {
|
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||||
commonConfig: {
|
|
||||||
labelWidth: 80,
|
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
|
||||||
componentProps: {
|
const taskTotal = ref(0);
|
||||||
allowClear: true,
|
const page = ref(1);
|
||||||
},
|
const loading = ref(false);
|
||||||
},
|
|
||||||
schema: [
|
const defaultFormData = {
|
||||||
{
|
flowName: '', // 流程定义名称
|
||||||
fieldName: 'category',
|
nodeName: '', // 任务名称
|
||||||
component: 'TreeSelect',
|
flowCode: '', // 流程定义编码
|
||||||
label: '流程分类',
|
createByIds: [] as string[], // 创建人
|
||||||
},
|
category: null as null | number, // 流程分类
|
||||||
{
|
|
||||||
fieldName: 'flowCode',
|
|
||||||
component: 'Input',
|
|
||||||
label: '流程编码',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'createByIds',
|
|
||||||
defaultValue: [],
|
|
||||||
component: markRaw(DefaultSlot),
|
|
||||||
label: '申请人',
|
|
||||||
renderComponentContent: (model) => ({
|
|
||||||
default: () => (
|
|
||||||
<CopyComponent avatarSize={30} v-model:userList={model.createByIds} />
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
|
||||||
// 日期选择格式化
|
|
||||||
fieldMappingTime: [
|
|
||||||
[
|
|
||||||
'createTime',
|
|
||||||
['params[beginTime]', 'params[endTime]'],
|
|
||||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
const formData = ref(cloneDeep(defaultFormData));
|
||||||
|
|
||||||
onMounted(async () => {
|
/**
|
||||||
const tree = await categoryTree();
|
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||||
tableApi.formApi.updateSchema([
|
*/
|
||||||
{
|
const isLoadComplete = computed(
|
||||||
fieldName: 'category',
|
() => taskList.value.length === taskTotal.value,
|
||||||
componentProps: {
|
);
|
||||||
fieldNames: { label: 'label', value: 'id' },
|
|
||||||
showSearch: true,
|
|
||||||
treeData: tree,
|
|
||||||
treeDefaultExpandAll: true,
|
|
||||||
treeLine: { showLeafIcon: false },
|
|
||||||
// 筛选的字段
|
|
||||||
treeNodeFilterProp: 'label',
|
|
||||||
// 选中后显示在输入框的值
|
|
||||||
treeNodeLabelProp: 'label',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const gridOptions: VxeGridProps = {
|
// 卡片父容器的ref
|
||||||
checkboxConfig: {
|
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||||
// 高亮
|
|
||||||
highlight: true,
|
|
||||||
// 翻页时保留选中状态
|
|
||||||
reserve: true,
|
|
||||||
},
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'id',
|
|
||||||
title: 'ID',
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// field: 'businessTitle',
|
|
||||||
// title: '业务标题',
|
|
||||||
// formatter: ({ cellValue }) => cellValue ?? '-',
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
field: 'flowName',
|
|
||||||
title: '流程名称',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'categoryName',
|
|
||||||
title: '流程分类',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'flowCode',
|
|
||||||
title: '流程编码',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'nodeName',
|
|
||||||
title: '当前任务',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'flowStatus',
|
|
||||||
title: '流程状态',
|
|
||||||
slots: {
|
|
||||||
default: ({ row }) => {
|
|
||||||
return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'createTime',
|
|
||||||
title: '创建时间',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
height: 'auto',
|
|
||||||
keepSource: true,
|
|
||||||
pagerConfig: {},
|
|
||||||
proxyConfig: {
|
|
||||||
ajax: {
|
|
||||||
query: async ({ page }, formValues = {}) => {
|
|
||||||
// 转换数据
|
|
||||||
if (isArray(formValues.createByIds)) {
|
|
||||||
formValues.createByIds = (formValues.createByIds as Array<any>).map(
|
|
||||||
(item) => item.userId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await pageByTaskCopy({
|
/**
|
||||||
pageNum: page.currentPage,
|
* @param resetFields 是否清空查询参数
|
||||||
pageSize: page.pageSize,
|
*/
|
||||||
...formValues,
|
async function reload(resetFields: boolean = false) {
|
||||||
});
|
// 需要先滚动到顶部
|
||||||
},
|
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||||
},
|
|
||||||
},
|
|
||||||
rowConfig: {
|
|
||||||
keyField: 'id',
|
|
||||||
isCurrent: true,
|
|
||||||
},
|
|
||||||
id: 'workflow-task-finish',
|
|
||||||
rowClassName: 'cursor-pointer',
|
|
||||||
};
|
|
||||||
|
|
||||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
page.value = 1;
|
||||||
formOptions,
|
currentTask.value = undefined;
|
||||||
gridOptions,
|
taskTotal.value = 0;
|
||||||
gridEvents: {
|
lastSelectId.value = '';
|
||||||
cellClick: ({ row }) => {
|
|
||||||
handleOpen(row);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [ApprovalPanelDrawer, drawerApi] = useVbenDrawer({
|
if (resetFields) {
|
||||||
connectedComponent: ApprovalPanelDrawerComp,
|
formData.value = cloneDeep(defaultFormData);
|
||||||
});
|
selectedUserList.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
function handleOpen(row: any) {
|
loading.value = true;
|
||||||
drawerApi.setData({ task: row, type: 'readonly' }).open();
|
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 - bottomOffset;
|
||||||
|
|
||||||
|
// 滚动到底部且没有加载完成
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page :auto-content-height="true">
|
<Page :auto-content-height="true">
|
||||||
<BasicTable table-title="我的抄送" />
|
<div class="flex h-full gap-2">
|
||||||
<ApprovalPanelDrawer @reload="() => tableApi.query()" />
|
<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>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.thin-scrollbar {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
@apply thin-scrollbar;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,183 +1,299 @@
|
|||||||
<script setup lang="tsx">
|
<!-- eslint-disable no-use-before-define -->
|
||||||
import type { VbenFormProps } from '@vben/common-ui';
|
<script setup lang="ts">
|
||||||
|
import type { User } from '#/api/system/user/model';
|
||||||
|
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||||
|
|
||||||
import type { ComponentType } from '#/adapter/component';
|
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
|
||||||
|
|
||||||
import { markRaw, onMounted } from 'vue';
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||||
|
|
||||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||||
import { DictEnum } from '@vben/constants';
|
import {
|
||||||
|
Empty,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
InputSearch,
|
||||||
|
Popover,
|
||||||
|
Spin,
|
||||||
|
Tooltip,
|
||||||
|
TreeSelect,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import { cloneDeep, debounce } from 'lodash-es';
|
||||||
|
|
||||||
import { isArray } from 'lodash-es';
|
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
|
||||||
import { categoryTree } from '#/api/workflow/category';
|
import { categoryTree } from '#/api/workflow/category';
|
||||||
import { pageByTaskFinish } from '#/api/workflow/task';
|
import { pageByTaskFinish } from '#/api/workflow/task';
|
||||||
import { renderDict } from '#/utils/render';
|
|
||||||
|
|
||||||
import { ApprovalPanelDrawerComp, CopyComponent } from '../components';
|
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||||
import { DefaultSlot } from '../components/helper';
|
import { bottomOffset } from './constant';
|
||||||
|
|
||||||
const formOptions: VbenFormProps<ComponentType> = {
|
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||||
commonConfig: {
|
|
||||||
labelWidth: 80,
|
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
|
||||||
componentProps: {
|
const taskTotal = ref(0);
|
||||||
allowClear: true,
|
const page = ref(1);
|
||||||
},
|
const loading = ref(false);
|
||||||
},
|
|
||||||
schema: [
|
const defaultFormData = {
|
||||||
{
|
flowName: '', // 流程定义名称
|
||||||
fieldName: 'category',
|
nodeName: '', // 任务名称
|
||||||
component: 'TreeSelect',
|
flowCode: '', // 流程定义编码
|
||||||
label: '流程分类',
|
createByIds: [] as string[], // 创建人
|
||||||
},
|
category: null as null | number, // 流程分类
|
||||||
{
|
|
||||||
fieldName: 'flowCode',
|
|
||||||
component: 'Input',
|
|
||||||
label: '流程编码',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'createByIds',
|
|
||||||
defaultValue: [],
|
|
||||||
component: markRaw(DefaultSlot),
|
|
||||||
label: '申请人',
|
|
||||||
renderComponentContent: (model) => ({
|
|
||||||
default: () => (
|
|
||||||
<CopyComponent avatarSize={30} v-model:userList={model.createByIds} />
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
|
||||||
// 日期选择格式化
|
|
||||||
fieldMappingTime: [
|
|
||||||
[
|
|
||||||
'createTime',
|
|
||||||
['params[beginTime]', 'params[endTime]'],
|
|
||||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
const formData = ref(cloneDeep(defaultFormData));
|
||||||
|
|
||||||
onMounted(async () => {
|
/**
|
||||||
const tree = await categoryTree();
|
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||||
tableApi.formApi.updateSchema([
|
*/
|
||||||
{
|
const isLoadComplete = computed(
|
||||||
fieldName: 'category',
|
() => taskList.value.length === taskTotal.value,
|
||||||
componentProps: {
|
);
|
||||||
fieldNames: { label: 'label', value: 'id' },
|
|
||||||
showSearch: true,
|
|
||||||
treeData: tree,
|
|
||||||
treeDefaultExpandAll: true,
|
|
||||||
treeLine: { showLeafIcon: false },
|
|
||||||
// 筛选的字段
|
|
||||||
treeNodeFilterProp: 'label',
|
|
||||||
// 选中后显示在输入框的值
|
|
||||||
treeNodeLabelProp: 'label',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const gridOptions: VxeGridProps = {
|
// 卡片父容器的ref
|
||||||
checkboxConfig: {
|
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||||
// 高亮
|
|
||||||
highlight: true,
|
|
||||||
// 翻页时保留选中状态
|
|
||||||
reserve: true,
|
|
||||||
},
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'id',
|
|
||||||
title: 'ID',
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// field: 'businessTitle',
|
|
||||||
// title: '业务标题',
|
|
||||||
// formatter: ({ cellValue }) => cellValue ?? '-',
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
field: 'flowName',
|
|
||||||
title: '流程名称',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'categoryName',
|
|
||||||
title: '流程分类',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'flowCode',
|
|
||||||
title: '流程编码',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'nodeName',
|
|
||||||
title: '当前任务',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'flowStatus',
|
|
||||||
title: '流程状态',
|
|
||||||
slots: {
|
|
||||||
default: ({ row }) => {
|
|
||||||
return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'createTime',
|
|
||||||
title: '创建时间',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
height: 'auto',
|
|
||||||
keepSource: true,
|
|
||||||
pagerConfig: {},
|
|
||||||
proxyConfig: {
|
|
||||||
ajax: {
|
|
||||||
query: async ({ page }, formValues = {}) => {
|
|
||||||
// 转换数据
|
|
||||||
if (isArray(formValues.createByIds)) {
|
|
||||||
formValues.createByIds = (formValues.createByIds as Array<any>).map(
|
|
||||||
(item) => item.userId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await pageByTaskFinish({
|
/**
|
||||||
pageNum: page.currentPage,
|
* @param resetFields 是否清空查询参数
|
||||||
pageSize: page.pageSize,
|
*/
|
||||||
...formValues,
|
async function reload(resetFields: boolean = false) {
|
||||||
});
|
// 需要先滚动到顶部
|
||||||
},
|
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||||
},
|
|
||||||
},
|
|
||||||
rowConfig: {
|
|
||||||
keyField: 'id',
|
|
||||||
isCurrent: true,
|
|
||||||
},
|
|
||||||
id: 'workflow-task-finish',
|
|
||||||
rowClassName: 'cursor-pointer',
|
|
||||||
};
|
|
||||||
|
|
||||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
page.value = 1;
|
||||||
formOptions,
|
currentTask.value = undefined;
|
||||||
gridOptions,
|
taskTotal.value = 0;
|
||||||
gridEvents: {
|
lastSelectId.value = '';
|
||||||
cellClick: ({ row }) => {
|
|
||||||
handleOpen(row);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [ApprovalPanelDrawer, drawerApi] = useVbenDrawer({
|
if (resetFields) {
|
||||||
connectedComponent: ApprovalPanelDrawerComp,
|
formData.value = cloneDeep(defaultFormData);
|
||||||
});
|
selectedUserList.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
function handleOpen(row: any) {
|
loading.value = true;
|
||||||
drawerApi.setData({ task: row, type: 'readonly' }).open();
|
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 - bottomOffset;
|
||||||
|
|
||||||
|
// 滚动到底部且没有加载完成
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page :auto-content-height="true">
|
<Page :auto-content-height="true">
|
||||||
<BasicTable table-title="已办理任务" />
|
<div class="flex h-full gap-2">
|
||||||
<ApprovalPanelDrawer @reload="() => tableApi.query()" />
|
<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>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.thin-scrollbar {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
@apply thin-scrollbar;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,188 +1,302 @@
|
|||||||
<script setup lang="tsx">
|
<!-- eslint-disable no-use-before-define -->
|
||||||
import type { VbenFormProps } from '@vben/common-ui';
|
<script setup lang="ts">
|
||||||
|
import type { User } from '#/api/system/user/model';
|
||||||
|
import type { TaskInfo } from '#/api/workflow/task/model';
|
||||||
|
|
||||||
import type { ComponentType } from '#/adapter/component';
|
import { computed, onMounted, ref, useTemplateRef } from 'vue';
|
||||||
import type { VxeGridProps } from '#/adapter/vxe-table';
|
|
||||||
|
|
||||||
import { markRaw, onMounted } from 'vue';
|
import { Page } from '@vben/common-ui';
|
||||||
|
import { useTabs } from '@vben/hooks';
|
||||||
|
import { addFullName, getPopupContainer } from '@vben/utils';
|
||||||
|
|
||||||
import { Page, useVbenDrawer } from '@vben/common-ui';
|
import { FilterOutlined, RedoOutlined } from '@ant-design/icons-vue';
|
||||||
import { DictEnum } from '@vben/constants';
|
import {
|
||||||
|
Empty,
|
||||||
|
Form,
|
||||||
|
FormItem,
|
||||||
|
Input,
|
||||||
|
InputSearch,
|
||||||
|
Popover,
|
||||||
|
Spin,
|
||||||
|
Tooltip,
|
||||||
|
TreeSelect,
|
||||||
|
} from 'ant-design-vue';
|
||||||
|
import { cloneDeep, debounce } from 'lodash-es';
|
||||||
|
|
||||||
import { isArray } from 'lodash-es';
|
|
||||||
|
|
||||||
import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
|
||||||
import { categoryTree } from '#/api/workflow/category';
|
import { categoryTree } from '#/api/workflow/category';
|
||||||
import { pageByTaskWait } from '#/api/workflow/task';
|
import { pageByTaskWait } from '#/api/workflow/task';
|
||||||
import { renderDict } from '#/utils/render';
|
|
||||||
|
|
||||||
import { ApprovalPanelDrawerComp, CopyComponent } from '../components';
|
import { ApprovalCard, ApprovalPanel, CopyComponent } from '../components';
|
||||||
import { DefaultSlot } from '../components/helper';
|
import { bottomOffset } from './constant';
|
||||||
|
|
||||||
const formOptions: VbenFormProps<ComponentType> = {
|
const emptyImage = Empty.PRESENTED_IMAGE_SIMPLE;
|
||||||
commonConfig: {
|
|
||||||
labelWidth: 80,
|
const taskList = ref<(TaskInfo & { active: boolean })[]>([]);
|
||||||
componentProps: {
|
const taskTotal = ref(0);
|
||||||
allowClear: true,
|
const page = ref(1);
|
||||||
},
|
const loading = ref(false);
|
||||||
},
|
|
||||||
schema: [
|
const defaultFormData = {
|
||||||
{
|
flowName: '', // 流程定义名称
|
||||||
fieldName: 'category',
|
nodeName: '', // 任务名称
|
||||||
component: 'TreeSelect',
|
flowCode: '', // 流程定义编码
|
||||||
label: '流程分类',
|
createByIds: [] as string[], // 创建人
|
||||||
},
|
category: null as null | number, // 流程分类
|
||||||
{
|
|
||||||
fieldName: 'flowCode',
|
|
||||||
component: 'Input',
|
|
||||||
label: '流程编码',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'createByIds',
|
|
||||||
defaultValue: [],
|
|
||||||
component: markRaw(DefaultSlot),
|
|
||||||
label: '申请人',
|
|
||||||
renderComponentContent: (model) => ({
|
|
||||||
default: () => (
|
|
||||||
<CopyComponent avatarSize={30} v-model:userList={model.createByIds} />
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
|
|
||||||
// 日期选择格式化
|
|
||||||
fieldMappingTime: [
|
|
||||||
[
|
|
||||||
'createTime',
|
|
||||||
['params[beginTime]', 'params[endTime]'],
|
|
||||||
['YYYY-MM-DD 00:00:00', 'YYYY-MM-DD 23:59:59'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
const formData = ref(cloneDeep(defaultFormData));
|
||||||
|
|
||||||
onMounted(async () => {
|
/**
|
||||||
const tree = await categoryTree();
|
* 是否已经加载全部数据 即 taskList.length === taskTotal
|
||||||
tableApi.formApi.updateSchema([
|
*/
|
||||||
{
|
const isLoadComplete = computed(
|
||||||
fieldName: 'category',
|
() => taskList.value.length === taskTotal.value,
|
||||||
componentProps: {
|
);
|
||||||
fieldNames: { label: 'label', value: 'id' },
|
|
||||||
showSearch: true,
|
|
||||||
treeData: tree,
|
|
||||||
treeDefaultExpandAll: true,
|
|
||||||
treeLine: { showLeafIcon: false },
|
|
||||||
// 筛选的字段
|
|
||||||
treeNodeFilterProp: 'label',
|
|
||||||
// 选中后显示在输入框的值
|
|
||||||
treeNodeLabelProp: 'label',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const gridOptions: VxeGridProps = {
|
// 卡片父容器的ref
|
||||||
checkboxConfig: {
|
const cardContainerRef = useTemplateRef('cardContainerRef');
|
||||||
// 高亮
|
|
||||||
highlight: true,
|
|
||||||
// 翻页时保留选中状态
|
|
||||||
reserve: true,
|
|
||||||
},
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
field: 'id',
|
|
||||||
title: 'ID',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'businessTitle',
|
|
||||||
title: '业务标题',
|
|
||||||
formatter: ({ cellValue }) => cellValue ?? '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'flowName',
|
|
||||||
title: '流程名称',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'categoryName',
|
|
||||||
title: '流程分类',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'flowCode',
|
|
||||||
title: '流程编码',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'nodeName',
|
|
||||||
title: '当前任务',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'processedByName',
|
|
||||||
title: '办理人',
|
|
||||||
formatter: ({ cellValue }) => cellValue?.split?.(',') ?? cellValue,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'flowStatus',
|
|
||||||
title: '流程状态',
|
|
||||||
slots: {
|
|
||||||
default: ({ row }) => {
|
|
||||||
return renderDict(row.flowStatus, DictEnum.WF_BUSINESS_STATUS);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'createTime',
|
|
||||||
title: '创建时间',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
height: 'auto',
|
|
||||||
keepSource: true,
|
|
||||||
pagerConfig: {},
|
|
||||||
proxyConfig: {
|
|
||||||
ajax: {
|
|
||||||
query: async ({ page }, formValues = {}) => {
|
|
||||||
// 转换数据
|
|
||||||
if (isArray(formValues.createByIds)) {
|
|
||||||
formValues.createByIds = (formValues.createByIds as Array<any>).map(
|
|
||||||
(item) => item.userId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await pageByTaskWait({
|
/**
|
||||||
pageNum: page.currentPage,
|
* @param resetFields 是否清空查询参数
|
||||||
pageSize: page.pageSize,
|
*/
|
||||||
...formValues,
|
async function reload(resetFields: boolean = false) {
|
||||||
});
|
// 需要先滚动到顶部
|
||||||
},
|
cardContainerRef.value?.scroll({ top: 0, behavior: 'auto' });
|
||||||
},
|
|
||||||
},
|
|
||||||
rowConfig: {
|
|
||||||
keyField: 'id',
|
|
||||||
isCurrent: true,
|
|
||||||
},
|
|
||||||
id: 'workflow-task-myself',
|
|
||||||
rowClassName: 'cursor-pointer',
|
|
||||||
};
|
|
||||||
|
|
||||||
const [BasicTable, tableApi] = useVbenVxeGrid({
|
page.value = 1;
|
||||||
formOptions,
|
currentTask.value = undefined;
|
||||||
gridOptions,
|
taskTotal.value = 0;
|
||||||
gridEvents: {
|
lastSelectId.value = '';
|
||||||
cellClick: ({ row }) => {
|
|
||||||
handleOpen(row);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [ApprovalPanelDrawer, drawerApi] = useVbenDrawer({
|
if (resetFields) {
|
||||||
connectedComponent: ApprovalPanelDrawerComp,
|
formData.value = cloneDeep(defaultFormData);
|
||||||
});
|
selectedUserList.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
function handleOpen(row: any) {
|
loading.value = true;
|
||||||
drawerApi.setData({ task: row, type: 'approve' }).open();
|
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;
|
||||||
|
}
|
||||||
|
// e.target.scrollTop 是元素顶部到当前可视区域顶部的距离,即已滚动的高度。
|
||||||
|
// e.target.clientHeight 是元素的可视高度。
|
||||||
|
// e.target.scrollHeight 是元素的总高度。
|
||||||
|
const { scrollTop, clientHeight, scrollHeight } = e.target as HTMLElement;
|
||||||
|
// 判断是否滚动到底部
|
||||||
|
const isBottom = scrollTop + clientHeight >= scrollHeight - bottomOffset;
|
||||||
|
|
||||||
|
// 滚动到底部且没有加载完成
|
||||||
|
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 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();
|
||||||
|
|
||||||
|
// 由于失去焦点浮层会消失 使用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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page :auto-content-height="true">
|
<Page :auto-content-height="true">
|
||||||
<BasicTable table-title="我的待办" />
|
<div class="flex h-full gap-2">
|
||||||
<ApprovalPanelDrawer @reload="() => tableApi.query()" />
|
<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="approve" @reload="refreshTab" />
|
||||||
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.thin-scrollbar {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-card-body) {
|
||||||
|
@apply thin-scrollbar;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -27,8 +27,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
|
"build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 turbo build",
|
||||||
"build:analyze": "turbo build:analyze",
|
"build:analyze": "turbo build:analyze",
|
||||||
"build:antd": "pnpm run build --filter=@vben/web-antd",
|
"build:antd": "pnpm run build --filter=@vben/web-antd build:prod",
|
||||||
"build:antd:test": "pnpm run build --filter=@vben/web-antd -- --mode test",
|
"build:antd:test": "pnpm run build --filter=@vben/web-antd build:test",
|
||||||
"build:docker": "./scripts/deploy/build-local-docker-image.sh",
|
"build:docker": "./scripts/deploy/build-local-docker-image.sh",
|
||||||
"build:docs": "pnpm run build --filter=@vben/docs",
|
"build:docs": "pnpm run build --filter=@vben/docs",
|
||||||
"build:play": "pnpm run build --filter=@vben/playground",
|
"build:play": "pnpm run build --filter=@vben/playground",
|
||||||
|
|||||||
@@ -390,10 +390,10 @@ $namespace: vben;
|
|||||||
var(--menu-item-margin-x);
|
var(--menu-item-margin-x);
|
||||||
font-size: var(--menu-font-size);
|
font-size: var(--menu-font-size);
|
||||||
color: var(--menu-item-color);
|
color: var(--menu-item-color);
|
||||||
text-decoration: none;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
list-style: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
background: var(--menu-item-background-color);
|
background: var(--menu-item-background-color);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: var(--menu-item-radius);
|
border-radius: var(--menu-item-radius);
|
||||||
@@ -495,7 +495,7 @@ $namespace: vben;
|
|||||||
&.is-rounded {
|
&.is-rounded {
|
||||||
--menu-item-margin-x: 8px;
|
--menu-item-margin-x: 8px;
|
||||||
--menu-item-collapse-margin-x: 6px;
|
--menu-item-collapse-margin-x: 6px;
|
||||||
--menu-item-radius: 8px;
|
--menu-item-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-horizontal:not(.is-rounded) {
|
&.is-horizontal:not(.is-rounded) {
|
||||||
@@ -717,8 +717,8 @@ $namespace: vben;
|
|||||||
width: var(--menu-item-icon-size);
|
width: var(--menu-item-icon-size);
|
||||||
height: var(--menu-item-icon-size);
|
height: var(--menu-item-icon-size);
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,15 +12,13 @@ function isAccessible(
|
|||||||
el: Element,
|
el: Element,
|
||||||
binding: DirectiveBinding<string | string[]>,
|
binding: DirectiveBinding<string | string[]>,
|
||||||
) {
|
) {
|
||||||
const { accessMode, hasAccessByCodes, hasAccessByRoles } = useAccess();
|
const { hasAccessByCodes, hasAccessByRoles } = useAccess();
|
||||||
|
|
||||||
const value = binding.value;
|
const value = binding.value;
|
||||||
|
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
const authMethod =
|
const authMethod =
|
||||||
accessMode.value === 'frontend' && binding.arg === 'role'
|
binding.arg === 'role' ? hasAccessByRoles : hasAccessByCodes;
|
||||||
? hasAccessByRoles
|
|
||||||
: hasAccessByCodes;
|
|
||||||
|
|
||||||
const values = Array.isArray(value) ? value : [value];
|
const values = Array.isArray(value) ? value : [value];
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ function useAccess() {
|
|||||||
*/
|
*/
|
||||||
function hasAccessByRoles(roles: string[]) {
|
function hasAccessByRoles(roles: string[]) {
|
||||||
const userRoleSet = new Set(userStore.userRoles);
|
const userRoleSet = new Set(userStore.userRoles);
|
||||||
|
// 超管的角色
|
||||||
|
if (userRoleSet.has('superadmin')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const intersection = roles.filter((item) => userRoleSet.has(item));
|
const intersection = roles.filter((item) => userRoleSet.has(item));
|
||||||
return intersection.length > 0;
|
return intersection.length > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,3 +141,8 @@ TODO: 最后一条数据hover/check仍会显示边框
|
|||||||
border-radius: var(--vxe-ui-table-border-radius)
|
border-radius: var(--vxe-ui-table-border-radius)
|
||||||
var(--vxe-ui-table-border-radius) 0 0;
|
var(--vxe-ui-table-border-radius) 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* modal/drawer里使用列配置 重置列弹窗被遮挡 */
|
||||||
|
.vxe-dynamics--modal > .vxe-modal--wrapper {
|
||||||
|
z-index: calc(var(--popup-z-index) + 1) !important;
|
||||||
|
}
|
||||||
|
|||||||
10
turbo.json
10
turbo.json
@@ -23,6 +23,16 @@
|
|||||||
".vitepress/dist/**"
|
".vitepress/dist/**"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"@vben/web-antd#build:prod": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"outputs": ["dist/**"]
|
||||||
|
},
|
||||||
|
"@vben/web-antd#build:test": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"outputs": ["dist/**"]
|
||||||
|
},
|
||||||
|
|
||||||
"preview": {
|
"preview": {
|
||||||
"dependsOn": ["^build"],
|
"dependsOn": ["^build"],
|
||||||
"outputs": ["dist/**"]
|
"outputs": ["dist/**"]
|
||||||
|
|||||||
Reference in New Issue
Block a user