feat(projects): 新增用户选择器组件,添加流程干预按钮

This commit is contained in:
AN 2025-06-24 23:46:15 +08:00
parent 81449ea77a
commit b8c771cd1d
4 changed files with 339 additions and 5 deletions

View File

@ -0,0 +1,308 @@
<script setup lang="tsx">
import { computed, ref, watch } from 'vue';
import { NButton } from 'naive-ui';
import { useLoading } from '@sa/hooks';
import { fetchGetDeptTree, fetchGetUserList } from '@/service/api/system';
import { useAppStore } from '@/store/modules/app';
import { useTable, useTableOperate } from '@/hooks/common/table';
import { useDict } from '@/hooks/business/dict';
import { $t } from '@/locales';
import UserSearch from '@/views/system/user/modules/user-search.vue';
import DictTag from './dict-tag.vue';
defineOptions({
name: 'UserSelectModal'
});
interface Props {
title?: string;
multiple?: boolean;
/** 禁选用户ID */
disabledIds?: CommonType.IdType[];
}
const props = withDefaults(defineProps<Props>(), {
title: '用户选择',
multiple: false,
disabledIds: () => []
});
const visible = defineModel<boolean>('visible', {
default: false
});
useDict('sys_normal_disable');
const appStore = useAppStore();
const {
columns,
columnChecks,
data,
getData,
getDataByPage,
loading,
mobilePagination,
searchParams,
resetSearchParams
} = useTable({
apiFn: fetchGetUserList,
apiParams: {
pageNum: 1,
pageSize: 10,
// if you want to use the searchParams in Form, you need to define the following properties, and the value is null
// the value can not be undefined, otherwise the property in Form will not be reactive
deptId: null,
userName: null,
nickName: null,
phonenumber: null,
status: null,
params: {}
},
immediate: false,
columns: () => [
{
type: 'selection',
multiple: props.multiple,
align: 'center',
width: 48,
disabled: row => props.disabledIds.includes(row.userId.toString())
},
{
key: 'index',
title: $t('common.index'),
align: 'center',
width: 64
},
{
key: 'userName',
title: $t('page.system.user.userName'),
align: 'center',
minWidth: 120,
ellipsis: true
},
{
key: 'nickName',
title: $t('page.system.user.nickName'),
align: 'center',
minWidth: 120,
ellipsis: true
},
{
key: 'deptName',
title: $t('page.system.user.deptName'),
align: 'center',
minWidth: 120,
ellipsis: true
},
{
key: 'phonenumber',
title: $t('page.system.user.phonenumber'),
align: 'center',
minWidth: 120,
ellipsis: true
},
{
key: 'status',
title: $t('page.system.user.status'),
align: 'center',
minWidth: 80,
render(row) {
return <DictTag dict-code="sys_normal_disable" value={row.status} />;
}
},
{
key: 'createTime',
title: $t('page.system.user.createTime'),
align: 'center',
minWidth: 120
}
]
});
const { checkedRowKeys } = useTableOperate(data, getData);
const { loading: treeLoading, startLoading: startTreeLoading, endLoading: endTreeLoading } = useLoading();
const deptPattern = ref<string>();
const deptData = ref<Api.Common.CommonTreeRecord>([]);
const selectedKeys = ref<string[]>([]);
async function getTreeData() {
startTreeLoading();
const { data: tree, error } = await fetchGetDeptTree();
if (!error) {
deptData.value = tree;
}
endTreeLoading();
}
function handleClickTree(keys: string[]) {
searchParams.deptId = keys.length ? keys[0] : null;
checkedRowKeys.value = [];
getDataByPage();
}
function handleResetTreeData() {
deptPattern.value = undefined;
getTreeData();
}
const expandedKeys = ref<CommonType.IdType[]>([100]);
const selectable = computed(() => {
return !loading.value;
});
function handleResetSearch() {
resetSearchParams();
selectedKeys.value = [];
}
function closeModal() {
visible.value = false;
}
function getRowProps(row: Api.System.User) {
return {
onClick: () => {
if (props.disabledIds.includes(row.userId.toString())) {
return;
}
if (props.multiple) {
const index = checkedRowKeys.value.findIndex(key => key === row.userId);
if (index > -1) {
checkedRowKeys.value.splice(index, 1);
} else {
checkedRowKeys.value.push(row.userId);
}
} else {
checkedRowKeys.value = [row.userId];
}
}
};
}
watch(visible, () => {
if (visible.value) {
getTreeData();
getData();
}
});
</script>
<template>
<NModal
v-model:show="visible"
class="user-select-modal max-h-800px max-w-90% w-1400px"
preset="card"
size="medium"
:title="props.title"
>
<TableSiderLayout :sider-title="$t('page.system.dept.title')">
<template #header-extra>
<NButton size="small" text class="h-18px" @click.stop="() => handleResetTreeData()">
<template #icon>
<SvgIcon icon="ic:round-refresh" />
</template>
</NButton>
</template>
<template #sider>
<NInput v-model:value="deptPattern" clearable :placeholder="$t('common.keywordSearch')" />
<NSpin class="dept-tree" :show="treeLoading">
<NTree
v-model:expanded-keys="expandedKeys"
v-model:selected-keys="selectedKeys"
block-node
show-line
:data="deptData as []"
:show-irrelevant-nodes="false"
:pattern="deptPattern"
class="infinite-scroll h-full min-h-200px py-3"
key-field="id"
label-field="label"
virtual-scroll
:selectable="selectable"
@update:selected-keys="handleClickTree"
>
<template #empty>
<NEmpty :description="$t('page.system.dept.empty')" class="h-full min-h-200px justify-center" />
</template>
</NTree>
</NSpin>
</template>
<div class="h-full flex-col-stretch gap-12px overflow-hidden lt-sm:max-h-500px lt-sm:overflow-auto">
<UserSearch v-model:model="searchParams" @reset="handleResetSearch" @search="getDataByPage" />
<TableRowCheckAlert v-model:checked-row-keys="checkedRowKeys" />
<NAlert v-if="props.disabledIds.length > 0" type="warning">
<span>已存在的用户无法被选择</span>
</NAlert>
<NCard
:title="$t('page.system.user.title')"
:bordered="false"
size="small"
class="sm:flex-1-hidden card-wrapper lt-sm:overflow-auto"
>
<template #header-extra>
<TableHeaderOperation
v-model:columns="columnChecks"
:loading="loading"
:show-add="false"
:show-delete="false"
:show-export="false"
@refresh="getData"
></TableHeaderOperation>
</template>
<NDataTable
v-model:checked-row-keys="checkedRowKeys"
:columns="columns"
:data="data"
size="small"
:flex-height="!appStore.isMobile"
:scroll-x="962"
:loading="loading"
:row-props="getRowProps"
remote
:row-key="row => row.userId"
:pagination="mobilePagination"
class="h-full lt-sm:max-h-300px"
/>
</NCard>
</div>
</TableSiderLayout>
<template #footer>
<NSpace justify="end" :size="16">
<NButton @click="closeModal">{{ $t('common.cancel') }}</NButton>
<NButton type="primary">{{ $t('common.confirm') }}</NButton>
</NSpace>
</template>
</NModal>
</template>
<style scoped lang="scss">
:deep(.n-layout) {
height: 600px;
@media (max-width: 639px) {
height: auto;
max-height: 500px;
}
}
.user-select-modal {
@media (max-width: 639px) {
:deep(.n-card-content) {
overflow: hidden;
}
:deep(.n-data-table) {
max-height: 300px;
}
}
}
.n-alert {
--n-padding: 5px 13px !important;
--n-icon-margin: 6px 8px 0 12px !important;
--n-icon-size: 20px !important;
}
</style>

View File

@ -1,12 +1,16 @@
<script lang="ts" setup>
import { useBoolean, useLoading } from '~/packages/hooks/src';
defineOptions({
name: 'FlowInterveneModal'
});
const { bool: multiInstanceVisible, setTrue: openMultiInstanceModal } = useBoolean();
const { bool: transferVisible, setTrue: openTransferModal } = useBoolean();
interface Props {
rowData: Api.Workflow.TaskOrHisTask;
rowData: Api.Workflow.Task;
}
const { loading: btnLoading } = useLoading();
const props = defineProps<Props>();
const visible = defineModel<boolean>('visible', {
@ -16,7 +20,7 @@ const visible = defineModel<boolean>('visible', {
<template>
<NModal v-model:show="visible" class="max-h-520px max-w-90% w-700px" title="流程干预" preset="card" size="medium">
<NDescriptions label-placement="left" :column="2" size="small" bordered>
<NDescriptions :title="props.rowData.flowName" label-placement="left" :column="2" size="small" bordered>
<NDescriptionsItem label="任务名称">
{{ props.rowData.nodeName }}
</NDescriptionsItem>
@ -29,6 +33,9 @@ const visible = defineModel<boolean>('visible', {
<NDescriptionsItem label="流程实例ID">
{{ props.rowData.instanceId }}
</NDescriptionsItem>
<NDescriptionsItem label="办理人">
<GroupTag :value="props.rowData.assigneeNames" />
</NDescriptionsItem>
<NDescriptionsItem label="版本号">
{{ props.rowData.version }}
</NDescriptionsItem>
@ -36,5 +43,21 @@ const visible = defineModel<boolean>('visible', {
{{ props.rowData.businessId }}
</NDescriptionsItem>
</NDescriptions>
<template #footer>
<NSpace justify="end" :size="16">
<NButton :disabled="btnLoading" type="primary" @click="openTransferModal">转办</NButton>
<NButton :disabled="btnLoading" type="primary" @click="openMultiInstanceModal">加签</NButton>
<NButton :disabled="btnLoading" type="primary">减签</NButton>
<NButton :disabled="btnLoading" type="error">中止</NButton>
</NSpace>
</template>
<!-- 加签用户选择器 -->
<UserSelectModal
v-model:visible="multiInstanceVisible"
multiple
:disabled-ids="props.rowData.assigneeIds.split(',')"
/>
<!-- 转办用户选择器 -->
<UserSelectModal v-model:visible="transferVisible" :disabled-ids="props.rowData.assigneeIds.split(',')" />
</NModal>
</template>

View File

@ -169,6 +169,7 @@ declare module 'vue' {
ThemeSchemaSwitch: typeof import('./../components/common/theme-schema-switch.vue')['default']
TinymceEditor: typeof import('./../components/custom/tinymce-editor.vue')['default']
UserSelect: typeof import('./../components/custom/user-select.vue')['default']
UserSelectModal: typeof import('./../components/custom/user-select-modal.vue')['default']
WaveBg: typeof import('./../components/custom/wave-bg.vue')['default']
}
}

View File

@ -30,6 +30,8 @@ const { bool: viewVisible, setTrue: showViewDrawer } = useBoolean(false);
const { bool: interveneVisible, setTrue: showInterveneDrawer } = useBoolean(false);
const dynamicComponent = shallowRef();
type Task = Api.Workflow.Task;
const waitingStatus = ref<boolean>(true);
const waitingStatusOptions = ref<WaitingStatusOption[]>([
{ label: '待办任务', value: true },
@ -114,7 +116,7 @@ const operateColumns = ref<NaiveUI.TableColumn<Api.Workflow.TaskOrHisTask>[]>([
/>
];
if (waitingStatus.value) {
if (waitingStatus.value && row.flowStatus !== 'draft') {
buttons.push(
<ButtonIcon
text
@ -302,7 +304,7 @@ function handleIntervene(row: Api.Workflow.TaskOrHisTask) {
class="sm:h-full"
/>
<component :is="dynamicComponent" :visible="viewVisible" operate-type="detail" :business-id="businessId" />
<FlowInterveneModal v-model:visible="interveneVisible" :row-data="interveneRowData!" />
<FlowInterveneModal v-model:visible="interveneVisible" :row-data="interveneRowData as Task" />
</NCard>
</div>
</TableSiderLayout>