Commit 8d4896cf authored by 刘斌's avatar 刘斌

fix: 基础版本更新

parent a336722d
......@@ -146,3 +146,13 @@ export function tenantSyncDict(tenantId?: string) {
successMessageMode: 'message',
});
}
/**
* 同步租户配置
* @returns void
*/
export function tenantSyncConfig() {
return requestClient.get('/system/tenant/syncTenantConfig', {
successMessageMode: 'message',
});
}
import type { TaskInfo } from '../task/model';
import type { FlowInfoResponse } from './model';
import type { FlowInfoResponse, FlowInstanceVariableResp } from './model';
import type { ID, IDS, PageQuery, PageResult } from '#/api/baseModel';
......@@ -106,8 +106,8 @@ export function flowInfo(businessId: string) {
* @returns Map<string,any>
*/
export function instanceVariable(instanceId: string) {
return requestClient.get<Record<string, any>>(
`/workflow/instance/variable/${instanceId}`,
return requestClient.get<FlowInstanceVariableResp>(
`/workflow/instance/instanceVariable/${instanceId}`,
);
}
......@@ -122,3 +122,21 @@ export function workflowInstanceInvalid(data: {
successMessageMode: 'message',
});
}
/**
* 修改流程参数
* @param data 参数
* @param data.instanceId 实例ID
* @param data.key 参数key
* @param data.value 值
* @returns void
*/
export function updateFlowVariable(data: {
instanceId: string;
key: string;
value: any;
}) {
return requestClient.put('/workflow/instance/updateVariable', data, {
successMessageMode: 'message',
});
}
export {};
export interface Flow {
id: string;
createTime: string;
......@@ -39,3 +41,14 @@ export interface FlowInfoResponse {
instanceId: string;
list: Flow[];
}
export interface FlowInstanceVariableResp {
/**
* json字符串 流程变量
*/
variable: string;
variableList: {
key: string;
value: any;
}[];
}
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',
// 隐藏下面的 按xxx获取帮助
help_accessibility: false,
// https://blog.csdn.net/qq_46380656/article/details/122171418
// 避免图片地址和链接地址转换成相对路径
relative_urls: false,
remove_script_host: false,
convert_urls: false,
...options,
/**
* 覆盖默认的base64行为
......
<script setup lang="ts">
import type { CheckboxChangeEvent } from 'ant-design-vue/es/checkbox/interface';
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 { uniq } from 'lodash-es';
import { Button, Checkbox, Tree } from 'ant-design-vue';
/** 需要禁止透传 */
defineOptions({ inheritAttrs: false });
const props = defineProps({
checkStrictly: {
default: true,
type: Boolean,
},
expandAllOnInit: {
default: false,
type: Boolean,
},
fieldNames: {
default: () => ({ key: 'id', title: 'name' }),
type: Object as PropType<{ key: string; title: string }>,
},
/** 点击节点关联/独立时 清空已勾选的节点 */
resetOnStrictlyChange: {
default: true,
type: Boolean,
},
treeData: {
default: () => [],
type: Array as PropType<DataNode[]>,
},
const props = withDefaults(defineProps<Props>(), {
expandAllOnInit: false,
fieldNames: () => ({ key: 'id', title: 'label' }),
resetOnStrictlyChange: true,
treeData: () => [],
});
const emit = defineEmits<{ checkStrictlyChange: [boolean] }>();
const expandStatus = ref(false);
const selectAllStatus = ref(false);
interface Props {
/**
* 是否展开所有节点 mount
*/
expandAllOnInit?: boolean;
/**
* 自定义字段
*/
fieldNames?: { key: string; title: string };
/**
* 点击节点关联/独立时 清空已勾选的节点
*/
resetOnStrictlyChange?: boolean;
/**
* 树结构数据
*/
treeData?: DataNode[];
}
/**
* 后台的这个字段跟antd/ele是反的
* 组件库这个字段代表不关联
* 后台这个代表关联
* 展开的状态
*/
const innerCheckedStrictly = computed(() => {
return !props.checkStrictly;
});
const expandStatus = ref(false);
/**
* 全选状态
*/
const selectAllStatus = ref(false);
const associationText = computed(() => {
return props.checkStrictly ? '父子节点关联' : '父子节点独立';
return checkStrictly.value ? '父子节点关联' : '父子节点独立';
});
/**
* 这个只用于界面显示
* 关联情况下 只会有最末尾的节点被选中
*/
const checkedKeys = defineModel('value', {
const checkedKeys = defineModel<(number | string)[]>('value', {
default: () => [],
type: Array as PropType<(number | string)[]>,
});
// 所有节点的ID
const allKeys = computed(() => {
const idField = props.fieldNames.key;
return treeToList(props.treeData).map((item: any) => item[idField]);
});
/** 已经选择的所有节点 包括子/父节点 用于提交 */
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 },
);
const checkStrictly = defineModel<boolean>('checkStrictly', {
default: () => true,
});
const computedCheckedKeys = computed<any>({
get() {
/**
* uniq 解决上面的id重复问题
* 严格模式(节点不关联) 需要返回{checked: string[] | number[], halfChecked: string[]}
* @see https://www.antdv.com/components/tree-cn#tree-props
*/
checkedRealKeys.value = uniq([...parentIds, ...checkedKeys.value]);
stop();
if (!checkStrictly.value) {
return {
checked: [...checkedKeys.value],
halfChecked: [],
};
}
if (!props.checkStrictly && checkedKeys.value.length > 0) {
/** 节点独立 这里是全部的节点 */
checkedRealKeys.value = checkedKeys.value;
stop();
return checkedKeys.value;
},
set(v) {
if (!checkStrictly.value) {
checkedKeys.value = [...v.checked, ...v.halfChecked];
return;
}
checkedKeys.value = v;
},
});
/**
*
* @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];
}
}
// 所有节点的ID
const allKeys = computed(() => {
const idField = props.fieldNames.key;
return treeToList(props.treeData).map((item: any) => item[idField]);
});
function handleExpandChange(e: CheckboxChangeEvent) {
function handleCheckedAllChange(e: CheckboxChangeEvent) {
// 这个用于展示
checkedKeys.value = e.target.checked ? allKeys.value : [];
// 这个用于提交
checkedRealKeys.value = e.target.checked ? allKeys.value : [];
}
const expandedKeys = ref<string[]>([]);
function handleExpandOrCollapseAll(e: CheckboxChangeEvent) {
const expand = e.target.checked;
expandedKeys.value = expand ? allKeys.value : [];
function handleExpandOrCollapseAll() {
expandStatus.value = !expandStatus.value;
expandedKeys.value = expandStatus.value ? allKeys.value : [];
}
function handleCheckStrictlyChange(e: CheckboxChangeEvent) {
emit('checkStrictlyChange', e.target.checked);
function handleCheckStrictlyChange() {
if (props.resetOnStrictlyChange) {
checkedKeys.value = [];
checkedRealKeys.value = [];
}
}
/**
* 暴露方法来获取用于提交的全部节点
* uniq去重(保险方案)
*/
defineExpose({
getCheckedKeys: () => uniq(checkedRealKeys.value),
});
onMounted(async () => {
if (props.expandAllOnInit) {
await nextTick();
expandedKeys.value = allKeys.value;
}
});
const slots = useSlots() as SetupContext['slots'];
</script>
<template>
<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>
<div class="opacity-75">
<span>节点状态: </span>
<span :class="[props.checkStrictly ? 'text-primary' : 'text-red-500']">
<span :class="[checkStrictly ? 'text-primary' : 'text-red-500']">
{{ associationText }}
</span>
</div>
<div>
已选中
<span class="text-primary mx-1 font-semibold">
{{ checkedRealKeys.length }}
</span>
个节点
</div>
</div>
<div
class="flex flex-wrap items-center justify-between border-b-[1px] py-2"
>
<Button size="small" @click="handleExpandOrCollapseAll">
展开/折叠全部
</Button>
<Checkbox
v-model:checked="expandStatus"
@change="handleExpandOrCollapseAll"
v-model:checked="selectAllStatus"
@change="handleCheckedAllChange"
>
展开/折叠全部
</Checkbox>
<Checkbox v-model:checked="selectAllStatus" @change="handleExpandChange">
全选/取消全选
</Checkbox>
<Checkbox :checked="checkStrictly" @change="handleCheckStrictlyChange">
<Checkbox
v-model:checked="checkStrictly"
@change="handleCheckStrictlyChange"
>
父子节点关联
</Checkbox>
</div>
<div class="py-2">
<Tree
v-if="treeData.length > 0"
v-model:check-strictly="innerCheckedStrictly"
v-model:checked-keys="checkedKeys"
:check-strictly="!checkStrictly"
v-model:checked-keys="computedCheckedKeys"
v-model:expanded-keys="expandedKeys"
:checkable="true"
:field-names="fieldNames"
:selectable="false"
:tree-data="treeData"
@check="handleChecked"
>
<template
v-for="slotName in Object.keys(slots)"
v-for="slotName in Object.keys($slots)"
:key="slotName"
#[slotName]="data"
>
......@@ -219,3 +174,20 @@ const slots = useSlots() as SetupContext['slots'];
</div>
</div>
</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>
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { markRaw } from 'vue';
import { useAccess } from '@vben/access';
import { DictEnum } from '@vben/constants';
import { getVxePopupContainer } from '@vben/utils';
......@@ -8,6 +10,8 @@ import { getVxePopupContainer } from '@vben/utils';
import { Tag } from 'ant-design-vue';
import { changeRoleStatus } from '#/api/auth/role';
import { DefaultSlot } from '#/components/global/slot';
import { TreeSelectPanel } from '#/components/tree';
import { getDictOptions } from '#/utils/dict';
/**
......@@ -134,15 +138,6 @@ export const dataScopeModalSchemas: VbenFormSchema[] = [
fieldName: 'id',
label: '角色ID',
},
{
component: 'Radio',
dependencies: {
show: () => false,
triggerFields: [''],
},
fieldName: 'deptCheckStrictly',
label: 'deptCheckStrictly',
},
{
component: 'Input',
componentProps: {
......@@ -171,12 +166,39 @@ export const dataScopeModalSchemas: VbenFormSchema[] = [
label: '权限范围',
},
{
component: 'TreeSelect',
component: 'Radio',
dependencies: {
show: () => false,
triggerFields: [''],
},
fieldName: 'deptCheckStrictly',
label: 'deptCheckStrictly',
},
{
// 这种的场景基本上是一个组件需要绑定两个或以上的场景
component: markRaw(DefaultSlot),
defaultValue: [],
componentProps: {
rootDivAttrs: {
class: 'w-full',
},
},
dependencies: {
show: (values) => values.dataScope === 5,
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',
help: '更改后立即生效',
label: '部门权限',
......
......@@ -2,14 +2,14 @@
import type { SysDeptApi } from '#/api/auth/dept';
import type { SysRoleApi } from '#/api/auth/role';
import { ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { findGroupParentIds } from '@vben/utils';
import { uniq } from 'lodash-es';
import { useVbenForm } from '#/adapter/form';
import { treeList } from '#/api/auth/dept';
import { authDataScope, getRoleDeptIds } from '#/api/auth/role';
import { TreeSelectPanel } from '#/components/tree';
import { defaultFormValueGetter, useBeforeCloseDiff } from '#/utils/popup';
import { dataScopeModalSchemas } from './data';
......@@ -29,30 +29,36 @@ const [BasicForm, formApi] = useVbenForm({
showDefaultActions: false,
});
const deptTree = ref<DeptTreeOption[]>([]);
// const deptTree = ref<DeptTreeOption[]>([]);
/**
* 保存部门数据 用于获取祖先节点
*/
let treeData: DeptTreeOption[] = [];
async function setupDeptTree(id: number) {
const [roleDeptIds, deptTreeList] = await Promise.all([
getRoleDeptIds(id),
treeList(),
]);
// const resp = await getRoleDeptIds(id);
formApi.setFieldValue('deptIds', roleDeptIds);
/**
* 设置部门树数据
*/
formApi.updateSchema([
{ fieldName: 'deptIds', componentProps: { treeData: deptTreeList } },
]);
/**
* 设置选中 必须先传递treeData
* Note: Tree missing follow keys: '1981565541727186945'
*/
await formApi.setFieldValue('deptIds', roleDeptIds);
// 设置菜单信息
deptTree.value = deptTreeList;
}
async function customFormValueGetter() {
const v = await defaultFormValueGetter(formApi)();
// 获取勾选信息
const menuIds = deptSelectRef.value?.[0]?.getCheckedKeys() ?? [];
const mixStr = v + menuIds.join(',');
return mixStr;
treeData = deptTreeList;
}
const { onBeforeClose, markInitialized, resetInitialized } = useBeforeCloseDiff(
{
initializedGetter: customFormValueGetter,
currentGetter: customFormValueGetter,
initializedGetter: defaultFormValueGetter(formApi),
currentGetter: defaultFormValueGetter(formApi),
},
);
......@@ -63,6 +69,7 @@ const [BasicModal, modalApi] = useVbenModal({
onConfirm: handleConfirm,
onOpenChange: async (isOpen) => {
if (!isOpen) {
treeData = [];
return null;
}
modalApi.modalLoading(true);
......@@ -78,11 +85,6 @@ const [BasicModal, modalApi] = useVbenModal({
},
});
/**
* 这里拿到的是一个数组ref
*/
const deptSelectRef = ref();
async function handleConfirm() {
try {
modalApi.lock(true);
......@@ -93,9 +95,16 @@ async function handleConfirm() {
// formApi.getValues拿到的是一个readonly对象,不能直接修改,需要cloneDeep
const data = await formApi.getValues();
// 不为自定义权限的话 删除部门id
let deptIds: number[] = [];
if (data.dataScope === 5) {
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;
} else {
data.deptIds = [];
......@@ -115,29 +124,10 @@ async function handleClosed() {
await formApi.resetForm();
resetInitialized();
}
/**
* 通过回调更新 无法通过v-model
* @param value 菜单选择是否严格模式
*/
function handleCheckStrictlyChange(value: boolean) {
formApi.setFieldValue('deptCheckStrictly', value);
}
</script>
<template>
<BasicModal class="min-h-[600px] w-[550px]" title="分配权限">
<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>
<BasicForm />
</BasicModal>
</template>
......@@ -137,7 +137,7 @@ function onDelete(row: DictTypeApi.DictType) {
async function handleRefreshCache() {
await refreshDictTypeCache();
await tableApi.query();
await gridApi.query();
}
</script>
<template>
......@@ -145,12 +145,12 @@ async function handleRefreshCache() {
<DictTypeModal @success="onRefresh" />
<Grid table-title="字典类型列表">
<template #toolbar-tools>
<a-button
<Button
v-access:code="['system:dict:edit']"
@click="handleRefreshCache"
>
刷新缓存
</a-button>
</Button>
<Button
type="primary"
v-access:code="['system:dict:add']"
......
......@@ -139,6 +139,18 @@ function handleSyncTenantDict() {
},
});
}
function handleSyncTenantConfig() {
Modal.confirm({
title: '提示',
iconType: 'warning',
content: '确认同步租户参数配置?',
onOk: async () => {
await syncTenantConfig();
await tableApi.query();
},
});
}
</script>
<template>
<Page auto-content-height>
......@@ -152,6 +164,12 @@ function handleSyncTenantDict() {
>
同步租户字典
</Button>
<Button
v-access:code="['system:tenant:edit']"
@click="handleSyncTenantConfig"
>
同步租户参数配置
</Button>
<Button
v-access:code="['system:tenant:export']"
@click="handleDownloadExcel"
......
<script setup lang="ts">
import type { ApprovalType } from '../type';
import type { User } from '#/api/core/user';
import type { TaskInfo } from '#/api/workflow/task/model';
import { computed, h } from 'vue';
import { useRouter } from 'vue-router';
import { useVbenModal } from '@vben/common-ui';
import { cn, getPopupContainer } from '@vben/utils';
import {
ArrowLeftOutlined,
CheckOutlined,
EditOutlined,
ExclamationCircleOutlined,
MenuOutlined,
RollbackOutlined,
UsergroupAddOutlined,
UsergroupDeleteOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
import { Button, Dropdown, Menu, MenuItem, Modal, Space } from 'ant-design-vue';
import {
cancelProcessApply,
deleteByInstanceIds,
} from '#/api/workflow/instance';
import {
taskOperation,
terminationTask,
updateAssignee,
} from '#/api/workflow/task';
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '..';
import { approveWithReasonModal } from '../helper';
import userSelectModal from '../user-select-modal.vue';
interface Props {
/**
* 行数据的taskInfo?
*/
task?: TaskInfo;
/**
* 审批类型 根据不同类型显示按钮
*/
type: ApprovalType;
/**
* 为审批类型时候 显示的按钮(按钮权限)
*/
buttonPermissions: Record<string, boolean>;
}
const props = defineProps<Props>();
const emit = defineEmits<{
reload: [];
}>();
// 是否显示 `其他` 按钮
const showButtonOther = computed(() => {
const moreCollections = new Set(['addSign', 'subSign', 'transfer', 'trust']);
return Object.keys(props.buttonPermissions).some(
(key) => moreCollections.has(key) && props.buttonPermissions[key],
);
});
// 进行中 可以撤销
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 { buttonPermissions } = props;
// 是否具有抄送权限
const copyPermission = buttonPermissions?.copy ?? false;
// 是否具有选人权限
const assignPermission = buttonPermissions?.pop ?? false;
approvalModalApi.setData({
taskId: props.task?.id,
copyPermission,
assignPermission,
});
approvalModalApi.open();
}
/**
* 委托
*/
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');
},
});
}
/**
* 是否显示 加签/减签操作
*/
const showMultiActions = computed(() => {
if (!props.task) {
return false;
}
if (Number(props.task.nodeRatio) > 0) {
return true;
}
return false;
});
</script>
<template>
<div
:class="
cn(
'absolute bottom-0 left-0',
'border-t-solid border-t-[1px]',
'bg-background w-full p-3',
)
"
>
<div class="flex justify-end">
<Space v-if="type === 'myself'">
<Button
v-if="revocable"
danger
ghost
type="primary"
:icon="h(RollbackOutlined)"
@click="handleCancel"
>
撤销申请
</Button>
<Button
type="primary"
ghost
v-if="editableAndRemoveable"
:icon="h(EditOutlined)"
@click="handleEdit"
>
重新编辑
</Button>
<Button
v-if="editableAndRemoveable"
danger
ghost
type="primary"
:icon="h(EditOutlined)"
@click="handleRemove"
>
删除
</Button>
</Space>
<Space v-if="type === 'approve'">
<Button
type="primary"
ghost
:icon="h(CheckOutlined)"
@click="handleApproval"
>
通过
</Button>
<Button
v-if="buttonPermissions?.termination"
danger
ghost
type="primary"
:icon="h(ExclamationCircleOutlined)"
@click="handleTermination"
>
终止
</Button>
<Button
v-if="buttonPermissions?.back"
danger
ghost
type="primary"
:icon="h(ArrowLeftOutlined)"
@click="handleRejection"
>
驳回
</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>
<Button v-if="showButtonOther" :icon="h(MenuOutlined)"> 其他 </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'">
<Button @click="handleFlowInterfere"> 流程干预 </Button>
<Button @click="() => updateAssigneeModalApi.open()">
修改办理人
</Button>
<FlowInterfereModal @complete="$emit('reload')" />
<UpdateAssigneeModal mode="single" @finish="handleUpdateAssignee" />
</Space>
</div>
</div>
</template>
export { default as FlowActions } from './flow-actions.vue';
......@@ -43,6 +43,8 @@ const [BasicModal, modalApi] = useVbenModal({
if (!isOpen) {
return null;
}
modalApi.modalLoading(true);
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',
defaultValue: [],
label: '抄送人',
// 默认不显示
dependencies: {
if: false,
triggerFields: [''],
},
},
],
showDefaultActions: false,
......
......@@ -21,8 +21,6 @@ defineOptions({
defineProps<{
currentFlowInfo: FlowInfoResponse;
iframeHeight: number;
iframeLoaded: boolean;
task: TaskInfo;
}>();
</script>
......
<!-- 该文件需要重构 但我没空 -->
<!--
TODO: 优化项
会先加载流程信息 再加载业务表单信息
-->
<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 { TaskInfo } from '#/api/workflow/task/model';
import { computed, h, onUnmounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
import { computed, ref, watch } from 'vue';
import { Fallback, useVbenModal, VbenAvatar } from '@vben/common-ui';
import { Fallback, VbenAvatar } from '@vben/common-ui';
import { DictEnum } from '@vben/constants';
import { cn, getPopupContainer } from '@vben/utils';
import { cn } from '@vben/utils';
import {
ArrowLeftOutlined,
CheckOutlined,
CopyOutlined,
EditOutlined,
ExclamationCircleOutlined,
MenuOutlined,
RollbackOutlined,
UsergroupAddOutlined,
UsergroupDeleteOutlined,
UserOutlined,
} from '@ant-design/icons-vue';
import { useClipboard, useEventListener } from '@vueuse/core';
import {
Button,
Card,
Divider,
Dropdown,
Menu,
MenuItem,
message,
Modal,
Space,
TabPane,
Tabs,
} from 'ant-design-vue';
import { isObject } from 'lodash-es';
import { CopyOutlined } from '@ant-design/icons-vue';
import { useClipboard } from '@vueuse/core';
import { Button, Card, Divider, message, TabPane, Tabs } from 'ant-design-vue';
import {
cancelProcessApply,
deleteByInstanceIds,
flowInfo,
} from '#/api/workflow/instance';
import {
getTaskByTaskId,
taskOperation,
terminationTask,
updateAssignee,
} from '#/api/workflow/task';
import { flowInfo } from '#/api/workflow/instance';
import { getTaskByTaskId } from '#/api/workflow/task';
import { renderDict } from '#/utils/render';
import { approvalModal, approvalRejectionModal, flowInterfereModal } from '.';
import { FlowActions } from './actions';
import ApprovalDetails from './approval-details.vue';
import FlowPreview from './flow-preview.vue';
import { approveWithReasonModal } from './helper';
import userSelectModal from './user-select-modal.vue';
defineOptions({
name: 'ApprovalPanel',
inheritAttrs: false,
});
const props = defineProps<{ task?: TaskInfo; type: ApprovalType }>();
const props = defineProps<Props>();
/**
* 下面按钮点击后会触发的事件
*/
const emit = defineEmits<{ reload: [] }>();
defineEmits<{ reload: [] }>();
const currentTask = ref<TaskInfo>();
/**
* 是否显示 加签/减签操作
interface Props {
/**
* 行数据(list)的info
*/
const showMultiActions = computed(() => {
if (!currentTask.value) {
return false;
}
if (Number(currentTask.value.nodeRatio) > 0) {
return true;
}
return false;
});
task?: TaskInfo;
/**
* 审批类型
*/
type: ApprovalType;
}
/**
* 目前的作用只为了获取按钮权限 因为list接口(行数据)获取为空
*/
const onlyForBtnPermissionTask = ref<TaskInfo>();
/**
* 按钮权限
*/
const buttonPermissions = computed(() => {
const record: Record<string, boolean> = {};
if (!currentTask.value) {
if (!onlyForBtnPermissionTask.value) {
return record;
}
currentTask.value.buttonList.forEach((item) => {
onlyForBtnPermissionTask.value.buttonList.forEach((item) => {
record[item.code] = item.show;
});
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(() => {
if (props.type === 'readonly') {
return false;
......@@ -132,36 +86,34 @@ const currentFlowInfo = ref<FlowInfoResponse>();
* card的loading状态
*/
const loading = ref(false);
const iframeLoaded = ref(false);
const iframeHeight = ref(300);
useEventListener('message', (event) => {
const data = event.data as { [key: string]: any; type: string };
if (!isObject(data)) return;
/**
* iframe通信 加载完毕后才显示表单 解决卡顿问题
*/
if (data.type === 'mounted') {
iframeLoaded.value = true;
async function handleLoadInfo(task: TaskInfo | undefined) {
if (!task) {
return null;
}
try {
loading.value = true;
/**
* 高度与表单高度保持一致
* 不为审批不需要调用`getTaskByTaskId`接口
*/
if (data.type === 'height') {
const height = data.height;
iframeHeight.value = height;
if (props.type !== 'approve') {
const flowResp = await flowInfo(task.businessId);
currentFlowInfo.value = flowResp;
return;
}
});
async function handleLoadInfo(task: TaskInfo | undefined) {
try {
if (!task) return null;
loading.value = true;
iframeLoaded.value = false;
const resp = await flowInfo(task.businessId);
currentFlowInfo.value = resp;
/**
* getTaskByTaskId主要为了获取按钮权限 目前没有其他功能
* 行数据(即props.task)获取的是没有按钮权限的
*/
const [flowResp, taskResp] = await Promise.all([
flowInfo(task.businessId),
getTaskByTaskId(task.id),
]);
const taskResp = await getTaskByTaskId(props.task!.id);
currentTask.value = taskResp;
currentFlowInfo.value = flowResp;
onlyForBtnPermissionTask.value = taskResp;
} catch (error) {
console.error(error);
} finally {
......@@ -171,217 +123,6 @@ async function handleLoadInfo(task: TaskInfo | undefined) {
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来保证兼容性
......@@ -408,6 +149,7 @@ async function handleCopy(text: string) {
<CopyOutlined class="cursor-pointer" @click="handleCopy(task.id)" />
</div>
</template>
<template #extra>
<Button size="small" @click="() => handleLoadInfo(task)">
<div class="flex items-center justify-center">
......@@ -415,6 +157,7 @@ async function handleCopy(text: string) {
</div>
</Button>
</template>
<div class="flex flex-col gap-5 p-4">
<div class="flex flex-col gap-3">
<div class="flex items-center gap-2">
......@@ -427,19 +170,24 @@ async function handleCopy(text: string) {
/>
</div>
</div>
<div class="flex items-center gap-2">
<VbenAvatar
:alt="task?.createByName ?? ''"
class="bg-primary size-[28px] rounded-full text-white"
src=""
/>
<span>{{ task.createByName }}</span>
<div class="flex items-center opacity-50">
<div class="flex items-center gap-1">
<span class="icon-[bxs--category-alt] size-[16px]"></span>
流程分类: {{ task.categoryName }}
</div>
<Divider type="vertical" />
<div class="flex items-center gap-1">
<span class="icon-[mdi--clock-outline] size-[16px]"></span>
提交时间: {{ task.createTime }}
......@@ -447,154 +195,32 @@ async function handleCopy(text: string) {
</div>
</div>
</div>
<Tabs v-if="currentFlowInfo" class="flex-1">
<TabPane key="1" tab="审批详情">
<ApprovalDetails
:current-flow-info="currentFlowInfo"
:iframe-loaded="iframeLoaded"
:iframe-height="iframeHeight"
:task="task"
/>
</TabPane>
<TabPane key="2" tab="审批流程图">
<FlowPreview :instance-id="currentFlowInfo.instanceId" />
</TabPane>
</Tabs>
</div>
<!-- 固定底部 -->
<!-- 固定底部 占位高度 -->
<div class="h-[58px]"></div>
<div
<FlowActions
v-if="showFooter"
:class="
cn(
'absolute bottom-0 left-0',
'border-t-solid border-t-[1px]',
'bg-background w-full p-3',
)
"
>
<div class="flex justify-end">
<Space v-if="type === 'myself'">
<Button
v-if="revocable"
danger
ghost
type="primary"
:icon="h(RollbackOutlined)"
@click="handleCancel"
>
撤销申请
</Button>
<Button
type="primary"
ghost
v-if="editableAndRemoveable"
:icon="h(EditOutlined)"
@click="handleEdit"
>
重新编辑
</Button>
<Button
v-if="editableAndRemoveable"
danger
ghost
type="primary"
:icon="h(EditOutlined)"
@click="handleRemove"
>
删除
</Button>
</Space>
<Space v-if="type === 'approve'">
<Button
type="primary"
ghost
:icon="h(CheckOutlined)"
@click="handleApproval"
>
通过
</Button>
<Button
v-if="buttonPermissions?.termination"
danger
ghost
type="primary"
:icon="h(ExclamationCircleOutlined)"
@click="handleTermination"
>
终止
</Button>
<Button
v-if="buttonPermissions?.back"
danger
ghost
type="primary"
:icon="h(ArrowLeftOutlined)"
@click="handleRejection"
>
驳回
</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>
<Button v-if="showButtonOther" :icon="h(MenuOutlined)">
其他
</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"
:type="type"
:task="task"
:button-permissions="buttonPermissions"
@reload="$emit('reload')"
/>
</Space>
<Space v-if="type === 'admin'">
<Button @click="handleFlowInterfere"> 流程干预 </Button>
<Button @click="() => updateAssigneeModalApi.open()">
修改办理人
</Button>
<FlowInterfereModal @complete="$emit('reload')" />
<UpdateAssigneeModal mode="single" @finish="handleUpdateAssignee" />
</Space>
</div>
</div>
</Card>
<slot v-else name="empty">
<Fallback title="点击左侧选择" />
</slot>
......
......@@ -45,11 +45,8 @@ onMounted(async () => {
}));
});
/**
* 这里无法处理昵称中带,的情况
*/
const isMultiplePerson = computed(
() => props.item.approveName?.split(',').length > 1,
() => props.item.approver?.split(',').length > 1,
);
</script>
......@@ -87,6 +84,7 @@ const isMultiplePerson = computed(
</div>
<div :class="cn('mt-2 flex flex-wrap gap-2')" v-if="isMultiplePerson">
<!-- 如果昵称中带, 这里的处理是不准确的 -->
<div
:class="cn('bg-foreground/5 flex items-center rounded-full', 'p-1')"
v-for="(name, index) in item.approveName.split(',')"
......
<script setup lang="ts">
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';
const props = defineProps<{
interface Props {
list: Flow[];
}>();
}
defineProps<Props>();
</script>
<template>
<Timeline v-if="props.list.length > 0">
<ApprovalTimelineItem
v-for="item in props.list"
:key="item.id"
:item="item"
/>
<Timeline v-if="list.length > 0">
<ApprovalTimelineItem v-for="item in list" :key="item.id" :item="item" />
</Timeline>
<Empty v-else />
</template>
<!-- 流程图预览组件 -->
<script setup lang="ts">
import { useAppConfig } from '@vben/hooks';
import { stringify } from '@vben/request';
......@@ -7,7 +9,14 @@ import { useWarmflowIframe } from './hook';
defineOptions({ name: 'FlowPreview' });
const props = defineProps<{ instanceId: number }>();
const props = defineProps<Props>();
interface Props {
/**
* 流程实例ID
*/
instanceId: number;
}
const { clientId } = useAppConfig(import.meta.env, import.meta.env.PROD);
......@@ -21,6 +30,7 @@ const params = {
/**
* iframe地址
* 后端地址 + 固定flow地址拼接
*/
const url = `${import.meta.env.VITE_GLOB_API_URL}/warm-flow-ui/index.html?${stringify(params)}`;
......@@ -28,5 +38,9 @@ const { iframeRef } = useWarmflowIframe();
</script>
<template>
<iframe ref="iframeRef" :src="url" class="h-[500px] w-full border"></iframe>
<iframe
ref="iframeRef"
:src="url"
class="h-[600px] w-full rounded-[6px] border"
></iframe>
</template>
......@@ -10,19 +10,25 @@ export function useWarmflowIframe() {
const iframeRef = useTemplateRef<HTMLIFrameElement>('iframeRef');
const { isDark } = usePreferences();
onMounted(() => {
/**
* load只是iframe加载完 而非vue加载完
*/
iframeRef.value?.addEventListener('load', async () => {
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(() => {
/**
* load只是iframe加载完 而非vue加载完
*/
iframeRef.value?.addEventListener('load', iframeLoadEvent);
});
});
// onBeforeUnmount(() => {
// iframeRef.value?.removeEventListener('load', iframeLoadEvent);
// });
// 监听主题切换 通知iframe切换
watch(isDark, (dark) => {
......
export {};
/**
* myself 我发起的
* readonly 只读 只用于查看
* approve 审批(我的待办)
* admin 流程监控 - 待办任务使用
*/
export type ApprovalType = 'admin' | 'approve' | 'myself' | 'readonly';
......@@ -149,9 +149,10 @@ const [InstanceVariableModal, instanceVariableModalApi] = useVbenModal({
connectedComponent: instanceVariableModal,
});
function handleVariable(row: Recordable<any>) {
instanceVariableModalApi.setData({ record: row.variable });
instanceVariableModalApi.setData({ instanceId: row.id });
instanceVariableModalApi.open();
}
const [FlowInfoModal, flowInfoModalApi] = useVbenModal({
connectedComponent: flowInfoModal,
});
......
<script setup lang="ts">
import { useVbenModal } from '@vben/common-ui';
<script setup lang="tsx">
import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { JsonPreview, useVbenModal } from '@vben/common-ui';
import { cn, getPopupContainer } from '@vben/utils';
import { message, Modal, Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
import { workflowInstanceInvalid } from '#/api/workflow/instance/index';
import { instanceVariable, updateFlowVariable } from '#/api/workflow/instance';
const emit = defineEmits<{ reload: [] }>();
interface ModalData {
/**
* 变量 json字符串
*/
record: string;
instanceId: string;
}
const data = ref({});
const [BasicModal, modalApi] = useVbenModal({
onConfirm: handleSubmit,
onCancel: handleCancel,
title: '流程变量',
fullscreenButton: false,
title: '作废原因',
footer: false,
onOpenChange: async (visible) => {
if (!visible) {
data.value = {};
return null;
}
modalApi.modalLoading(true);
await loadData();
modalApi.modalLoading(false);
},
});
const [BasicForm, formApi] = useVbenForm({
const fieldTypeColors = {
string: 'cyan',
number: 'blue',
boolean: 'orange',
object: 'purple',
};
function getFieldTypeColor(fieldType: string) {
return (
fieldTypeColors[fieldType as keyof typeof fieldTypeColors] ?? 'default'
);
}
async function loadData() {
const { instanceId } = modalApi.getData() as ModalData;
const resp = await instanceVariable(instanceId);
const jsonObj = JSON.parse(resp.variable);
data.value = jsonObj;
// 表单
const objEntry = Object.entries(jsonObj);
interface OptionsType {
label: string;
value: string;
fieldType: string;
}
formApi.updateSchema([
{
fieldName: 'key',
componentProps: {
options: objEntry.map(
([key, value]) =>
({
label: key,
value: key,
fieldType: typeof value,
}) as OptionsType,
),
},
renderComponentContent: () => ({
option: (option: OptionsType) => (
<div>
{option.label}
<Tag class="ml-1" color={getFieldTypeColor(option.fieldType)}>
{option.fieldType}
</Tag>
</div>
),
}),
},
]);
}
const [Form, formApi] = useVbenForm({
commonConfig: {
formItemClass: 'col-span-2',
componentProps: {
class: 'w-full',
allowClear: true,
},
labelWidth: 80,
},
layout: 'vertical',
schema: [
{
fieldName: 'comment',
label: '作废原因',
component: 'Textarea',
fieldName: 'key',
component: 'Select',
label: '变量名称',
rules: 'selectRequired',
componentProps: {
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',
component: 'Input',
label: '变量值',
rules: 'required',
},
],
showDefaultActions: false,
wrapperClass: 'grid-cols-2',
resetButtonOptions: {
show: false,
},
submitButtonOptions: {
content: '修改',
},
handleSubmit: async (values) => {
console.log(values);
Modal.confirm({
title: '修改流程变量',
content: '确认修改流程变量吗?',
centered: true,
okButtonProps: {
danger: true,
},
onOk: async () => {
await handleSubmit(values);
},
});
},
});
async function handleCancel() {
modalApi.close();
await formApi.resetForm();
}
async function handleSubmit(values: any) {
try {
modalApi.lock(true);
const { instanceId } = modalApi.getData() as ModalData;
async function handleSubmit() {
let transformValue = values.value;
if (values.valueType !== 'string') {
try {
modalApi.modalLoading(true);
const { valid } = await formApi.validate();
if (!valid) {
return;
transformValue = JSON.parse(values.value);
} catch (error) {
console.error(error);
if (error instanceof Error) {
message.error(error.message);
}
const data = cloneDeep(await formApi.getValues());
data.id = modalApi.getData().id;
await workflowInstanceInvalid(data as any);
emit('reload');
handleCancel();
throw error;
}
}
// 修改
const requestData = {
instanceId,
key: values.key,
value: transformValue,
};
await updateFlowVariable(requestData);
await formApi.resetForm();
// 查询修改后的
const resp = await instanceVariable(instanceId);
const jsonObj = JSON.parse(resp.variable);
data.value = jsonObj;
} catch (error) {
console.error(error);
} finally {
modalApi.modalLoading(false);
modalApi.lock(false);
}
}
</script>
<template>
<BasicModal>
<BasicForm />
<div
:class="cn('min-h-[400px] overflow-y-auto border', 'rounded-[4px] p-2')"
>
<JsonPreview :data="data" />
</div>
<!-- <div class="mt-2 break-all text-sm font-medium text-orange-500">
需要支持变量类型需要更改后端代码(原版只支持string类型)
<div>
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/FlowVariableBo.java
</div>
将value的类型改为Object才能使用
</div> -->
<Form class="mt-2" />
</BasicModal>
</template>
......@@ -495,7 +495,7 @@ $namespace: vben;
&.is-rounded {
--menu-item-margin-x: 8px;
--menu-item-collapse-margin-x: 6px;
--menu-item-radius: 8px;
--menu-item-radius: 6px;
}
&.is-horizontal:not(.is-rounded) {
......
......@@ -12,15 +12,13 @@ function isAccessible(
el: Element,
binding: DirectiveBinding<string | string[]>,
) {
const { accessMode, hasAccessByCodes, hasAccessByRoles } = useAccess();
const { hasAccessByCodes, hasAccessByRoles } = useAccess();
const value = binding.value;
if (!value) return;
const authMethod =
accessMode.value === 'frontend' && binding.arg === 'role'
? hasAccessByRoles
: hasAccessByCodes;
binding.arg === 'role' ? hasAccessByRoles : hasAccessByCodes;
const values = Array.isArray(value) ? value : [value];
......
......@@ -17,6 +17,10 @@ function useAccess() {
*/
function hasAccessByRoles(roles: string[]) {
const userRoleSet = new Set(userStore.userRoles);
// 超管的角色
if (userRoleSet.has('superadmin')) {
return true;
}
const intersection = roles.filter((item) => userRoleSet.has(item));
return intersection.length > 0;
}
......
......@@ -133,3 +133,8 @@ TODO: 最后一条数据hover/check仍会显示边框
border-radius: var(--vxe-ui-table-border-radius)
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;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment