czh-20260414-实现分发功能的前端

This commit is contained in:
zhihao
2026-04-14 10:03:00 +08:00
parent 6650905733
commit f42296180b
7 changed files with 2839 additions and 0 deletions
@@ -0,0 +1,91 @@
import { defHttp } from '/@/utils/http/axios';
enum Api {
list = '/officialdocDistribute/djOfficialdocDistribute/list',
save = '/officialdocDistribute/djOfficialdocDistribute/add',
edit = '/officialdocDistribute/djOfficialdocDistribute/edit',
deleteOne = '/officialdocDistribute/djOfficialdocDistribute/delete',
}
export const list = (params?) => defHttp.get({ url: Api.list, params });
export const save = (params) => defHttp.post({ url: Api.save, params });
export const edit = (params) => defHttp.post({ url: Api.edit, params });
export const deleteOne = (params) => defHttp.delete({ url: Api.deleteOne, params }, { joinParamsToUrl: true });
export const saveOrUpdate = (params, isUpdate) => {
let url = isUpdate ? Api.edit : Api.save;
return defHttp.post({ url, params });
};
export interface DepartTreeNode {
id: string;
key: string;
value: string;
title: string;
isLeaf?: boolean;
departType?: string | number;
distributable?: number;
disableCheckbox?: boolean;
assignType?: number;
children?: DepartTreeNode[];
description?: string;
parentId?: string;
orgCode?: string;
}
export interface UserItem {
id: string;
username: string;
realname: string;
orgCodeTxt?: string;
userSecurityLevel?: number;
userSecurityLevel_dictText?: string;
sex_dictText?: string;
sortno?: number;
departId?: string;
departName?: string;
assignType?: number;
}
export interface DistributeRecord {
id: string;
name: string;
userId: string;
username: string;
selected: string;
}
export type AssignType = 1 | 2 | 3;
export function deleteIfExist<T extends Record<string, any>>(array: T[], value: T, key: string): void {
const idx = array.findIndex((item) => item[key] === value[key]);
if (idx >= 0) {
array.splice(idx, 1);
}
}
export function recurTree<T extends Record<string, any>, R>(tree: T[], callback: (node: T) => R[]): R[] {
let res: R[] = [];
tree.forEach((item) => {
res = res.concat(callback(item));
if (item.children) {
res = res.concat(recurTree(item.children, callback));
}
});
return res;
}
export function filterDepartTree<T extends Record<string, any>>(departTree: T[], searchValue: string): T[] {
let filtered: T[] = [];
departTree.forEach((item) => {
if (item.title && item.title.includes(searchValue)) {
filtered.push(Object.assign({}, item, { children: null, isLeaf: true }));
}
if (item.children) {
const result = filterDepartTree(item.children, searchValue);
if (result.length > 0) {
filtered = filtered.concat(result);
}
}
});
return filtered;
}
@@ -0,0 +1,696 @@
<template>
<div>
<a-button type="primary" @click="showModal" v-if="showButton">
分发对象
</a-button>
<a-row v-if="showButton" style="margin-top: 8px">
<a-col :span="24" style="width: 850px">
<a-button-group v-for="d in distributes" :key="d.id" style="margin: 0 8px 0 0">
<a-button type="primary" @click="setDistributeAndSubmit(d)" :title="d.name" style="min-width: 80px">
<span class="distribute-name">{{ d.name }}</span>
</a-button>
<a-button type="primary" @click="deleteDistribute(d.id)">
<CloseOutlined />
</a-button>
</a-button-group>
</a-col>
</a-row>
<a-card style="margin: 8px 0 8px 0; width: 850px; height: 170px; overflow-y: auto" :body-style="{ padding: '8px 8px 0 8px' }">
<SzSelectedDepartsAndUsers
key="out"
:disabled="true"
:selectedReadUsers="selectedReadUsers"
:selectedUsers="selectedUsers"
:selectedReadDeparts="selectedReadDeparts"
:selectedAssistDeparts="selectedAssistDeparts"
:selectedMainDeparts="selectedMainDeparts"
@update:selectedMainDeparts="handleMainDepartsChange"
@update:selectedAssistDeparts="handleAssistDepartsChange"
@update:selectedReadDeparts="handleReadDepartsChange"
@update:selectedUsers="handleUsersChange"
@update:selectedReadUsers="handleReadUsersChange"
/>
</a-card>
<a-modal
:width="width"
:title="titleText"
v-model:open="visible"
centered
:confirm-loading="confirmLoading"
@ok="handleOk"
@cancel="handleCancel"
:mask-closable="false"
>
<div>
<a-row :gutter="8">
<a-col :span="3">
<h3 style="padding-left: 8px">已存方案</h3>
<div style="height: 556px">
<a-empty v-if="distributes.length === 0" description="未保存方案" />
<a-button-group v-for="d in distributes" :key="d.id" class="small-space distribute-btn-group">
<a-button size="small" type="primary" @click="setDistribute(d)" :title="d.name" style="width: 100px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis">
<span class="distribute-name">{{ d.name }}</span>
</a-button>
<a-button size="small" type="primary" @click="deleteDistribute(d.id)">
<CloseOutlined />
</a-button>
</a-button-group>
</div>
</a-col>
<a-col :span="12">
<a-card
:tab-list="tabList"
:active-tab-key="activeTab"
@tabChange="(key) => onTabChange(key)"
style="overflow: hidden"
:body-style="{ height: '556px', overflow: 'hidden' }"
>
<div v-if="activeTab === '1'">
<SzSelectDepart
:selectedReadDeparts="selectedReadDeparts"
:selectedAssistDeparts="selectedAssistDeparts"
:selectedMainDeparts="selectedMainDeparts"
@update:selectedMainDeparts="handleMainDepartsChange"
@update:selectedAssistDeparts="handleAssistDepartsChange"
@update:selectedReadDeparts="handleReadDepartsChange"
:excludeDeparts="excludeDeparts"
:departId="departId"
:role="role"
/>
</div>
<div v-else-if="activeTab === '2'">
<SzSelectUser
key="selectUser"
v-model:selectedDepId="selectedUserDeptId"
@update:checkedUser="handleUsersChange"
:checkedUser="selectedUsers"
:secLevel="secLevel"
:departId="departId"
:role="role"
:isSpecial="isSpecial"
/>
</div>
<div v-else-if="activeTab === '3'">
<SzSelectUser
key="selectReadUser"
v-model:selectedDepId="selectedReadUserDeptId"
@update:checkedUser="handleReadUsersChange"
:checkedUser="selectedReadUsers"
:secLevel="secLevel"
:departId="departId"
:role="role"
:isSpecial="isSpecial"
/>
</div>
</a-card>
</a-col>
<a-col :span="9">
<a-card :body-style="{ height: '556px', display: 'flex', flexDirection: 'column' }">
<template #title>
已选&nbsp;&nbsp;
<a-button type="danger" @click="clearSelected">清空</a-button>
</template>
<div style="flex: 1; overflow: auto; min-height: 0">
<SzSelectedDepartsAndUsers
key="in"
:selectedReadUsers="selectedReadUsers"
:selectedUsers="selectedUsers"
:selectedReadDeparts="selectedReadDeparts"
:selectedAssistDeparts="selectedAssistDeparts"
:selectedMainDeparts="selectedMainDeparts"
@update:selectedMainDeparts="handleMainDepartsChange"
@update:selectedAssistDeparts="handleAssistDepartsChange"
@update:selectedReadDeparts="handleReadDepartsChange"
@update:selectedUsers="handleUsersChange"
@update:selectedReadUsers="handleReadUsersChange"
/>
</div>
<div style="margin: 0 8px 8px 8px">
<a-form-item label="分发意见">
<a-textarea placeholder="请在此处输入意见。" style="height: 100px" v-model:value="reason" />
</a-form-item>
</div>
<div style="margin: 0 8px 0 8px">
<a-input-search v-model:value="activeDistribute" @search="saveDistributes" enter-button="保存为方案" />
</div>
</a-card>
</a-col>
</a-row>
</div>
</a-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch, onMounted, h } from 'vue';
import { CloseOutlined } from '@ant-design/icons-vue';
import { Modal, message, Empty } from 'ant-design-vue';
import { defHttp } from '/@/utils/http/axios';
import { useUserStore } from '/@/store/modules/user';
import { filterDictTextByCache } from '/@/utils/dict/JDictSelectUtil';
import { deleteIfExist, list, save, edit, deleteOne } from './SzDistribute.api';
import type { DepartTreeNode, UserItem } from './SzDistribute.api';
import SzSelectDepart from './SzSelectDepart.vue';
import SzSelectUser from './SzSelectUser.vue';
import SzSelectedDepartsAndUsers from './SzSelectedDepartsAndUsers.vue';
const props = withDefaults(
defineProps<{
width?: string | number;
title: string;
docId?: string;
departId?: string;
role: string | number;
secLevel: any;
checkedMainDepartIds?: string;
checkedAssistDepartIds?: string;
checkedReadDepartIds?: string;
checkedUsernames?: string;
checkedReadUsernames?: string;
reasons?: string;
isSpecial?: number;
modelValue?: string;
showButton?: boolean;
}>(),
{
width: 1500,
title: '分发方案',
docId: '',
departId: '',
role: 1,
checkedMainDepartIds: '',
checkedAssistDepartIds: '',
checkedReadDepartIds: '',
checkedUsernames: '',
checkedReadUsernames: '',
reasons: '',
isSpecial: 0,
modelValue: '',
showButton: true,
}
);
const emit = defineEmits([
'update:checkedDepartIds',
'update:checkedMainDepartIds',
'update:checkedAssistDepartIds',
'update:checkedReadDepartIds',
'update:checkedUsernames',
'update:checkedReadUsernames',
'update:reasons',
'update:modelValue',
'update:checkedDeparts',
'change',
]);
const excludeDeparts = ref<string[]>([]);
const visible = ref(false);
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE;
const confirmLoading = ref(false);
const activeTab = ref('1');
const activeDistribute = ref('');
const selectedMainDeparts = ref<DepartTreeNode[]>([]);
const selectedAssistDeparts = ref<DepartTreeNode[]>([]);
const selectedReadDeparts = ref<DepartTreeNode[]>([]);
const selectedUsers = ref<UserItem[]>([]);
const selectedReadUsers = ref<UserItem[]>([]);
const selectedUserDeptId = ref('');
const selectedReadUserDeptId = ref('');
const reason = ref('');
const distributes = ref<any[]>([]);
const tabList = [
{ key: '1', tab: '选择部门' },
{ key: '2', tab: '选择办文人员' },
{ key: '3', tab: '选择阅文人员' },
];
const selectedMainDepartIds = computed(() => selectedMainDeparts.value.map((item) => item.id));
const selectedAssistDepartIds = computed(() => selectedAssistDeparts.value.map((item) => item.id));
const selectedReadDepartIds = computed(() => selectedReadDeparts.value.map((item) => item.id));
const selectedDepartIds = computed(() => selectedMainDepartIds.value.concat(selectedAssistDepartIds.value).concat(selectedReadDepartIds.value));
const selectedUsernames = computed(() => selectedUsers.value.map((item) => item.username));
const selectedReadUsernames = computed(() => selectedReadUsers.value.map((item) => item.username));
const distributeStr = computed(() => {
const mainIds = selectedMainDeparts.value.map((item) => item.id);
const assistIds = selectedAssistDeparts.value.map((item) => item.id);
const readIds = selectedReadDeparts.value.map((item) => item.id);
const usernames = selectedUsers.value.map((item) => item.username);
const readUsernames = selectedReadUsers.value.map((item) => item.username);
let res = '';
if (mainIds.length > 0) res += 'main: ' + mainIds.join(',');
if (assistIds.length > 0) res += 'assist: ' + assistIds.join(',');
if (readIds.length > 0) res += 'read:' + readIds.join(',');
if (usernames.length > 0) res += 'user:' + usernames.join(',');
if (readUsernames.length > 0) res += 'readUser:' + readUsernames.join(',');
return res;
});
const titleText = computed(() => {
const secLevelText = filterDictTextByCache('officialdoc_security_level', props.secLevel);
if (secLevelText && secLevelText !== 'undefined' && secLevelText !== props.secLevel) {
return '' + secLevelText + '' + props.title;
}
return props.title;
});
watch(
() => props.docId,
() => {
getExcludeDepartIds();
}
);
watch(
() => props.checkedMainDepartIds,
(newVal, oldVal) => {
if (newVal !== oldVal) refreshDepartsAndUsersFromIds({ mainDepartIds: newVal });
}
);
watch(
() => props.checkedAssistDepartIds,
(newVal, oldVal) => {
if (newVal !== oldVal) refreshDepartsAndUsersFromIds({ assistDepartIds: newVal });
}
);
watch(
() => props.checkedReadDepartIds,
(newVal, oldVal) => {
if (newVal !== oldVal) refreshDepartsAndUsersFromIds({ readDepartIds: newVal });
}
);
watch(
() => props.checkedUsernames,
(newVal, oldVal) => {
if (newVal !== oldVal) refreshDepartsAndUsersFromIds({ usernames: newVal });
}
);
watch(
() => props.checkedReadUsernames,
(newVal, oldVal) => {
if (newVal !== oldVal) refreshDepartsAndUsersFromIds({ readUsernames: newVal });
}
);
function refreshDepartsAndUsersFromIds(params: {
mainDepartIds?: string;
assistDepartIds?: string;
readDepartIds?: string;
usernames?: string;
readUsernames?: string;
}) {
const { mainDepartIds, assistDepartIds, readDepartIds, usernames, readUsernames } = params;
if (mainDepartIds) {
defHttp.get({ url: '/sys/sysDepart/queryTreeList', params: { ids: mainDepartIds } }).then((res: any) => {
res.forEach((item: any) => (item.assignType = 1));
selectedMainDeparts.value = [...res];
}).catch(() => { selectedMainDeparts.value = []; });
} else {
selectedMainDeparts.value = [];
}
if (assistDepartIds) {
defHttp.get({ url: '/sys/sysDepart/queryTreeList', params: { ids: assistDepartIds } }).then((res: any) => {
res.forEach((item: any) => (item.assignType = 2));
selectedAssistDeparts.value = [...res];
}).catch(() => { selectedAssistDeparts.value = []; });
} else {
selectedAssistDeparts.value = [];
}
if (readDepartIds) {
defHttp.get({ url: '/sys/sysDepart/queryTreeList', params: { ids: readDepartIds } }).then((res: any) => {
selectedReadDeparts.value = [...res];
}).catch(() => { selectedReadDeparts.value = []; });
} else {
selectedReadDeparts.value = [];
}
if (usernames) {
defHttp.get({ url: '/sys/user/queryUserComponentData', params: { username: usernames, isMultiTranslate: 'true', pageNo: 1, pageSize: 9999 } }).then((res: any) => {
selectedUsers.value = [...(res.records || res)];
}).catch(() => { selectedUsers.value = []; });
} else {
selectedUsers.value = [];
}
if (readUsernames) {
defHttp.get({ url: '/sys/user/queryUserComponentData', params: { username: readUsernames, isMultiTranslate: 'true', pageNo: 1, pageSize: 9999 } }).then((res: any) => {
selectedReadUsers.value = [...(res.records || res)];
}).catch(() => { selectedReadUsers.value = []; });
} else {
selectedReadUsers.value = [];
}
}
function showModal() {
selectedMainDeparts.value = [];
selectedAssistDeparts.value = [];
selectedReadDeparts.value = [];
selectedUsers.value = [];
selectedReadUsers.value = [];
refreshDepartsAndUsersFromIds({
mainDepartIds: props.checkedMainDepartIds,
assistDepartIds: props.checkedAssistDepartIds,
readDepartIds: props.checkedReadDepartIds,
usernames: props.checkedUsernames,
readUsernames: props.checkedReadUsernames,
});
getExcludeDepartIds();
visible.value = true;
}
function submitDistribute() {
emitChanges();
}
function handleOk() {
submitDistribute();
visible.value = false;
}
function handleCancel() {
visible.value = false;
selectedMainDeparts.value = [];
selectedAssistDeparts.value = [];
selectedReadDeparts.value = [];
selectedUsers.value = [];
selectedReadUsers.value = [];
refreshDepartsAndUsersFromIds({
mainDepartIds: props.checkedMainDepartIds,
assistDepartIds: props.checkedAssistDepartIds,
readDepartIds: props.checkedReadDepartIds,
usernames: props.checkedUsernames,
readUsernames: props.checkedReadUsernames,
});
}
function onTabChange(key: string) {
activeTab.value = key;
}
function validateUser(savedUsers: UserItem[]) {
const uniqueUsers = Array.from(new Set(savedUsers));
const userIds = uniqueUsers.map((user) => user.id).join(',');
defHttp
.get({ url: '/sys/user/queryUserComponentData', params: { id: userIds, isMultiTranslate: 'true', pageNo: 1, pageSize: 9999 } })
.then((res: any) => {
const userList = res.records || res;
const refreshedUsersMap = new Map(userList.map((user: any) => [user.id, user]));
let secProhibitUser: any[] = [];
let departChangeUser: any[] = [];
let abnormalUser: any[] = [];
uniqueUsers.forEach((user) => {
const refreshedUser = refreshedUsersMap.get(user.id);
if (refreshedUser) {
if (refreshedUser.userSecurityLevel <= props.secLevel) {
secProhibitUser.push(refreshedUser);
}
if (user.orgCodeTxt !== refreshedUser.orgCodeTxt) {
departChangeUser.push(refreshedUser);
}
} else {
abnormalUser.push(user);
}
});
let confirmStr = '';
if (secProhibitUser.length > 0) {
confirmStr += '密级不满足要求的员工: ' + secProhibitUser.map((user) => user.realname).join(',') + '。';
}
if (departChangeUser.length > 0) {
confirmStr += '部门发生变动的员工:' + departChangeUser.map((user) => user.realname).join(',') + '。';
}
if (abnormalUser.length > 0) {
confirmStr += '已离职或状态为冻结的员工:' + abnormalUser.map((user) => user.realname).join(',');
}
if (confirmStr !== '') {
Modal.warning({
title: '方案保存的部分员工发生变动!将自动取消以下人员:',
content: h('div', confirmStr),
onOk: () => {
const deleteUsers = secProhibitUser.concat(departChangeUser).concat(abnormalUser);
deleteUsers.forEach((deleteUser) => {
const delIdx = selectedUsers.value.findIndex((user) => user.id === deleteUser.id);
if (delIdx >= 0) {
selectedUsers.value.splice(delIdx, 1);
}
const delIdx1 = selectedReadUsers.value.findIndex((user) => user.id === deleteUser.id);
if (delIdx1 >= 0) {
selectedReadUsers.value.splice(delIdx1, 1);
}
});
},
});
}
return secProhibitUser.length === 0 && departChangeUser.length === 0 && abnormalUser.length === 0;
});
}
function setDistribute(distribute: any) {
validateUser(distribute.selected.selectedUsers.concat(distribute.selected.selectedReadUsers));
activeTab.value = '1';
activeDistribute.value = distribute.name;
selectedMainDeparts.value = [...distribute.selected.selectedMainDeparts];
selectedAssistDeparts.value = [...distribute.selected.selectedAssistDeparts];
selectedReadDeparts.value = [...distribute.selected.selectedReadDeparts];
selectedUsers.value = [...distribute.selected.selectedUsers];
selectedReadUsers.value = [...distribute.selected.selectedReadUsers];
reason.value = distribute.selected.reason;
message.success('加载方案成功!');
}
function setDistributeAndSubmit(distribute: any) {
setDistribute(distribute);
submitDistribute();
}
function refreshDistributes() {
list().then((res: any) => {
distributes.value = [...res];
});
}
function saveDistributes(name: string) {
if (name === '') {
message.error('请输入方案名!');
return;
}
const dIdx = distributes.value.findIndex((item) => item.name === name);
const userStore = useUserStore();
const userId = userStore.getUserInfo?.id;
const username = userStore.getUserInfo?.username;
const data = {
name,
userId,
username,
selected: {
selectedMainDeparts: selectedMainDeparts.value,
selectedAssistDeparts: selectedAssistDeparts.value,
selectedReadDeparts: selectedReadDeparts.value,
selectedUsers: selectedUsers.value,
selectedReadUsers: selectedReadUsers.value,
reason: reason.value,
},
};
if (dIdx < 0) {
save(data).then(() => {
message.success('新增方案成功!');
refreshDistributes();
});
} else {
data.id = distributes.value[dIdx].id;
edit(data).then(() => {
message.success('更新方案成功!');
refreshDistributes();
});
}
}
function deleteDistribute(id: string) {
Modal.confirm({
title: '确认删除',
content: '是否删除该方案?',
onOk: () => {
deleteOne({ id }).then(() => {
refreshDistributes();
});
},
});
}
function generateReasonText(): string {
let result = '';
if (selectedMainDeparts.value.length > 0) {
const departNames = selectedMainDeparts.value.map((item) => item.title).join('');
result += '请' + departNames + '主办。';
}
if (selectedAssistDeparts.value.length > 0) {
const departNames = selectedAssistDeparts.value.map((item) => item.title).join('');
result += '请' + departNames + '协办。';
}
if (selectedReadDeparts.value.length > 0) {
const departNames = selectedReadDeparts.value.map((item) => item.title).join('');
result += '请' + departNames + '阅。';
}
if (selectedUsers.value.length > 0) {
const userNames = selectedUsers.value.map((item) => item.realname).join('');
result += '请' + userNames + '办理。';
}
if (selectedReadUsers.value.length > 0) {
const readUserNames = selectedReadUsers.value.map((item) => item.realname).join('');
result += '请' + readUserNames + '阅。';
}
return result;
}
function clearSelected() {
selectedMainDeparts.value = [];
selectedAssistDeparts.value = [];
selectedReadDeparts.value = [];
selectedUsers.value = [];
selectedReadUsers.value = [];
}
function getExcludeDepartIds() {
if (props.docId !== '') {
defHttp
.get({ url: '/distributionProcessinfo/officialdocDistributionProcessinfo/queryDeptIds', params: { docId: props.docId } })
.then((res: any) => {
excludeDeparts.value = res;
})
.catch(() => {
excludeDeparts.value = [];
});
}
}
function handleMainDepartsChange(newVal: DepartTreeNode[]) {
selectedMainDeparts.value = [...newVal];
}
function handleAssistDepartsChange(newVal: DepartTreeNode[]) {
selectedAssistDeparts.value = [...newVal];
}
function handleReadDepartsChange(newVal: DepartTreeNode[]) {
selectedReadDeparts.value = [...newVal];
}
function handleUsersChange(newVal: UserItem[]) {
selectedUsers.value = [...newVal];
}
function handleReadUsersChange(newVal: UserItem[]) {
selectedReadUsers.value = [...newVal];
}
function emitChanges() {
emit('update:checkedDepartIds', selectedDepartIds.value.join(','));
emit('update:checkedMainDepartIds', selectedMainDepartIds.value.join(','));
emit('update:checkedAssistDepartIds', selectedAssistDepartIds.value.join(','));
emit('update:checkedReadDepartIds', selectedReadDepartIds.value.join(','));
emit('update:checkedUsernames', selectedUsernames.value.join(','));
emit('update:checkedReadUsernames', selectedReadUsernames.value.join(','));
emit('update:modelValue', distributeStr.value);
emit('change', distributeStr.value);
if (reason.value) {
emit('update:reasons', reason.value);
} else {
emit('update:reasons', generateReasonText());
}
emit('update:checkedDeparts', selectedMainDeparts.value.concat(selectedAssistDeparts.value).concat(selectedReadDeparts.value));
}
onMounted(() => {
if (props.role === 2 || props.role === '2') {
activeTab.value = '3';
}
refreshDepartsAndUsersFromIds({
mainDepartIds: props.checkedMainDepartIds,
assistDepartIds: props.checkedAssistDepartIds,
readDepartIds: props.checkedReadDepartIds,
usernames: props.checkedUsernames,
readUsernames: props.checkedReadUsernames,
});
refreshDistributes();
});
</script>
<style lang="less">
.small-space {
margin: 0 8px 8px 8px;
}
.distribute-btn-group {
display: flex;
justify-content: center;
}
.distribute-name-btn {
flex: 1;
overflow: hidden;
}
.distribute-name {
display: inline-block;
vertical-align: bottom;
max-width: 60px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ant-btn {
&.btn-red {
&,
&:active,
&.active {
color: #f5222d;
background: #fff1f0;
border-color: #ffa39e;
}
&:hover,
&:focus {
color: #f5222d;
background: #fff1f0;
border-color: #ffa39e;
}
}
&.btn-blue {
&,
&:active,
&.active {
color: #1890ff;
background: #e6f7ff;
border-color: #91d5ff;
}
&:hover,
&:focus {
color: #1890ff;
background: #e6f7ff;
border-color: #91d5ff;
}
}
&.btn-green {
&,
&:active,
&.active {
color: #52c41a;
background: #f6ffed;
border-color: #b7eb8f;
}
&:hover,
&:focus {
color: #52c41a;
background: #f6ffed;
border-color: #b7eb8f;
}
}
}
</style>
@@ -0,0 +1,659 @@
<template>
<div>
<a-spin tip="Loading..." :spinning="false">
<a-row :gutter="16">
<a-col :span="6">
<a-input-search v-model:value="searchDepartValue" style="margin-bottom: 1px" placeholder="请输入部门名称" />
</a-col>
<a-col :span="18">
<a-space>
<a-input-group v-if="showCheckAll" compact style="display: inline-block; width: auto">
<a-button @click="checkAllDepart">全选部门</a-button>
<a-select v-model:value="checkAllAssignType" :options="assignTypeList" />
</a-input-group>
<a-button type="danger" v-if="showCancelCheckAll" @click="uncheckAllDepart">取消全选</a-button>
<a-input-group v-if="showCheckAllMange" compact style="display: inline-block; width: auto">
<a-button @click="checkAllManageDepart">全选职能部门</a-button>
<a-select v-model:value="checkAllManageAssignType" :options="assignTypeList" />
</a-input-group>
<a-button type="danger" v-if="showCancelCheckAllMange" @click="uncheckAllManageDepart">取消全选职能</a-button>
<a-input-group v-if="showCheckAllBusiness" compact style="display: inline-block; width: auto">
<a-button @click="checkAllBusinessesDepart">全选业务部门</a-button>
<a-select v-model:value="checkAllBusinessAssignType" :options="assignTypeList" />
</a-input-group>
<a-button type="danger" v-if="showCancelCheckAllBusiness" @click="uncheckAllBusinessesDepart">取消全选业务</a-button>
<a-input-group v-if="showCheckAllCurrentSubDepart" compact style="display: inline-block; width: auto">
<a-button @click="checkAllCurrentSubDepart">全选本部门科室</a-button>
<a-select v-model:value="checkAllCurrentSubDepartAssignType" :options="assignTypeList" />
</a-input-group>
<a-button type="danger" v-if="showCancelCheckAllCurrentSubDepart" @click="uncheckAllCurrentSubDepart">取消本部门科室</a-button>
</a-space>
</a-col>
</a-row>
<div v-if="filterDepartTreeData.length === 0" style="height: 470px; display: flex; align-items: center; justify-content: center">
<a-empty description="暂无数据" />
</div>
<a-tree
v-else
ref="treeRef"
checkable
v-model:checkedKeys="checkedKeys"
v-model:selectedKeys="selectedKeys"
class="my-dept-select-tree"
:tree-data="filterDepartTreeData"
:check-strictly="true"
@check="onDepartCheck"
:default-expanded-keys="defaultExpandedKeys"
style="overflow-y: auto"
>
<template #title="{ key: treeKey, title, disableCheckbox, assignType }">
<a-dropdown :trigger="['contextmenu']">
<span v-if="disableCheckbox" style="color: rgba(0,0,0,0.25); cursor: not-allowed">{{ title }}&nbsp;&nbsp;</span>
<span v-else @click="toggleCheck(treeKey, assignType)">{{ title }}&nbsp;&nbsp;</span>
</a-dropdown>
<a-button-group v-if="!disableCheckbox && checkedKeys.checked?.includes(treeKey)">
<a-button size="small" :class="assignType === 1 ? 'btn-red' : ''" @click="checkMainDepart(treeKey, assignType)"></a-button>
<a-button size="small" :class="assignType === 2 ? 'btn-blue' : ''" @click="checkAssistDepart(treeKey, assignType)"></a-button>
<a-button size="small" :class="assignType === 3 || !assignType ? 'btn-green' : ''" @click="checkReadDepart(treeKey, assignType)"></a-button>
</a-button-group>
</template>
</a-tree>
</a-spin>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch, onMounted } from 'vue';
import { queryTreeList } from '/@/api/common/api';
import { pushIfNotExist } from '/@/utils/common/compUtils';
import { deleteIfExist, recurTree, filterDepartTree } from './SzDistribute.api';
import type { DepartTreeNode } from './SzDistribute.api';
const props = withDefaults(
defineProps<{
selectedMainDeparts: DepartTreeNode[];
selectedAssistDeparts: DepartTreeNode[];
selectedReadDeparts: DepartTreeNode[];
excludeDeparts?: string[];
departId: string;
role: string | number;
}>(),
{
excludeDeparts: () => [],
}
);
const emit = defineEmits([
'update:selectedMainDeparts',
'update:selectedAssistDeparts',
'update:selectedReadDeparts',
]);
const treeRef = ref();
const treeData = ref<DepartTreeNode[]>([]);
const checkedKeys = ref<{ checked: string[]; halfChecked: string[] }>({ checked: [], halfChecked: [] });
const selectedKeys = ref<string[]>([]);
const searchDepartValue = ref('');
const departType = ref<string | number>('');
const defaultExpandedKeys = ref<string[]>([]);
const checkAllAssignType = ref<number>(3);
const checkAllManageAssignType = ref<number>(3);
const checkAllBusinessAssignType = ref<number>(3);
const checkAllCurrentSubDepartAssignType = ref<number>(3);
const assignTypeList = [
{ value: 1, label: '办' },
{ value: 2, label: '协' },
{ value: 3, label: '阅' },
];
const selectedDeparts = computed(() => [...props.selectedMainDeparts, ...props.selectedAssistDeparts, ...props.selectedReadDeparts]);
const filterDepartTreeData = computed(() => {
if (!searchDepartValue.value) return treeData.value;
return filterDepartTree(treeData.value, searchDepartValue.value);
});
const manageDeparts = computed(() => {
return recurTree(treeData.value, (node) => {
if (String(node.departType) === '1' && !node.disableCheckbox) return [node];
return [];
});
});
const businessesDeparts = computed(() => {
return recurTree(treeData.value, (node) => {
if (String(node.departType) === '2' && !node.disableCheckbox) return [node];
return [];
});
});
const subDeparts = computed(() => {
return recurTree(treeData.value, (node) => {
if (String(node.departType) === '3' && !node.disableCheckbox) return [node];
return [];
});
});
const showCheckAll = computed(() => {
if (props.role === 1 || props.role === '1') {
return (manageDeparts.value.length + businessesDeparts.value.length > 0) && selectedDeparts.value.length < manageDeparts.value.length + businessesDeparts.value.length;
} else {
return (departType.value === 0 || departType.value === '0' || departType.value === 1 || departType.value === '1')
&& (manageDeparts.value.length + businessesDeparts.value.length > 0)
&& selectedDeparts.value.length < manageDeparts.value.length + businessesDeparts.value.length;
}
});
const showCancelCheckAll = computed(() => {
if (props.role === 1 || props.role === '1') {
return selectedDeparts.value.length > 0 && !showCheckAll.value;
} else {
return (departType.value === 0 || departType.value === '0' || departType.value === 1 || departType.value === '1')
&& selectedDeparts.value.length > 0 && !showCheckAll.value;
}
});
const showCheckAllMange = computed(() => {
if (props.role === 1 || props.role === '1') {
return manageDeparts.value.length > 0 && !manageDeparts.value.every((item) => selectedDeparts.value.some((i) => i.id === item.id));
} else {
return (departType.value === 0 || departType.value === '0' || departType.value === 1 || departType.value === '1')
&& manageDeparts.value.length > 0
&& !manageDeparts.value.every((item) => selectedDeparts.value.some((i) => i.id === item.id));
}
});
const showCancelCheckAllMange = computed(() => {
if (props.role === 1 || props.role === '1') {
return selectedDeparts.value.length > 0 && !showCheckAllMange.value;
} else {
return (departType.value === 0 || departType.value === '0' || departType.value === 1 || departType.value === '1')
&& selectedDeparts.value.length > 0 && !showCheckAllMange.value;
}
});
const showCheckAllBusiness = computed(() => {
if (props.role === 1 || props.role === '1') {
return businessesDeparts.value.length > 0 && !businessesDeparts.value.every((item) => selectedDeparts.value.some((i) => i.id === item.id));
} else {
return (departType.value === 0 || departType.value === '0' || departType.value === 1 || departType.value === '1')
&& businessesDeparts.value.length > 0
&& !businessesDeparts.value.every((item) => selectedDeparts.value.some((i) => i.id === item.id));
}
});
const showCancelCheckAllBusiness = computed(() => {
if (props.role === 1 || props.role === '1') {
return selectedDeparts.value.length > 0 && !showCheckAllBusiness.value;
} else {
return (departType.value === 0 || departType.value === '0' || departType.value === 1 || departType.value === '1')
&& selectedDeparts.value.length > 0 && !showCheckAllBusiness.value;
}
});
const showCheckAllCurrentSubDepart = computed(() => {
return (props.role === 2 || props.role === '2')
&& (departType.value === 2 || departType.value === '2' || departType.value === 3 || departType.value === '3')
&& subDeparts.value.length > 0
&& !subDeparts.value.every((item) => selectedDeparts.value.some((i) => i.id === item.id));
});
const showCancelCheckAllCurrentSubDepart = computed(() => {
return (props.role === 2 || props.role === '2')
&& (departType.value === 2 || departType.value === '2' || departType.value === 3 || departType.value === '3')
&& selectedDeparts.value.length > 0
&& !showCheckAllCurrentSubDepart.value;
});
watch(
selectedDeparts,
() => {
setDefaultCheckedKeys();
}
);
watch(
() => props.excludeDeparts,
() => {
loadDepart();
}
);
function emitChanges(payload: { mainDeparts?: DepartTreeNode[]; assistDeparts?: DepartTreeNode[]; readDeparts?: DepartTreeNode[] }) {
if (payload.mainDeparts) emit('update:selectedMainDeparts', payload.mainDeparts);
if (payload.assistDeparts) emit('update:selectedAssistDeparts', payload.assistDeparts);
if (payload.readDeparts) emit('update:selectedReadDeparts', payload.readDeparts);
}
function getFromInfo(treeKey: string, assignType: number) {
let fromArray: DepartTreeNode[] = [];
let fromKey: string;
if (assignType === 1) {
fromArray = [...props.selectedMainDeparts];
fromKey = 'mainDeparts';
} else if (assignType === 2) {
fromArray = [...props.selectedAssistDeparts];
fromKey = 'assistDeparts';
} else {
fromArray = [...props.selectedReadDeparts];
fromKey = 'readDeparts';
}
const fromIdx = fromArray.findIndex((item) => item.key === treeKey);
return { fromArray, fromIdx, fromKey };
}
function leaderTreeRender(tree: DepartTreeNode[]) {
let childDistributable = false;
let res: DepartTreeNode[] = [];
tree.forEach((item) => {
if (item.distributable === 1) {
childDistributable = true;
if (item.children) {
const r = leaderTreeRender(item.children);
if (r.res) {
item.children = r.res;
} else {
item.children = undefined;
}
}
res.push(item);
} else {
if (item.children) {
item.disableCheckbox = true;
const r = leaderTreeRender(item.children);
if (r.res && r.childDistributable) {
item.children = r.res;
res.push(item);
}
}
}
});
return { childDistributable, res };
}
function manageTreeRender(tree: DepartTreeNode[]) {
let childDistributable = false;
let res: DepartTreeNode[] = [];
tree.forEach((item) => {
if (item.distributable === 1 && item.id === props.departId) {
childDistributable = true;
item.disableCheckbox = true;
res.push(item);
} else if (item.distributable === 1 && (String(item.departType) === '1' || String(item.departType) === '2')) {
childDistributable = true;
if (item.children) {
const r = manageTreeRender(item.children);
if (r.res) {
item.children = r.res;
} else {
item.children = undefined;
}
}
res.push(item);
} else {
if (item.children) {
item.disableCheckbox = true;
const r = manageTreeRender(item.children);
if (r.res && r.childDistributable) {
item.children = r.res;
res.push(item);
}
}
}
});
return { childDistributable, res };
}
function businessesRender(tree: DepartTreeNode[]) {
let childDistributable = false;
let res: DepartTreeNode[] = [];
tree.forEach((item) => {
if (item.distributable === 1 && item.id === props.departId) {
childDistributable = true;
defaultExpandedKeys.value.push(item.key);
item.disableCheckbox = true;
res.push(item);
} else {
if (item.children) {
item.disableCheckbox = true;
const r = businessesRender(item.children);
if (r.res && r.childDistributable) {
item.children = r.res;
res.push(item);
}
}
}
});
return { childDistributable, res };
}
function subDepartsRender(tree: DepartTreeNode[]) {
let childDistributable = false;
let res: DepartTreeNode[] = [];
tree.forEach((item) => {
if (item.children && item.children.some((child) => child.id === props.departId)) {
childDistributable = true;
defaultExpandedKeys.value.push(item.key);
item.disableCheckbox = true;
res.push(item);
} else {
if (item.children) {
item.disableCheckbox = true;
const r = subDepartsRender(item.children);
if (r.res && r.childDistributable) {
item.children = r.res;
res.push(item);
}
}
}
});
return { childDistributable, res };
}
function excludeDepartsTree(tree: DepartTreeNode[]) {
const excludes = props.excludeDeparts;
tree.forEach((item) => {
if (excludes.includes(item.id)) {
item.disableCheckbox = true;
}
if (item.children) {
excludeDepartsTree(item.children);
}
});
}
function renderMainAssistRead() {
recurTree(treeData.value, (node) => {
selectedDeparts.value.some((item) => {
if (item.key === node.key) {
node.assignType = item.assignType;
return true;
}
});
});
}
async function loadDepart() {
try {
const res = await queryTreeList();
let arr = [...(res as any)];
recurTree(arr, (node) => {
if (node.distributable === undefined || node.distributable === null) {
node.distributable = 1;
}
if (node.id === props.departId) {
departType.value = node.departType;
}
});
if (props.role === 1 || props.role === '1') {
treeData.value = leaderTreeRender(arr).res;
} else {
if (departType.value === 0 || departType.value === '0') {
treeData.value = leaderTreeRender(arr).res;
} else if (departType.value === 1 || departType.value === '1') {
treeData.value = manageTreeRender(arr).res;
} else if (departType.value === 2 || departType.value === '2') {
treeData.value = businessesRender(arr).res;
} else if (departType.value === 3 || departType.value === '3') {
treeData.value = subDepartsRender(arr).res;
}
}
excludeDepartsTree(treeData.value);
setDefaultCheckedKeys();
treeData.value.forEach((item) => {
defaultExpandedKeys.value.push(item.key);
});
} catch (e) {
console.error('loadDepart error:', e);
}
}
function onDepartCheck(checkedKeysValue: any, e: { checked: boolean; checkedNodes: any[]; node: any; event: Event }) {
const node = e.node.dataRef || e.node;
node.assignType = 3;
let mainDeparts = [...props.selectedMainDeparts];
let assistDeparts = [...props.selectedAssistDeparts];
let readDeparts = [...props.selectedReadDeparts];
if (e.checked) {
if (node.assignType === 1) {
mainDeparts.push(node);
} else if (node.assignType === 2) {
assistDeparts.push(node);
} else {
readDeparts.push(node);
}
} else {
deleteIfExist(mainDeparts, node, 'id');
deleteIfExist(assistDeparts, node, 'id');
deleteIfExist(readDeparts, node, 'id');
}
emitChanges({ mainDeparts, assistDeparts, readDeparts });
}
function setDefaultCheckedKeys() {
checkedKeys.value = { checked: selectedDeparts.value.map((item) => item.key), halfChecked: [] };
renderMainAssistRead();
}
function toggleCheck(treeKey: string, assignType: number) {
const selectedNode = recurTree(treeData.value, (node) => {
if (node.id === treeKey) return [node];
return [];
});
let readDeparts = [...props.selectedReadDeparts];
if (checkedKeys.value.checked.includes(treeKey)) {
const { fromArray, fromIdx, fromKey } = getFromInfo(treeKey, assignType);
fromArray.splice(fromIdx, 1);
emitChanges({ [fromKey]: fromArray });
} else {
selectedNode[0].assignType = 3;
readDeparts.push(selectedNode[0]);
emitChanges({ readDeparts });
}
}
function checkAllDepart() {
const allDepart = recurTree(treeData.value, (node) => {
if (!node.disableCheckbox && (String(node.departType) === '1' || String(node.departType) === '2')) {
node.assignType = checkAllAssignType.value;
return [node];
}
return [];
});
let mainDeparts: DepartTreeNode[] = [];
let assistDeparts: DepartTreeNode[] = [];
let readDeparts: DepartTreeNode[] = [];
if (checkAllAssignType.value === 1) {
mainDeparts = allDepart;
} else if (checkAllAssignType.value === 2) {
assistDeparts = allDepart;
} else if (checkAllAssignType.value === 3) {
readDeparts = allDepart;
}
emitChanges({ mainDeparts, assistDeparts, readDeparts });
}
function uncheckAllDepart() {
emitChanges({ mainDeparts: [], assistDeparts: [], readDeparts: [] });
}
function checkAllManageDepart() {
const allManageDepart = recurTree(treeData.value, (node) => {
if (!node.disableCheckbox && String(node.departType) === '1') {
node.assignType = checkAllManageAssignType.value;
return [node];
}
return [];
});
let mainDeparts = [...props.selectedMainDeparts];
let assistDeparts = [...props.selectedAssistDeparts];
let readDeparts = [...props.selectedReadDeparts];
if (checkAllManageAssignType.value === 1) {
mainDeparts = allManageDepart.concat(props.selectedMainDeparts);
} else if (checkAllManageAssignType.value === 2) {
assistDeparts = allManageDepart.concat(props.selectedAssistDeparts);
} else if (checkAllManageAssignType.value === 3) {
readDeparts = allManageDepart.concat(props.selectedReadDeparts);
}
emitChanges({ mainDeparts, assistDeparts, readDeparts });
}
function uncheckAllManageDepart() {
const allManageDepart = recurTree(treeData.value, (node) => {
if (String(node.departType) === '1') return [node];
return [];
});
const mainDeparts = props.selectedMainDeparts.filter((item) => !allManageDepart.some((manage) => item.id === manage.id));
const assistDeparts = props.selectedAssistDeparts.filter((item) => !allManageDepart.some((manage) => item.id === manage.id));
const readDeparts = props.selectedReadDeparts.filter((item) => !allManageDepart.some((manage) => item.id === manage.id));
emitChanges({ mainDeparts, assistDeparts, readDeparts });
}
function checkAllBusinessesDepart() {
const allBusinessesDepart = recurTree(treeData.value, (node) => {
if (!node.disableCheckbox && String(node.departType) === '2') {
node.assignType = checkAllBusinessAssignType.value;
return [node];
}
return [];
});
let mainDeparts = [...props.selectedMainDeparts];
let assistDeparts = [...props.selectedAssistDeparts];
let readDeparts = [...props.selectedReadDeparts];
if (checkAllBusinessAssignType.value === 1) {
mainDeparts = allBusinessesDepart.concat(props.selectedMainDeparts);
} else if (checkAllBusinessAssignType.value === 2) {
assistDeparts = allBusinessesDepart.concat(props.selectedAssistDeparts);
} else if (checkAllBusinessAssignType.value === 3) {
readDeparts = allBusinessesDepart.concat(props.selectedReadDeparts);
}
emitChanges({ mainDeparts, assistDeparts, readDeparts });
}
function uncheckAllBusinessesDepart() {
const allBusinessesDepart = recurTree(treeData.value, (node) => {
if (String(node.departType) === '2') return [node];
return [];
});
const mainDeparts = props.selectedMainDeparts.filter((item) => !allBusinessesDepart.some((manage) => item.id === manage.id));
const assistDeparts = props.selectedAssistDeparts.filter((item) => !allBusinessesDepart.some((manage) => item.id === manage.id));
const readDeparts = props.selectedReadDeparts.filter((item) => !allBusinessesDepart.some((manage) => item.id === manage.id));
emitChanges({ mainDeparts, assistDeparts, readDeparts });
}
function checkAllCurrentSubDepart() {
const allSubDepart = recurTree(treeData.value, (node) => {
if (!node.disableCheckbox && String(node.departType) === '3') {
node.assignType = checkAllCurrentSubDepartAssignType.value;
return [node];
}
return [];
});
let mainDeparts = [...props.selectedMainDeparts];
let assistDeparts = [...props.selectedAssistDeparts];
let readDeparts = [...props.selectedReadDeparts];
if (checkAllCurrentSubDepartAssignType.value === 1) {
mainDeparts = allSubDepart.concat(props.selectedMainDeparts);
} else if (checkAllCurrentSubDepartAssignType.value === 2) {
assistDeparts = allSubDepart.concat(props.selectedAssistDeparts);
} else if (checkAllCurrentSubDepartAssignType.value === 3) {
readDeparts = allSubDepart.concat(props.selectedReadDeparts);
}
emitChanges({ mainDeparts, assistDeparts, readDeparts });
}
function uncheckAllCurrentSubDepart() {
const allCurrentSubDepart = recurTree(treeData.value, (node) => {
if (String(node.departType) === '3') return [node];
return [];
});
const mainDeparts = props.selectedMainDeparts.filter((item) => !allCurrentSubDepart.some((manage) => item.id === manage.id));
const assistDeparts = props.selectedAssistDeparts.filter((item) => !allCurrentSubDepart.some((manage) => item.id === manage.id));
const readDeparts = props.selectedReadDeparts.filter((item) => !allCurrentSubDepart.some((manage) => item.id === manage.id));
emitChanges({ mainDeparts, assistDeparts, readDeparts });
}
function checkMainDepart(treeKey: string, assignType: number) {
const selected = props.selectedMainDeparts.some((item) => item.key === treeKey);
if (!selected) {
const { fromArray, fromIdx, fromKey } = getFromInfo(treeKey, assignType);
const toArray = [...props.selectedMainDeparts];
recurTree(treeData.value, (node) => {
if (node.key === treeKey) {
node.assignType = 1;
toArray.push(node);
fromArray.splice(fromIdx, 1);
}
});
emitChanges({ [fromKey]: fromArray, mainDeparts: toArray });
}
}
function checkAssistDepart(treeKey: string, assignType: number) {
const selected = props.selectedAssistDeparts.some((item) => item.key === treeKey);
if (!selected) {
const { fromArray, fromIdx, fromKey } = getFromInfo(treeKey, assignType);
const toArray = [...props.selectedAssistDeparts];
recurTree(treeData.value, (node) => {
if (node.key === treeKey) {
node.assignType = 2;
toArray.push(node);
fromArray.splice(fromIdx, 1);
}
});
emitChanges({ [fromKey]: fromArray, assistDeparts: toArray });
}
}
function checkReadDepart(treeKey: string, assignType: number) {
const selected = props.selectedReadDeparts.some((item) => item.key === treeKey);
if (!selected) {
const { fromArray, fromIdx, fromKey } = getFromInfo(treeKey, assignType);
const toArray = [...props.selectedReadDeparts];
recurTree(treeData.value, (node) => {
if (node.key === treeKey) {
node.assignType = 3;
toArray.push(node);
fromArray.splice(fromIdx, 1);
}
});
emitChanges({ [fromKey]: fromArray, readDeparts: toArray });
}
}
onMounted(() => {
loadDepart();
});
</script>
<style lang="less">
.my-dept-select-tree {
height: 470px;
&.fullscreen {
height: calc(100vh - 250px);
}
overflow-y: auto;
}
</style>
<style lang="less" scoped>
.btn-red {
background-color: #ff4d4f;
color: #fff;
border-color: #ff4d4f;
}
.btn-blue {
background-color: #1890ff;
color: #fff;
border-color: #1890ff;
}
.btn-green {
background-color: #52c41a;
color: #fff;
border-color: #52c41a;
}
</style>
@@ -0,0 +1,411 @@
<template>
<div>
<a-row>
<a-col :md="12" :sm="24">
<div style="height: 100%; border-right: 1px solid #e8e8e8; padding-right: 16px; margin-right: 16px">
<a-input-search v-model:value="searchDepartValue" style="margin-bottom: 16px" placeholder="请输入部门名称" />
<a-directory-tree
selectable
v-model:selectedKeys="selectedDepIds"
:check-strictly="true"
:dropdown-style="{ maxHeight: '200px', overflow: 'auto' }"
:tree-data="filterDepartTreeData"
:expand-action="false"
v-model:expandedKeys="expandedKeys"
@select="onDepSelect"
style="height: 470px; overflow: auto"
/>
</div>
</a-col>
<a-col :md="12" :sm="24">
<a-input-search
:style="{ width: '100px', marginBottom: '15px' }"
placeholder="输入姓名"
v-model:value="queryParam.realname"
@search="onSearch"
/>
<a-button @click="searchReset(1)" style="margin-left: 8px">重置</a-button>
<a-button type="primary" @click="selectAll" style="margin-left: 8px">全选</a-button>
<a-button type="danger" @click="unselectAll" style="margin-left: 8px">清空</a-button>
<a-empty v-if="selectedDepIds.length === 0" description="请先选择部门!" />
<a-table
v-else
:scroll="{ y: 460, ...scrollTrigger }"
size="middle"
row-key="id"
:columns="columns"
:data-source="dataSource"
:pagination="ipagination"
:row-selection="{ selectedRowKeys: selectedRowKeys, onSelectAll: onSelectAll, onSelect: onSelect, type: getType }"
:loading="loading"
@change="handleTableChange"
:custom-row="customRow"
/>
</a-col>
</a-row>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, watch, onMounted } from 'vue';
import { queryTreeList } from '/@/api/common/api';
import { defHttp } from '/@/utils/http/axios';
import { pushIfNotExist, filterObj } from '/@/utils/common/compUtils';
import { deleteIfExist, recurTree, filterDepartTree } from './SzDistribute.api';
import type { UserItem, DepartTreeNode } from './SzDistribute.api';
const props = withDefaults(
defineProps<{
secLevel: any;
checkedUser?: UserItem[];
selectedDepId?: string;
departId: string;
role: string | number;
isSpecial: number;
multi?: boolean;
}>(),
{
checkedUser: () => [],
selectedDepId: '',
multi: true,
}
);
const emit = defineEmits(['update:checkedUser', 'update:selectedDepId']);
const searchDepartValue = ref('');
const queryParam = reactive<Record<string, any>>({ realname: '' });
const dataSource = ref<UserItem[]>([]);
const allUserList = ref<UserItem[]>([]);
const selectedDepIds = ref<string[]>([]);
const selectedRows = ref<UserItem[]>([]);
const departTree = ref<DepartTreeNode[]>([]);
const fullDepartTree = ref<DepartTreeNode[]>([]);
const loading = ref(false);
const expandedKeys = ref<string[]>([]);
const scrollTrigger = ref<Record<string, any>>({});
const isTreeLoaded = ref(false);
function findDepartIdByOrgCode(tree: DepartTreeNode[], orgCode: string): string | null {
for (const node of tree) {
if (node.orgCode === orgCode) {
return node.id;
}
if (node.children && node.children.length > 0) {
const found = findDepartIdByOrgCode(node.children, orgCode);
if (found) return found;
}
}
return null;
}
function resolveDepartId(departIdOrOrgCode: string): string {
if (!departIdOrOrgCode) return departIdOrOrgCode;
const uuidPattern = /^[0-9a-f]{32}$/i;
if (uuidPattern.test(departIdOrOrgCode.replace(/-/g, ''))) return departIdOrOrgCode;
const realId = findDepartIdByOrgCode(fullDepartTree.value, departIdOrOrgCode);
if (realId) {
console.log(`resolveDepartId: "${departIdOrOrgCode}" (orgCode) -> "${realId}" (UUID)`);
return realId;
}
console.warn(`resolveDepartId: cannot find UUID for orgCode "${departIdOrOrgCode}", using as-is`);
return departIdOrOrgCode;
}
const columns = [
{ title: '用户姓名', align: 'center' as const, dataIndex: 'realname' },
{ title: '人员密级', align: 'center' as const, dataIndex: 'userSecurityLevel_dictText' },
];
const ipagination = reactive({
current: 1,
pageSize: 7,
showQuickJumper: true,
total: 0,
});
const isorter = reactive({
column: 'createTime',
order: 'desc',
});
const url = {
pageUserDepart: '/sys/user/queryUserComponentData',
listUserDepart: '/sys/user/queryUserComponentData',
};
const getType = computed(() => (props.multi ? 'checkbox' : 'radio'));
const selectedRowKeys = computed(() => selectedRows.value.map((item) => item.id));
const filterDepartTreeData = computed(() => {
if (!searchDepartValue.value) return departTree.value;
return filterDepartTree(departTree.value, searchDepartValue.value);
});
watch(
() => props.checkedUser,
() => {
selectedRows.value = [...(props.checkedUser || [])];
}
);
watch(
() => props.departId,
() => {
queryDepartTree();
},
{ immediate: true }
);
watch(
() => props.role,
() => {
queryDepartTree();
}
);
watch(
() => props.secLevel,
() => {
if (selectedDepIds.value.length > 0) {
getAllUsersInDepart();
loadData();
}
}
);
watch(
() => props.selectedDepId,
(newVal) => {
if (newVal && (props.role === 1 || props.role === '1')) {
selectedDepIds.value = [resolveDepartId(newVal)];
getAllUsersInDepart();
loadData();
}
}
);
function initData() {
if (props.role === 1 || props.role === '1') {
const depId = props.selectedDepId || props.departId;
if (depId) {
selectedDepIds.value = [resolveDepartId(depId)];
getAllUsersInDepart();
loadData();
}
} else {
const resolvedId = resolveDepartId(props.departId);
selectedDepIds.value = [resolvedId];
getAllUsersInDepart();
loadData();
}
if (props.checkedUser) {
selectedRows.value = [...props.checkedUser];
}
}
async function loadData(arg?: number) {
if (selectedDepIds.value.length > 0) {
if (arg === 1) {
ipagination.current = 1;
}
const params = getQueryParams();
loading.value = true;
try {
const res: any = await defHttp.get({ url: url.pageUserDepart, params });
dataSource.value = (res.records || []).filter((user: UserItem) => {
const userLevel = user.userSecurityLevel;
const docLevel = props.secLevel;
return userLevel !== null && userLevel > docLevel;
});
ipagination.total = res.total;
} finally {
loading.value = false;
}
}
}
function getQueryParams() {
const param = Object.assign({}, queryParam, isorter);
param.searchSecurityLevel = props.secLevel;
param.field = getQueryField();
param.pageNo = ipagination.current;
param.pageSize = ipagination.pageSize;
param.departId = selectedDepIds.value.join(',');
param.isSpecial = props.isSpecial;
return filterObj(param);
}
function getQueryField() {
let str = 'id,';
for (let a = 0; a < columns.length; a++) {
str += ',' + columns[a].dataIndex;
}
return str;
}
function searchReset(num?: number) {
if (num !== 0) {
queryParam.realname = '';
loadData(1);
}
}
function handleTableChange(pagination: any, _filters: any, sorter: any) {
if (Object.keys(sorter).length > 0) {
isorter.column = sorter.field;
isorter.order = sorter.order === 'ascend' ? 'asc' : 'desc';
}
Object.assign(ipagination, pagination);
loadData();
}
async function getAllUsersInDepart() {
if (selectedDepIds.value.length === 0 || (selectedDepIds.value.length === 1 && !selectedDepIds.value[0])) {
console.warn('getAllUsersInDepart: selectedDepIds is empty, skip loading');
allUserList.value = [];
return;
}
const param = Object.assign({}, queryParam);
param.searchSecurityLevel = props.secLevel;
param.field = getQueryField();
param.departId = selectedDepIds.value.join(',');
param.isSpecial = props.isSpecial;
param.pageNo = 1;
param.pageSize = 9999;
const filteredParam = filterObj(param);
try {
const res: any = await defHttp.get({ url: url.listUserDepart, params: filteredParam });
allUserList.value = (res.records || res).filter((user: UserItem) => {
const userLevel = user.userSecurityLevel;
const docLevel = props.secLevel;
return userLevel !== null && userLevel > docLevel;
});
} catch (e) {
console.error('getAllUsersInDepart error:', e);
allUserList.value = [];
}
}
function onDepSelect(keys: string[]) {
if (keys.length > 0 && keys[0] != null) {
selectedDepIds.value = [keys[0]];
emit('update:selectedDepId', keys[0]);
loadData(1);
getAllUsersInDepart();
}
}
function onSelect(record: UserItem, selected: boolean) {
if (selected) {
pushIfNotExist(selectedRows.value, record, 'id');
} else {
deleteIfExist(selectedRows.value, record, 'id');
}
selectedRows.value.sort((a, b) => (a.sortno || 0) - (b.sortno || 0));
emit('update:checkedUser', selectedRows.value);
}
function onSelectAll(selected: boolean, selectedRecords: UserItem[], changeRecords: UserItem[]) {
if (selected) {
selectedRecords.forEach((item) => {
pushIfNotExist(selectedRows.value, item, 'id');
});
} else {
const rowsToRemove = changeRecords && changeRecords.length > 0 ? changeRecords : dataSource.value;
rowsToRemove.forEach((item) => {
deleteIfExist(selectedRows.value, item, 'id');
});
}
selectedRows.value.sort((a, b) => (a.sortno || 0) - (b.sortno || 0));
emit('update:checkedUser', selectedRows.value);
}
function onSearch() {
loadData(1);
}
async function queryDepartTree() {
expandedKeys.value = [];
departTree.value = [];
try {
const res: any = await queryTreeList();
const arr = [...res];
const departId = props.departId;
fullDepartTree.value = [...arr];
if (props.role === 1 || props.role === '1') {
departTree.value = [...arr];
} else {
const resolvedDepartId = resolveDepartId(departId);
const tree = recurTree(arr, (node) => {
if (node.id === resolvedDepartId) return [node];
return [];
});
departTree.value = [...tree];
}
departTree.value.forEach((item) => {
expandedKeys.value.push(item.key);
});
isTreeLoaded.value = true;
await initData();
} catch (e) {
console.error('queryDepartTree error:', e);
}
}
function selectAll() {
allUserList.value.forEach((item) => {
pushIfNotExist(selectedRows.value, item, 'id');
});
selectedRows.value.sort((a, b) => (a.sortno || 0) - (b.sortno || 0));
emit('update:checkedUser', selectedRows.value);
}
function unselectAll() {
allUserList.value.forEach((item) => {
deleteIfExist(selectedRows.value, item, 'id');
});
emit('update:checkedUser', selectedRows.value);
}
function customRow(record: UserItem) {
return {
onClick: (_event: Event) => {
const clickRowKey = record.id;
if (selectedRowKeys.value.includes(clickRowKey)) {
deleteIfExist(selectedRows.value, record, 'id');
} else {
pushIfNotExist(selectedRows.value, record, 'id');
}
selectedRows.value.sort((a, b) => (a.sortno || 0) - (b.sortno || 0));
emit('update:checkedUser', selectedRows.value);
},
};
}
onMounted(async () => {
resetScreenSize();
await queryDepartTree();
});
function resetScreenSize() {
const screenWidth = document.body.clientWidth;
if (screenWidth < 500) {
scrollTrigger.value = { x: 800 };
} else {
scrollTrigger.value = {};
}
}
</script>
<style scoped>
.ant-table-tbody .ant-table-row td {
padding-top: 10px;
padding-bottom: 10px;
}
</style>
@@ -0,0 +1,341 @@
<template>
<div style="margin-left: 1px">
<a-empty v-if="!haveSelected" />
<div v-else>
<a-button-group v-for="(depart, index) in selectedMainDeparts" :key="'main-' + depart.id + '-' + index" class="small-space selected-depart">
<a-dropdown>
<template v-if="!disabled" #overlay>
<a-menu @click="handMainDepartAssignType($event, index)">
<a-menu-item key="1"></a-menu-item>
<a-menu-item key="2"></a-menu-item>
<a-menu-item key="3"></a-menu-item>
</a-menu>
</template>
<a-button class="btn-red" size="small">
<ApartmentOutlined />
</a-button>
</a-dropdown>
<a-popover trigger="click">
<template #content>
<a-descriptions title="部门信息" :column="1" style="width: 300px">
<a-descriptions-item label="部门名">{{ depart.title }}</a-descriptions-item>
<a-descriptions-item label="是否有子部门">{{ depart.children ? '是' : '否' }}</a-descriptions-item>
<a-descriptions-item label="部门描述">{{ depart.description }}</a-descriptions-item>
</a-descriptions>
</template>
<a-button class="btn-red" size="small">{{ depart.title }}</a-button>
</a-popover>
<a-button v-if="!disabled" class="btn-red" size="small" @click="deleteSelectedMainDepart(index)">
<CloseOutlined />
</a-button>
</a-button-group>
<a-button-group v-for="(depart, index) in selectedAssistDeparts" :key="'assist-' + depart.id + '-' + index" class="small-space selected-depart">
<a-dropdown>
<template v-if="!disabled" #overlay>
<a-menu @click="handAssistDepartAssignType($event, index)">
<a-menu-item key="1"></a-menu-item>
<a-menu-item key="2"></a-menu-item>
<a-menu-item key="3"></a-menu-item>
</a-menu>
</template>
<a-button class="btn-blue" size="small">
<ApartmentOutlined />
</a-button>
</a-dropdown>
<a-popover trigger="click">
<template #content>
<a-descriptions title="部门信息" :column="1" style="width: 300px">
<a-descriptions-item label="部门名">{{ depart.title }}</a-descriptions-item>
<a-descriptions-item label="是否有子部门">{{ depart.children ? '是' : '否' }}</a-descriptions-item>
<a-descriptions-item label="部门描述">{{ depart.description }}</a-descriptions-item>
</a-descriptions>
</template>
<a-button class="btn-blue" size="small">{{ depart.title }}</a-button>
</a-popover>
<a-button v-if="!disabled" class="btn-blue" size="small" @click="deleteSelectedAssistDepart(index)">
<CloseOutlined />
</a-button>
</a-button-group>
<a-button-group v-for="(depart, index) in selectedReadDeparts" :key="'read-' + depart.id + '-' + index" class="small-space selected-depart">
<a-dropdown>
<template v-if="!disabled" #overlay>
<a-menu @click="handReadDepartAssignType($event, index)">
<a-menu-item key="1"></a-menu-item>
<a-menu-item key="2"></a-menu-item>
<a-menu-item key="3"></a-menu-item>
</a-menu>
</template>
<a-button class="btn-green" size="small">
<ApartmentOutlined />
</a-button>
</a-dropdown>
<a-popover trigger="click">
<template #content>
<a-descriptions title="部门信息" :column="1" style="width: 300px">
<a-descriptions-item label="部门名">{{ depart.title }}</a-descriptions-item>
<a-descriptions-item label="是否有子部门">{{ depart.children ? '是' : '否' }}</a-descriptions-item>
<a-descriptions-item label="部门描述">{{ depart.description }}</a-descriptions-item>
</a-descriptions>
</template>
<a-button class="btn-green" size="small">{{ depart.title }}</a-button>
</a-popover>
<a-button v-if="!disabled" class="btn-green" size="small" @click="deleteSelectedReadDepart(index)">
<CloseOutlined />
</a-button>
</a-button-group>
<a-button-group v-for="(user, index) in selectedUsers" :key="'user-' + user.id + '-' + index" class="small-space selected-user-do">
<a-dropdown>
<template v-if="!disabled" #overlay>
<a-menu @click="handUserAssignType($event, index)">
<a-menu-item key="1"></a-menu-item>
<a-menu-item key="3"></a-menu-item>
</a-menu>
</template>
<a-button class="btn-red" size="small">
<UserOutlined />
</a-button>
</a-dropdown>
<a-popover trigger="click">
<template #content>
<a-descriptions title="人员信息" :column="1" style="width: 300px">
<a-descriptions-item label="姓名">{{ user.realname }}</a-descriptions-item>
<a-descriptions-item label="性别">{{ user.sex_dictText }}</a-descriptions-item>
<a-descriptions-item label="密级">{{ user.userSecurityLevel_dictText }}</a-descriptions-item>
<a-descriptions-item label="所属部门">{{ user.orgCodeTxt }}</a-descriptions-item>
</a-descriptions>
</template>
<a-button class="btn-red" size="small">{{ user.realname }}</a-button>
</a-popover>
<a-button v-if="!disabled" class="btn-red" size="small" @click="deleteSelectedUsers(index)">
<CloseOutlined />
</a-button>
</a-button-group>
<a-button-group v-for="(user, index) in selectedReadUsers" :key="'readuser-' + user.id + '-' + index" class="small-space selected-user-do">
<a-dropdown>
<template v-if="!disabled" #overlay>
<a-menu @click="handReadUserAssignType($event, index)">
<a-menu-item key="1"></a-menu-item>
<a-menu-item key="3"></a-menu-item>
</a-menu>
</template>
<a-button class="btn-green" size="small">
<UserOutlined />
</a-button>
</a-dropdown>
<a-popover trigger="click">
<template #content>
<a-descriptions title="人员信息" :column="1" style="width: 300px">
<a-descriptions-item label="姓名">{{ user.realname }}</a-descriptions-item>
<a-descriptions-item label="性别">{{ user.sex_dictText }}</a-descriptions-item>
<a-descriptions-item label="密级">{{ user.userSecurityLevel_dictText }}</a-descriptions-item>
<a-descriptions-item label="所属部门">{{ user.orgCodeTxt }}</a-descriptions-item>
</a-descriptions>
</template>
<a-button class="btn-green" size="small">{{ user.realname }}</a-button>
</a-popover>
<a-button v-if="!disabled" class="btn-green" size="small" @click="deleteSelectedReadUser(index)">
<CloseOutlined />
</a-button>
</a-button-group>
</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { ApartmentOutlined, UserOutlined, CloseOutlined } from '@ant-design/icons-vue';
import type { DepartTreeNode, UserItem } from './SzDistribute.api';
const props = withDefaults(
defineProps<{
disabled?: boolean;
selectedMainDeparts: DepartTreeNode[];
selectedAssistDeparts: DepartTreeNode[];
selectedReadDeparts: DepartTreeNode[];
selectedUsers: UserItem[];
selectedReadUsers: UserItem[];
}>(),
{
disabled: false,
selectedMainDeparts: () => [],
selectedAssistDeparts: () => [],
selectedReadDeparts: () => [],
selectedUsers: () => [],
selectedReadUsers: () => [],
}
);
const emit = defineEmits([
'update:selectedMainDeparts',
'update:selectedAssistDeparts',
'update:selectedReadDeparts',
'update:selectedUsers',
'update:selectedReadUsers',
]);
const haveSelected = computed(() => {
return !(
props.selectedMainDeparts.length === 0 &&
props.selectedAssistDeparts.length === 0 &&
props.selectedReadDeparts.length === 0 &&
props.selectedUsers.length === 0 &&
props.selectedReadUsers.length === 0
);
});
function emitChanges(payload: {
mainDeparts?: DepartTreeNode[];
assistDeparts?: DepartTreeNode[];
readDeparts?: DepartTreeNode[];
users?: UserItem[];
readUsers?: UserItem[];
}) {
if (payload.mainDeparts) emit('update:selectedMainDeparts', payload.mainDeparts);
if (payload.assistDeparts) emit('update:selectedAssistDeparts', payload.assistDeparts);
if (payload.readDeparts) emit('update:selectedReadDeparts', payload.readDeparts);
if (payload.users) emit('update:selectedUsers', payload.users);
if (payload.readUsers) emit('update:selectedReadUsers', payload.readUsers);
}
function handMainDepartAssignType(e: { key: string }, index: number) {
if (e.key === '2') {
const fromArray = [...props.selectedMainDeparts];
const targetArray = [...props.selectedAssistDeparts];
fromArray[index].assignType = 2;
targetArray.push(fromArray[index]);
fromArray.splice(index, 1);
emitChanges({ mainDeparts: fromArray, assistDeparts: targetArray });
} else if (e.key === '3') {
const fromArray = [...props.selectedMainDeparts];
const targetArray = [...props.selectedReadDeparts];
fromArray[index].assignType = 3;
targetArray.push(fromArray[index]);
fromArray.splice(index, 1);
emitChanges({ mainDeparts: fromArray, readDeparts: targetArray });
}
}
function handAssistDepartAssignType(e: { key: string }, index: number) {
if (e.key === '1') {
const fromArray = [...props.selectedAssistDeparts];
const targetArray = [...props.selectedMainDeparts];
fromArray[index].assignType = 1;
targetArray.push(fromArray[index]);
fromArray.splice(index, 1);
emitChanges({ assistDeparts: fromArray, mainDeparts: targetArray });
} else if (e.key === '3') {
const fromArray = [...props.selectedAssistDeparts];
const targetArray = [...props.selectedReadDeparts];
fromArray[index].assignType = 3;
targetArray.push(fromArray[index]);
fromArray.splice(index, 1);
emitChanges({ assistDeparts: fromArray, readDeparts: targetArray });
}
}
function handReadDepartAssignType(e: { key: string }, index: number) {
if (e.key === '1') {
const fromArray = [...props.selectedReadDeparts];
const targetArray = [...props.selectedMainDeparts];
fromArray[index].assignType = 1;
targetArray.push(fromArray[index]);
fromArray.splice(index, 1);
emitChanges({ readDeparts: fromArray, mainDeparts: targetArray });
} else if (e.key === '2') {
const fromArray = [...props.selectedReadDeparts];
const targetArray = [...props.selectedAssistDeparts];
fromArray[index].assignType = 2;
targetArray.push(fromArray[index]);
fromArray.splice(index, 1);
emitChanges({ readDeparts: fromArray, assistDeparts: targetArray });
}
}
function handUserAssignType(e: { key: string }, index: number) {
if (e.key === '3') {
const fromArray = [...props.selectedUsers];
const targetArray = [...props.selectedReadUsers];
fromArray[index].assignType = 3;
targetArray.push(fromArray[index]);
fromArray.splice(index, 1);
emitChanges({ users: fromArray, readUsers: targetArray });
}
}
function handReadUserAssignType(e: { key: string }, index: number) {
if (e.key === '1') {
const fromArray = [...props.selectedReadUsers];
const targetArray = [...props.selectedUsers];
fromArray[index].assignType = 1;
targetArray.push(fromArray[index]);
fromArray.splice(index, 1);
emitChanges({ readUsers: fromArray, users: targetArray });
}
}
function deleteSelectedMainDepart(index: number) {
const changeArray = [...props.selectedMainDeparts];
changeArray.splice(index, 1);
emitChanges({ mainDeparts: changeArray });
}
function deleteSelectedAssistDepart(index: number) {
const changeArray = [...props.selectedAssistDeparts];
changeArray.splice(index, 1);
emitChanges({ assistDeparts: changeArray });
}
function deleteSelectedReadDepart(index: number) {
const changeArray = [...props.selectedReadDeparts];
changeArray.splice(index, 1);
emitChanges({ readDeparts: changeArray });
}
function deleteSelectedUsers(index: number) {
const changeArray = [...props.selectedUsers];
changeArray.splice(index, 1);
emitChanges({ users: changeArray });
}
function deleteSelectedReadUser(index: number) {
const changeArray = [...props.selectedReadUsers];
changeArray.splice(index, 1);
emitChanges({ readUsers: changeArray });
}
</script>
<style lang="less" scoped>
.small-space {
margin: 2px;
}
.selected-depart {
margin-bottom: 2px;
}
.selected-user-do {
margin-bottom: 2px;
}
.btn-red {
background-color: #ff4d4f;
color: #fff;
border-color: #ff4d4f;
}
.btn-blue {
background-color: #1890ff;
color: #fff;
border-color: #1890ff;
}
.btn-green {
background-color: #52c41a;
color: #fff;
border-color: #52c41a;
}
</style>
@@ -0,0 +1,290 @@
# SzDistribute 分发方案组件 使用说明
## 组件概述
SzDistribute 是一个公文分发方案组件,支持按部门(主办/协办/阅文)和人员(办文/阅文)进行分发配置,并提供方案保存/加载功能。
基于 JeecgBoot 3.8.0 + Vue 3 + TypeScript 开发。
---
## 文件结构
```
distributeComponent/
├── SzDistribute.vue # 主入口组件(分发方案弹窗)
├── SzSelectDepart.vue # 部门选择组件(树形多选 + 办/协/阅标记)
├── SzSelectUser.vue # 人员选择组件(部门树 + 人员表格)
├── SzSelectedDepartsAndUsers.vue # 已选部门和人员展示组件
├── SzDistribute.api.ts # 方案增删改查 API
├── types.ts # TypeScript 类型定义
├── utils.ts # 工具函数
└── USAGE.md # 本文件
```
---
## 快速开始
### 基础用法
```vue
<template>
<SzDistribute
title="分发方案"
:secLevel="secLevel"
:role="role"
:departId="departId"
:docId="docId"
:isSpecial="isSpecial"
v-model:checkedMainDepartIds="checkedMainDepartIds"
v-model:checkedAssistDepartIds="checkedAssistDepartIds"
v-model:checkedReadDepartIds="checkedReadDepartIds"
v-model:checkedUsernames="checkedUsernames"
v-model:checkedReadUsernames="checkedReadUsernames"
v-model:reasons="reasons"
@change="handleDistributeChange"
/>
</template>
<script lang="ts" setup>
import SzDistribute from '/@/components/semri/distributeComponent/SzDistribute.vue';
const secLevel = 3; // 密级:1=非密, 2=内部, 3=秘密, 4=机密
const role = 1; // 角色:1=领导, 2=部门负责人
const departId = 'A01'; // 当前用户部门(支持 orgCode 或 UUID
const docId = ''; // 公文ID(可选,用于排除已分发部门)
const isSpecial = 0; // 是否特殊分发
const checkedMainDepartIds = ref('');
const checkedAssistDepartIds = ref('');
const checkedReadDepartIds = ref('');
const checkedUsernames = ref('');
const checkedReadUsernames = ref('');
const reasons = ref('');
</script>
```
---
## SzDistribute 主组件 API
### Props(输入)
| 属性 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `secLevel` | `Number` | 是 | - | 文件密级。1=非密, 2=内部, 3=秘密, 4=机密 |
| `role` | `Number \| String` | 是 | `1` | 当前用户角色。1=领导, 2=部门负责人 |
| `departId` | `String` | 是 | `''` | 当前用户部门ID。支持 orgCode(如 `"A01"`)或 UUID 格式,组件内部自动转换 |
| `docId` | `String` | 否 | `''` | 公文ID。传入后自动排除该公文已分发的部门 |
| `isSpecial` | `Number` | 否 | `0` | 是否特殊分发。0=否, 1=是 |
| `title` | `String` | 否 | `'分发方案'` | 弹窗标题(会自动追加密级文字) |
| `width` | `Number \| String` | 否 | `1500` | 弹窗宽度(px |
| `showButton` | `Boolean` | 否 | `true` | 是否显示"分发对象"触发按钮。设为 `false` 时需通过 ref 调用 `showModal()` |
### v-model(双向绑定)
| 属性 | 类型 | 说明 |
|------|------|------|
| `checkedMainDepartIds` | `String` | 主办部门ID列表,逗号分隔,如 `"id1,id2,id3"` |
| `checkedAssistDepartIds` | `String` | 协办部门ID列表,逗号分隔 |
| `checkedReadDepartIds` | `String` | 阅文部门ID列表,逗号分隔 |
| `checkedUsernames` | `String` | 办文人员用户名列表,逗号分隔,如 `"zhangsan,lisi"` |
| `checkedReadUsernames` | `String` | 阅文人员用户名列表,逗号分隔 |
| `reasons` | `String` | 分发意见/备注文本 |
| `modelValue` | `String` | 完整分发数据字符串(内部格式) |
### Events(事件)
| 事件 | 参数 | 说明 |
|------|------|------|
| `change` | `distributeStr: string` | 分发方案确认时触发 |
| `update:checkedDepartIds` | `ids: string` | 所有已选部门ID(逗号分隔) |
| `update:checkedDeparts` | `DepartTreeNode[]` | 所有已选部门节点对象数组 |
### Methods(通过 ref 调用)
```typescript
const distributeRef = ref();
// 手动打开分发弹窗
distributeRef.value.showModal();
```
---
## 角色权限说明
### 领导(role=1
- 部门选择 Tab:显示全部部门树,可自由选择主办/协办/阅文
- 办文人员 Tab:显示全部部门树,选择部门后加载该部门人员
- 阅文人员 Tab:同办文人员
- 默认激活 Tab:选择部门
### 部门负责人(role=2
- 部门选择 Tab:根据当前部门的类型(职能/业务/科室)显示可见部门
- 办文人员 Tab:只显示本部门及下级部门的人员
- 阅文人员 Tab:同办文人员
- 默认激活 Tab:选择阅文人员(因部门负责人通常负责阅文分发)
---
## 子组件说明
### SzSelectDepart — 部门选择
树形结构展示部门,每个选中的部门可标记为「办」「协」「阅」三种类型。
**Props:**
| 属性 | 类型 | 说明 |
|------|------|------|
| `selectedMainDeparts` | `DepartTreeNode[]` | 当前主办部门列表 |
| `selectedAssistDeparts` | `DepartTreeNode[]` | 当前协办部门列表 |
| `selectedReadDeparts` | `DepartTreeNode[]` | 当前阅文部门列表 |
| `excludeDeparts` | `string[]` | 需排除的部门ID列表 |
| `departId` | `string` | 当前用户部门ID |
| `role` | `string \| number` | 用户角色 |
**Events:** `update:selectedMainDeparts`, `update:selectedAssistDeparts`, `update:selectedReadDeparts`
**快捷操作:**
- 全选部门 / 全选职能部门 / 全选业务部门 / 全选本部门科室
- 每种全选可指定办/协/阅类型
- 右键节点文字可快速切换选中状态
### SzSelectUser — 人员选择
左侧部门树 + 右侧人员表格,支持搜索、分页、全选。
**Props:**
| 属性 | 类型 | 说明 |
|------|------|------|
| `secLevel` | `any` | 文件密级,用于过滤人员(只显示密级 > 文件密级的人员) |
| `checkedUser` | `UserItem[]` | 已选中的人员列表 |
| `selectedDepId` | `string` | 当前选中的部门ID |
| `departId` | `string` | 当前用户部门ID(支持 orgCode 或 UUID |
| `role` | `string \| number` | 用户角色 |
| `isSpecial` | `number` | 是否特殊分发 |
| `multi` | `boolean` | 是否多选(默认 `true` |
**v-model:** `selectedDepId` — 当前选中部门
**Events:** `update:checkedUser`, `update:selectedDepId`
**密级过滤规则:** 只显示 `人员密级 > 文件密级` 的人员。例如文件密级为 3(秘密),则只显示密级为 4(机密)的人员。
### SzSelectedDepartsAndUsers — 已选展示
以标签形式展示已选择的部门和人员,支持删除和办/协/阅类型切换。
**Props:**
| 属性 | 类型 | 说明 |
|------|------|------|
| `disabled` | `boolean` | 是否禁用操作(默认 `false` |
| `selectedMainDeparts` | `DepartTreeNode[]` | 主办部门 |
| `selectedAssistDeparts` | `DepartTreeNode[]` | 协办部门 |
| `selectedReadDeparts` | `DepartTreeNode[]` | 阅文部门 |
| `selectedUsers` | `UserItem[]` | 办文人员 |
| `selectedReadUsers` | `UserItem[]` | 阅文人员 |
**Events:** `update:selectedMainDeparts`, `update:selectedAssistDeparts`, `update:selectedReadDeparts`, `update:selectedUsers`, `update:selectedReadUsers`
---
## 类型定义
### DepartTreeNode
```typescript
interface DepartTreeNode {
id: string; // 部门 UUID
key: string; // 树节点 key(等于 id
value: string; // 树节点 value(等于 id
title: string; // 部门名称
isLeaf?: boolean; // 是否叶子节点
departType?: string | number; // 部门类型:0=根, 1=职能, 2=业务, 3=科室
distributable?: number; // 是否可分发:1=是
disableCheckbox?: boolean; // 是否禁用选择
assignType?: number; // 分配类型:1=办, 2=协, 3=阅
children?: DepartTreeNode[]; // 子部门
description?: string; // 部门描述
parentId?: string; // 父部门ID
orgCode?: string; // 部门编码(如 "A01", "A03A02"
}
```
### UserItem
```typescript
interface UserItem {
id: string;
username: string;
realname: string;
orgCodeTxt?: string; // 所属部门名称
userSecurityLevel?: number; // 人员密级数值
userSecurityLevel_dictText?: string; // 人员密级文字
sex_dictText?: string; // 性别
sortno?: number; // 排序号
departId?: string;
departName?: string;
assignType?: number; // 分配类型:1=办, 3=阅
}
```
---
## departId 说明
`departId` 属性支持两种格式,组件内部会自动识别并转换:
| 格式 | 示例 | 说明 |
|------|------|------|
| orgCode | `"A01"`, `"A03A02"` | 部门编码,通常来自 `userInfo.orgCode` |
| UUID | `"f4d5979edc1d4785ba2322f0b8200a21"` | 部门唯一标识 |
推荐从 `useUserStore().getUserInfo.orgCode` 获取:
```typescript
import { useUserStore } from '/@/store/modules/user';
const userStore = useUserStore();
const departId = userStore.getUserInfo?.orgCode || '';
```
---
## 方案管理
组件内置方案保存/加载功能,数据存储在后端 `djOfficialdocDistribute` 表中。
- **保存方案**:输入方案名 → 点击"保存为方案"
- **加载方案**:在弹窗左侧已存方案列表中点击方案名
- **快速应用**:在弹窗外部的方案按钮直接点击即可应用并提交
- **删除方案**:点击方案旁的关闭按钮
加载方案时会自动校验人员状态(密级变更、部门变动、离职冻结),异常人员会弹出提示并自动移除。
---
## 依赖的后端接口
| 接口 | 用途 |
|------|------|
| `GET /sys/sysDepart/queryTreeList` | 获取部门树 |
| `GET /sys/user/queryUserComponentData` | 查询部门下的人员 |
| `GET /officialdocDistribute/djOfficialdocDistribute/list` | 方案列表 |
| `POST /officialdocDistribute/djOfficialdocDistribute/add` | 新增方案 |
| `POST /officialdocDistribute/djOfficialdocDistribute/edit` | 编辑方案 |
| `DELETE /officialdocDistribute/djOfficialdocDistribute/delete` | 删除方案 |
| `GET /distributionProcessinfo/officialdocDistributionProcessinfo/queryDeptIds` | 查询公文已分发部门ID |
---
## 测试页面
访问 `/semri/test/SzDistributeTest` 可查看组件演示和所有输入输出字段。