【v3.8.3】组织机构部门大改造(支持子公司、岗位与不能功能划分更清晰,岗位可以设置上下级,岗位可以设置职级,支持回报关系)

This commit is contained in:
JEECG
2025-09-14 10:26:24 +08:00
parent adeebee840
commit 39c0d5b3f5
21 changed files with 1472 additions and 46 deletions

View File

@@ -14,6 +14,8 @@ export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
export { default as JAreaLinkage } from './src/jeecg/components/JAreaLinkage.vue';
export { default as JSelectUser } from './src/jeecg/components/JSelectUser.vue';
export { default as JSelectDept } from './src/jeecg/components/JSelectDept.vue';
export { default as JSelectDepartPost } from './src/jeecg/components/JSelectDepartPost.vue';
export { default as JSelectUserByDeptPost } from './src/jeecg/components/JSelectUserByDeptPost.vue';
export { default as JCodeEditor } from './src/jeecg/components/JCodeEditor.vue';
export { default as JCategorySelect } from './src/jeecg/components/JCategorySelect.vue';
export { default as JSelectMultiple } from './src/jeecg/components/JSelectMultiple.vue';

View File

@@ -44,6 +44,7 @@ import JSelectRole from './jeecg/components/JSelectRole.vue';
import JImageUpload from './jeecg/components/JImageUpload.vue';
import JDictSelectTag from './jeecg/components/JDictSelectTag.vue';
import JSelectDept from './jeecg/components/JSelectDept.vue';
import JSelectDepartPost from './jeecg/components/JSelectDepartPost.vue';
import JAreaSelect from './jeecg/components/JAreaSelect.vue';
import JEditor from './jeecg/components/JEditor.vue';
// import JMarkdownEditor from './jeecg/components/JMarkdownEditor.vue';
@@ -77,6 +78,7 @@ import JRangeDate from './jeecg/components/JRangeDate.vue'
import JRangeTime from './jeecg/components/JRangeTime.vue'
import JInputSelect from './jeecg/components/JInputSelect.vue'
import RoleSelectInput from './jeecg/components/roleSelect/RoleSelectInput.vue';
import JSelectUserByDeptPost from './jeecg/components/JSelectUserByDeptPost.vue';
import {DatePickerInFilter, CascaderPcaInFilter} from "@/components/InFilter";
const componentMap = new Map<ComponentType, Component>();
@@ -174,6 +176,8 @@ componentMap.set('RangeDate', JRangeDate);
componentMap.set('RangeTime', JRangeTime);
componentMap.set('RoleSelect', RoleSelectInput);
componentMap.set('JInputSelect', JInputSelect);
componentMap.set('JSelectDepartPost', JSelectDepartPost);
componentMap.set('JSelectUserByDeptPost', JSelectUserByDeptPost);

View File

@@ -0,0 +1,178 @@
<!--部门选择组件-->
<template>
<div class="JSelectDepartPost">
<JSelectBiz @change="handleSelectChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs" :isCustomRenderTag="isCustomRenderTag" :rowKey="getBindValue?.rowKey"/>
<a-form-item>
<DeptSelectModal @register="regModal" @getSelectResult="setValue" modalTitle="部门岗位选择" v-bind="getBindValue" :multiple="multiple" @close="handleClose" />
</a-form-item>
</div>
</template>
<script lang="ts">
import DeptSelectModal from './modal/DeptSelectModal.vue';
import JSelectBiz from './base/JSelectBiz.vue';
import { defineComponent, ref, reactive, watchEffect, watch, provide, unref, toRaw } from 'vue';
import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { SelectValue } from 'ant-design-vue/es/select';
import { cloneDeep } from 'lodash-es';
export default defineComponent({
name: 'JSelectDepartPost',
components: {
DeptSelectModal,
JSelectBiz,
},
inheritAttrs: false,
props: {
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
// 是否允许多选,默认 true
multiple: propTypes.bool.def(true),
//是否只选择岗位
izOnlySelectDepartPost: propTypes.bool.def(true),
// 自定义渲染tag
isCustomRenderTag: propTypes.bool.def(true),
},
emits: ['options-change', 'change', 'select', 'update:value'],
setup(props, { emit, refs }) {
const emitData = ref<any[]>();
//注册model
const [regModal, { openModal }] = useModal();
//下拉框选项值
const selectOptions = ref<SelectValue>([]);
//下拉框选中值
let selectValues = reactive<Recordable>({
value: [],
});
let tempSave: any = [];
// 是否正在加载回显数据
const loadingEcho = ref<boolean>(false);
//下发 selectOptions,xxxBiz组件接收
provide('selectOptions', selectOptions);
//下发 selectValues,xxxBiz组件接收
provide('selectValues', selectValues);
//下发 loadingEcho,xxxBiz组件接收
provide('loadingEcho', loadingEcho);
const tag = ref(false);
const attrs = useAttrs();
/**
* 监听组件值
*/
watchEffect(() => {
tempSave = [];
props.value && initValue();
});
watch(
() => props.value,
() => {
initValue();
}
);
watch(selectOptions, () => {
if (selectOptions) {
emit('select', toRaw(unref(selectOptions)), toRaw(unref(selectValues)));
}
});
/**
* 打卡弹出框
*/
function handleOpen() {
tag.value = true;
openModal(true, {
isUpdate: false,
});
}
/**
* 将字符串值转化为数组
*/
function initValue() {
let value = props.value ? props.value : [];
if (value && typeof value === 'string') {
selectValues.value = value.split(',');
tempSave = value.split(',');
} else {
selectValues.value = value;
tempSave = cloneDeep(value);
}
}
/**
* 设置下拉框的值
*/
function setValue(options, values) {
selectOptions.value = options;
selectValues.value = values;
send(values);
}
const getBindValue = Object.assign({}, unref(props), unref(attrs));
const handleClose = () => {
if (tempSave.length) {
selectValues.value = cloneDeep(tempSave);
} else {
send(tempSave);
}
};
const handleSelectChange = (values) => {
tempSave = cloneDeep(values);
send(tempSave);
};
const send = (values) => {
let result = typeof props.value == 'string' ? values.join(',') : values;
emit('update:value', result);
emit('change', result);
if (!values || values.length == 0) {
emit('select', null, null);
}
};
return {
// state,
attrs,
selectOptions,
selectValues,
loadingEcho,
getBindValue,
tag,
regModal,
setValue,
handleOpen,
handleClose,
handleSelectChange,
};
},
});
</script>
<style lang="less" scoped>
.JSelectDepartPost {
> .ant-form-item {
display: none;
}
}
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>

View File

@@ -1,7 +1,7 @@
<!--部门选择组件-->
<template>
<div class="JSelectDept">
<JSelectBiz @change="handleSelectChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"/>
<JSelectBiz @change="handleSelectChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs" :isCustomRenderTag="isCustomRenderTag" :rowKey="getBindValue?.rowKey"/>
<!-- update-begin--author:liaozhiyang---date:20240515---forQQYUN-9260必填模式下会影响到弹窗内antd组件的样式 -->
<a-form-item>
<DeptSelectModal @register="regModal" @getSelectResult="setValue" v-bind="getBindValue" :multiple="multiple" @close="handleClose"/>
@@ -31,6 +31,8 @@
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
// 是否允许多选,默认 true
multiple: propTypes.bool.def(true),
// 自定义渲染tag
isCustomRenderTag: propTypes.bool.def(true),
},
emits: ['options-change', 'change', 'select', 'update:value'],
setup(props, { emit, refs }) {

View File

@@ -0,0 +1,158 @@
<!--用户选择组件-->
<template>
<div>
<JSelectBiz @change="handleChange" @handleOpen="handleOpen" :loading="loadingEcho" v-bind="attrs"></JSelectBiz>
<UserSelectByDepPostModal :rowKey="rowKey" @register="regModal" @getSelectResult="setValue" v-bind="getBindValue"></UserSelectByDepPostModal>
</div>
</template>
<script lang="ts">
import { unref } from 'vue';
import UserSelectByDepPostModal from './modal/UserSelectByDepPostModal.vue';
import JSelectBiz from './base/JSelectBiz.vue';
import { defineComponent, ref, reactive, watchEffect, watch, provide } from 'vue';
import { useModal } from '/@/components/Modal';
import { propTypes } from '/@/utils/propTypes';
import { useRuleFormItem } from '/@/hooks/component/useFormItem';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { SelectValue } from 'ant-design-vue/es/select';
export default defineComponent({
name: 'JSelectUserByDeptPost',
components: {
UserSelectByDepPostModal,
JSelectBiz,
},
inheritAttrs: false,
props: {
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
rowKey: {
type: String,
default: 'username',
},
labelKey: {
type: String,
default: 'realname',
},
},
emits: ['options-change', 'change', 'update:value'],
setup(props, { emit, refs }) {
const emitData = ref<any[]>();
//注册model
const [regModal, { openModal }] = useModal();
//表单值
const [state] = useRuleFormItem(props, 'value', 'change', emitData);
//下拉框选项值
const selectOptions = ref<SelectValue>([]);
//下拉框选中值
let selectValues = reactive<object>({
value: [],
change: false,
});
// 是否正在加载回显数据
const loadingEcho = ref<boolean>(false);
//下发 selectOptions,xxxBiz组件接收
provide('selectOptions', selectOptions);
//下发 selectValues,xxxBiz组件接收
provide('selectValues', selectValues);
//下发 loadingEcho,xxxBiz组件接收
provide('loadingEcho', loadingEcho);
const tag = ref(false);
const attrs = useAttrs();
/**
* 监听组件值
*/
watchEffect(() => {
initValue();
});
/**
* 监听selectValues变化
*/
watch(selectValues, () => {
if (selectValues) {
// update-begin--author:liaozhiyang---date:20250616---for【QQYUN-12869】通过部门选择用户组件必填状态下选择用户后点击重置后会出校验信息
if (props.value === undefined && selectValues.value?.length == 0) {
return;
}
// update-end--author:liaozhiyang---date:20250616---for【QQYUN-12869】通过部门选择用户组件必填状态下选择用户后点击重置后会出校验信息
state.value = selectValues.value;
}
});
/**
* 打卡弹出框
*/
function handleOpen() {
tag.value = true;
openModal(true, {
isUpdate: false,
});
}
/**
* 将字符串值转化为数组
*/
function initValue() {
let value = props.value ? props.value : [];
if (value && typeof value === 'string' && value != 'null' && value != 'undefined') {
state.value = value.split(',');
selectValues.value = value.split(',');
} else {
selectValues.value = value;
}
}
/**
* 设置下拉框的值
*/
function setValue(options, values) {
selectOptions.value = options;
//emitData.value = values.join(",");
state.value = values;
selectValues.value = values;
emit('update:value', values);
emit('options-change', options);
}
function handleChange(values) {
emit('update:value', values);
}
const getBindValue = Object.assign({}, unref(props), unref(attrs));
return {
state,
attrs,
selectOptions,
getBindValue,
selectValues,
loadingEcho,
tag,
regModal,
setValue,
handleOpen,
handleChange,
};
},
});
</script>
<style lang="less" scoped>
.j-select-row {
@width: 82px;
.left {
width: calc(100% - @width - 8px);
}
.right {
width: @width;
}
.full {
width: 100%;
}
:deep(.ant-select-search__field) {
display: none !important;
}
}
</style>

View File

@@ -25,7 +25,16 @@
style="width: 100%"
@click="!disabled && openModal(false)"
v-bind="attrs"
></a-select>
>
<template v-if="isCustomRenderTag" #tagRender="{ label, value, option}">
<a-tag class="ant-select-selection-item">
<span class="ant-select-selection-item-content" style="font-size: 14px;max-width: 300px" :title="tagRender(label, value, option)">{{ tagRender(label, value, option) }}</span>
<span class="ant-select-selection-item-remove">
<Icon icon="ant-design:close-outlined" size="12" @click="handleRemoveClick(value)"></Icon>
</span>
</a-tag>
</template>
</a-select>
</a-col>
<a-col v-if="showButton" class="right">
<a-button v-if="buttonIcon" :preIcon="buttonIcon" type="primary" @click="openModal(true)" :disabled="disabled">
@@ -43,6 +52,7 @@
import { propTypes } from '/@/utils/propTypes';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { LoadingOutlined } from '@ant-design/icons-vue';
import { getDepartPathNameByOrgCode } from "@/utils/common/compUtils";
export default defineComponent({
name: 'JSelectBiz',
@@ -69,6 +79,11 @@
buttonIcon: propTypes.string.def(''),
// 【TV360X-1002】是否是详情模式
isDetailsMode: propTypes.bool.def(false),
//update-begin---author:wangshuai---date:2025-09-06---for: 多选时是否自定义渲染tag文本为空不渲染不支持单选---
//是否自定义渲染tag
isCustomRenderTag: propTypes.bool.def(false),
rowKey: propTypes.string.def('id'),
//update-end---author:wangshuai---date:2025-09-06---for:多选时是否自定义渲染tag文本为空不渲染不支持单选---
},
emits: ['handleOpen', 'change'],
setup(props, { emit, refs }) {
@@ -78,6 +93,10 @@
const selectValues = inject('selectValues') || ref({});
const attrs = useAttrs();
const detailStr = ref('');
//存放部门名称
const departNamePath = ref<Record<string, string>>({});
/**
* 打开弹出框
*/
@@ -98,7 +117,54 @@
selectValues.change = true;
emit('change', value);
}
/**
* 多选tag自定义渲染
*
* @param label
* @param value
* @param isEllipsis 是否省略
*/
function tagRender(label, value, isEllipsis) {
if (departNamePath.value[value]) {
//是否需要省略显示
if(!isEllipsis){
return departNamePath.value[value];
} else {
let departName = departNamePath.value[value];
//超过20则截取后20位的字符前面加省略号
if(departName && departName.length >= 20){
const name:any = departName.substring(departName.length - 20);
return '...' + name;
} else {
return departName;
}
}
}
//判断rowKey是否为orgCode
if(props?.rowKey && props?.rowKey === 'orgCode'){
getDepartPathNameByOrgCode(value, label, '').then((data) => {
departNamePath.value[value] = data;
});
} else {
//否则按照id处理
getDepartPathNameByOrgCode('', label, value).then((data) => {
departNamePath.value[value] = data;
});
}
}
/**
* tag删除
*
* @param value
*/
function handleRemoveClick(value) {
if(selectValues?.value){
let values = selectValues?.value.filter(item => item !== value);
handleChange(values);
}
}
// -update-begin--author:liaozhiyang---date:20240617---for【TV360X-1002】详情页面行编辑用户组件和部门组件显示方式优化
watch(
[selectValues, options],
@@ -121,6 +187,8 @@
handleChange,
openModal,
detailStr,
tagRender,
handleRemoveClick,
};
},
});

View File

@@ -1,7 +1,8 @@
<!--部门选择框-->
<template>
<div>
<BasicModal v-bind="$attrs" @register="register" :title="modalTitle" width="500px" :maxHeight="maxHeight" @ok="handleOk" destroyOnClose @visible-change="visibleChange">
<BasicModal v-bind="$attrs" @register="register" :title="modalTitle" width="600px" :minHeight="300" :maxHeight="maxHeight" @ok="handleOk" destroyOnClose @visible-change="visibleChange">
<a-input-search v-if="izOnlySelectDepartPost" placeholder="按岗位名称搜索…" style="margin-bottom: 10px" @search="onSearch" @change="handelSearchChange"/>
<BasicTree
ref="treeRef"
:treeData="treeData"
@@ -11,9 +12,15 @@
@check="onCheck"
:fieldNames="fieldNames"
:checkedKeys="checkedKeys"
:expandedKeys="expandedKeys"
:multiple="multiple"
:checkStrictly="getCheckStrictly"
/>
:key="reloadKey"
>
<template #title="{ orgCategory, title }">
<TreeIcon :orgCategory="orgCategory" :title="title"></TreeIcon>
</template>
</BasicTree>
<!--树操作部分-->
<template #insertFooter>
<a-dropdown placement="top">
@@ -34,17 +41,19 @@
<script lang="ts">
import { defineComponent, ref, unref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { queryDepartTreeSync, queryTreeList } from '/@/api/common/api';
import { queryDepartTreeSync, queryTreeList, queryDepartAndPostTreeSync } from '/@/api/common/api';
import { useAttrs } from '/@/hooks/core/useAttrs';
import { treeProps } from '/@/components/Form/src/jeecg/props/props';
import { BasicTree, TreeActionType } from '/@/components/Tree';
import { useTreeBiz } from '/@/components/Form/src/jeecg/hooks/useTreeBiz';
import {propTypes} from "/@/utils/propTypes";
import { omit } from 'lodash-es';
import TreeIcon from '@/components/Form/src/jeecg/components/TreeIcon/TreeIcon.vue';
export default defineComponent({
name: 'DeptSelectModal',
components: {
TreeIcon,
BasicModal,
BasicTree,
},
@@ -61,17 +70,28 @@
default: 500,
},
// update-end--author:liaozhiyang---date:20231220---for【QQYUN-7678】部门组件内容过多没有滚动条给一个默认最大高
value: propTypes.oneOfType([propTypes.string, propTypes.array])
value: propTypes.oneOfType([propTypes.string, propTypes.array]),
//查询参数
params: {
type: Object,
default: () => ({}),
},
},
emits: ['register', 'getSelectResult', 'close'],
setup(props, { emit, refs }) {
setup(props, { emit }) {
//注册弹框
const [register, { closeModal }] = useModalInner();
const attrs = useAttrs();
const treeRef = ref<Nullable<TreeActionType>>(null);
//加载树key
const reloadKey = ref<number>(Math.random());
//update-begin-author:taoyan date:2022-10-28 for: 部门选择警告类型不匹配
let propValue = props.value === ''?[]:props.value;
// 确保传递给BasicTree的value是数组格式
if (propValue && typeof propValue === 'string') {
propValue = propValue.split(',');
}
//update-begin-author:liusq date:2023-05-26 for: [issues/538]JSelectDept组件受 dynamicDisabled 影响
let temp = Object.assign({}, unref(props), unref(attrs), {value: propValue},{disabled: false});
const getBindValue = omit(temp, 'multiple');
@@ -79,9 +99,9 @@
//update-end-author:taoyan date:2022-10-28 for: 部门选择警告类型不匹配
const queryUrl = getQueryUrl();
const [{ visibleChange, checkedKeys, getCheckStrictly, getSelectTreeData, onCheck, onLoadData, treeData, checkALL, expandAll, onSelect }] =
const [{ visibleChange, checkedKeys, getCheckStrictly, getSelectTreeData, onCheck, onLoadData, treeData, checkALL, expandAll, onSelect, onSearch, expandedKeys }] =
useTreeBiz(treeRef, queryUrl, getBindValue, props, emit);
const searchInfo = ref(props.params);
const searchInfo = ref(props.params || {});
const tree = ref([]);
//替换treeNode中key字段为treeData中对应的字段
const fieldNames = {
@@ -102,12 +122,21 @@
/** 获取查询数据方法 */
function getQueryUrl() {
let queryFn = props.sync ? queryDepartTreeSync : queryTreeList;
let queryFn = props.izOnlySelectDepartPost ? queryDepartAndPostTreeSync :props.sync ? queryDepartTreeSync : queryTreeList;
//update-begin-author:taoyan date:2022-7-4 for: issues/I5F3P4 online配置部门选择后编辑查看数据应该显示部门名称不是部门代码
return (params) => queryFn(Object.assign({}, params, { primaryKey: props.rowKey }));
//update-end-author:taoyan date:2022-7-4 for: issues/I5F3P4 online配置部门选择后编辑查看数据应该显示部门名称不是部门代码
}
/**
* 搜索值改变事件
* @param value
*/
function handelSearchChange(value) {
if(!value.target.value){
reloadKey.value = Math.random();
}
}
return {
tree,
handleOk,
@@ -120,12 +149,31 @@
expandAll,
fieldNames,
checkedKeys,
expandedKeys,
register,
getBindValue,
getCheckStrictly,
visibleChange,
onLoadData,
onSearch,
reloadKey,
handelSearchChange,
};
},
});
</script>
<style>
.svg-company {
width: 18px;
height: 18px;
position: relative;
top: 1px;
right: 2px;
}
.svg-depart,.svg-post {
width: 14px;
height: 16px;
position: relative;
right: 2px;
}
</style>

View File

@@ -121,21 +121,14 @@
};
//定义表格列
const columns = [
{
title: '职务编码',
dataIndex: 'code',
width: 180,
align: 'left',
},
{
title: '职务名称',
dataIndex: 'name',
// width: 180,
},
{
title: '职务级',
dataIndex: 'postRank_dictText',
width: 180,
title: '职务级',
dataIndex: 'postLevel',
},
];
//已选择的table信息

View File

@@ -2,7 +2,7 @@
<template>
<BasicModal v-bind="$attrs" @register="register" :title="modalTitle" width="1200px" @ok="handleOk" destroyOnClose @visible-change="visibleChange">
<a-row :gutter="10">
<a-col :md="7" :sm="24">
<a-col :md="7" :sm="24" style="height: 613px;overflow: auto ">
<a-card :style="{ minHeight: '613px', overflow: 'auto' }">
<!--组织机构-->
<BasicTree
@@ -15,7 +15,11 @@
:selectedKeys="selectedDepIds"
:expandedKeys="expandedKeys"
:clickRowToExpand="false"
></BasicTree>
>
<template #title="{ orgCategory, title }">
<TreeIcon :orgCategory="orgCategory" :title="title"></TreeIcon>
</template>
</BasicTree>
</a-card>
</a-col>
<a-col :md="17" :sm="24">
@@ -37,9 +41,11 @@
import { useAttrs } from '/@/hooks/core/useAttrs';
import { queryDepartTreeSync as queryDepartTreeSyncOrigin } from '/@/views/system/depart/depart.api';
import { selectProps } from '/@/components/Form/src/jeecg/props/props';
import TreeIcon from '@/components/Form/src/jeecg/components/TreeIcon/TreeIcon.vue';
export default defineComponent({
name: 'UserSelectByDepModal',
components: {
TreeIcon,
//此处需要异步加载BasicTable
BasicModal,
BasicTree,
@@ -108,7 +114,7 @@
md: 6,
lg: 8,
xl: 6,
xxl: 10,
xxl: 8,
},
//update-begin-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
actionColOptions: {
@@ -126,6 +132,11 @@
field: 'username',
component: 'Input',
},
{
label: '姓名',
field: 'realname',
component: 'Input',
},
],
resetFunc: customResetFunc,
},

View File

@@ -0,0 +1,291 @@
<!--通过部门选择用户-->
<template>
<BasicModal v-bind="$attrs" @register="register" :title="modalTitle" width="1200px" @ok="handleOk" destroyOnClose @visible-change="visibleChange">
<a-row :gutter="10">
<a-col :md="7" :sm="24">
<a-card :style="{ minHeight: '613px', overflow: 'auto' }">
<a-input-search placeholder="按岗位名称搜索…" style="margin-bottom: 10px" @search="onSearch" @change="handelSearchChange"/>
<!--组织机构-->
<BasicTree
ref="treeRef"
:style="{ minWidth: '250px' }"
selectable
@select="onDepSelect"
:load-data="loadChildrenTreeData"
:treeData="departTree"
:selectedKeys="selectedDepIds"
:expandedKeys="expandedKeys"
:clickRowToExpand="false"
:key="reloadKey"
>
<template #title="{ orgCategory, title }">
<TreeIcon :orgCategory="orgCategory" :title="title"></TreeIcon>
</template>
</BasicTree>
</a-card>
</a-col>
<a-col :md="17" :sm="24">
<a-card :style="{ minHeight: '613px', overflow: 'auto' }">
<!--用户列表-->
<BasicTable ref="tableRef" v-bind="getBindValue" :searchInfo="searchInfo" :api="getTableList" :rowSelection="rowSelection"></BasicTable>
</a-card>
</a-col>
</a-row>
</BasicModal>
</template>
<script lang="ts">
import { defineComponent, unref, ref } from 'vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicTree } from '/@/components/Tree/index';
import { queryDepartPostUserPageList as getTableListOrigin} from '/@/api/common/api';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { useSelectBiz } from '/@/components/Form/src/jeecg/hooks/useSelectBiz';
import { useAttrs } from '/@/hooks/core/useAttrs';
import TreeIcon from '/@/components/Form/src/jeecg/components/TreeIcon/TreeIcon.vue';
import { queryDepartAndPostTreeSync as queryDepartTreeSyncOrigin } from '/@/views/system/depart/depart.api';
import { selectProps } from '/@/components/Form/src/jeecg/props/props';
import {defHttp} from "@/utils/http/axios";
export default defineComponent({
name: 'UserSelectByDepPostModal',
components: {
//此处需要异步加载BasicTable
BasicModal,
BasicTree,
BasicTable: createAsyncComponent(() => import('/@/components/Table/src/BasicTable.vue'), {
loading: true,
}),
TreeIcon,
},
props: {
...selectProps,
//选择框标题
modalTitle: {
type: String,
default: '部门用户选择',
},
},
emits: ['register', 'getSelectResult'],
setup(props, { emit, refs }) {
const tableRef = ref();
const treeRef = ref();
//注册弹框
const [register, { closeModal }] = useModalInner(async (data) => {
await queryDepartTree();
});
const attrs = useAttrs();
const departTree = ref<any>([]);
const selectedDepIds = ref([]);
const expandedKeys = ref([]);
const searchInfo = {};
//树加载的key
const reloadKey = ref(Math.random());
/**
*表格配置
*/
const tableProps = {
columns: [
{
title: '用户账号',
dataIndex: 'username',
width: 180,
},
{
title: '用户姓名',
dataIndex: 'realname',
width: 180,
},
{
title: '性别',
dataIndex: 'sex_dictText',
width: 80,
},
{
title: '手机号码',
dataIndex: 'phone',
},
],
useSearchForm: true,
canResize: false,
showIndexColumn: false,
striped: true,
bordered: true,
size: 'small',
formConfig: {
//labelWidth: 200,
baseColProps: {
xs: 24,
sm: 8,
md: 6,
lg: 8,
xl: 6,
xxl: 10,
},
//update-begin-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
actionColOptions: {
xs: 24,
sm: 12,
md: 12,
lg: 12,
xl: 8,
xxl: 8,
},
//update-end-author:liusq date:2023-10-30 for: [issues/5514]组件页面显示错位
schemas: [
{
label: '账号',
field: 'username',
component: 'Input',
},
],
resetFunc: customResetFunc,
},
};
const getBindValue = Object.assign({}, unref(props), unref(attrs), tableProps);
const [{ rowSelection, visibleChange, indexColumnProps, getSelectResult, reset }] = useSelectBiz(getTableList, getBindValue);
function getTableList(params) {
params = parseParams(params);
return getTableListOrigin({ ...params });
}
function queryDepartTreeSync(params) {
params = parseParams(params);
return queryDepartTreeSyncOrigin({ ...params });
}
/**
* 解析参数
* @param params
*/
function parseParams(params) {
if (props?.params) {
return {
...params,
...props.params,
};
}
return params;
}
/**
* 加载树形数据
*/
function queryDepartTree() {
queryDepartTreeSync({}).then((res) => {
if (res) {
departTree.value = res;
}
});
}
/**
* 加载子级部门
*/
async function loadChildrenTreeData(treeNode) {
try {
const result = await queryDepartTreeSync({
pid: treeNode.eventKey,
});
const asyncTreeAction = unref(treeRef);
if (asyncTreeAction) {
asyncTreeAction.updateNodeByKey(treeNode.eventKey, { children: result });
asyncTreeAction.setExpandedKeys([treeNode.eventKey, ...asyncTreeAction.getExpandedKeys()]);
}
} catch (e) {
console.error(e);
}
return Promise.resolve();
}
/**
* 点击树节点,筛选出对应的用户
*/
function onDepSelect(keys) {
if (keys[0] != null) {
if (unref(selectedDepIds)[0] !== keys[0]) {
selectedDepIds.value = [keys[0]];
}
searchInfo['departId'] = unref(selectedDepIds).join(',');
tableRef.value.reload();
}
}
/**
* 自定义重置方法
* */
async function customResetFunc() {
console.log('自定义查询');
//树节点清空
selectedDepIds.value = [];
//查询条件清空
searchInfo['departId'] = '';
//选择项清空
reset();
}
/**
* 确定选择
*/
function handleOk() {
getSelectResult((options, values) => {
//回传选项和已选择的值
emit('getSelectResult', options, values);
//关闭弹窗
closeModal();
});
}
/**
* 岗位搜索
*
* @param value
*/
async function onSearch(value) {
if(value){
let result = await defHttp.get({ url: "/sys/sysDepart/searchBy", params: { keyWord: value, orgCategory: "3",...props.params } });
if (Array.isArray(result)) {
departTree.value = result;
} else {
departTree.value = [];
}
} else {
departTree.value = [];
await queryDepartTree();
}
}
/**
* 搜索值改变事件
* @param value
*/
function handelSearchChange(value) {
if(!value.target.value){
reloadKey.value = Math.random();
}
}
return {
//config,
handleOk,
searchInfo,
register,
indexColumnProps,
visibleChange,
getBindValue,
rowSelection,
departTree,
selectedDepIds,
expandedKeys,
treeRef,
tableRef,
getTableList,
onDepSelect,
loadChildrenTreeData,
onSearch,
handelSearchChange,
reloadKey,
};
},
});
</script>
<style scoped lang="less"></style>

View File

@@ -121,6 +121,7 @@ export type ComponentType =
| 'JImageUpload'
| 'JDictSelectTag'
| 'JSelectDept'
| 'JSelectDepartPost'
| 'JAreaSelect'
| 'JEditor'
| 'JMarkdownEditor'
@@ -139,6 +140,7 @@ export type ComponentType =
| 'JTreeSelect'
| 'JEllipsis'
| 'JSelectUserByDept'
| 'JSelectUserByDeptPost'
| 'JSelectUserByDepartment'
| 'JUpload'
| 'JSearchSelect'

View File

@@ -441,7 +441,7 @@
const showTitle = title || toolbar || search || slots.headerTitle;
const scrollStyle: CSSProperties = { height: 'calc(100% - 38px)' };
return (
<div class={[bem(), 'h-full', attrs.class]}>
<div class={[bem(), 'h-full',unref(getBindValues).multiple === false ? 'custom-radio':'' , attrs.class]}>
{showTitle && (
<TreeHeader
checkable={checkable}
@@ -475,3 +475,27 @@
},
});
</script>
<style lang="less" scoped>
// update-begin--author:liaozhiyang---date:20250908---for【JHHB-192】主职务选择多选框改成单选
.custom-radio {
:deep(.ant-tree) {
.ant-tree-checkbox {
.ant-tree-checkbox-inner {
border-style: solid;
border-width: 1px;
border-radius: 50%;
&::after {
width: 0;
height: 0;
left: 50%;
border-width: 6px;
border-radius: 50%;
margin-left: -3px;
margin-top: 1px;
}
}
}
}
}
// update-end--author:liaozhiyang---date:20250908---for【JHHB-192】主职务选择多选框改成单选
</style>

View File

@@ -0,0 +1,97 @@
<template>
<a-row :class="['p-4', `${prefixCls}--box`]" type="flex" :gutter="10">
<a-col :xl="12" :lg="24" :md="24" style="margin-bottom: 10px">
<DepartLeftTree
v-if="showDepart"
ref="leftTree"
@select="onTreeSelect"
@rootTreeData="onRootTreeData"
:isTenantDepart="true"
:loginTenantName="loginTenantName"
/>
</a-col>
<a-col :xl="12" :lg="24" :md="24" style="margin-bottom: 10px">
<div style="height: 100%" :class="[`${prefixCls}`]">
<a-tabs v-show="departData != null" defaultActiveKey="base-info">
<a-tab-pane tab="基本信息" key="base-info" forceRender style="position: relative">
<div style="padding: 20px">
<DepartFormTab v-if="showDepart" :data="departData" :rootTreeData="rootTreeData" @success="onSuccess" :isTenantDepart="true" />
</div>
</a-tab-pane>
</a-tabs>
<div v-show="departData == null" style="padding-top: 40px">
<a-empty description="尚未选择部门" />
</div>
</div>
</a-col>
</a-row>
</template>
<script lang="ts" setup name="TenantDepartList">
import { onMounted, provide, ref } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import DepartLeftTree from '/@/views/system/depart/components/DepartLeftTree.vue';
import DepartFormTab from '/@/views/system/depart/components/DepartFormTab.vue';
import { getLoginTenantName } from '@/views/system/tenant/tenant.api';
import { tenantSaasMessage } from '@/utils/common/compUtils';
const { prefixCls } = useDesign('tenant-depart-manage');
provide('prefixCls', prefixCls);
// 给子组件定义一个ref变量
const leftTree = ref();
//是否显示部门
const showDepart = ref(false);
// 当前选中的部门信息
const departData = ref({});
const rootTreeData = ref<any[]>([]);
const loginTenantName = ref<string>('');
/**
* 获取租户名称
*/
getTenantName();
async function getTenantName() {
loginTenantName.value = await getLoginTenantName();
if (loginTenantName.value) {
showDepart.value = true;
} else {
showDepart.value = false;
}
}
// 左侧树选择后触发
function onTreeSelect(data) {
departData.value = data;
}
// 左侧树rootTreeData触发
function onRootTreeData(data) {
rootTreeData.value = data;
}
function onSuccess() {
leftTree.value.loadRootTreeData();
}
onMounted(() => {
//提示信息
tenantSaasMessage('租户部门');
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-tenant-depart-manage';
.@{prefix-cls} {
background: @component-background;
&--box {
.ant-tabs-nav {
padding: 0 20px;
}
}
}
</style>

View File

@@ -1,6 +1,17 @@
<template>
<BasicModal :title="title" :width="800" v-bind="$attrs" @ok="handleOk" @register="registerModal">
<BasicForm @register="registerForm" />
<BasicForm @register="registerForm" >
<template #depPostParentId="{ model, field }">
<a-tree-select v-model:value="depPostValue" :treeData="treeData" allowClear treeCheckable @select="treeSelect">
<template #title="{ orgCategory, title }">
<TreeIcon :orgCategory="orgCategory" :title="title"></TreeIcon>
</template>
<template #tagRender="{option}">
<span style="margin-left: 10px" v-if="orgNameMap[option.id]">{{orgNameMap[option.id]}}</span>
</template>
</a-tree-select>
</template>
</BasicForm>
</BasicModal>
</template>
@@ -12,6 +23,8 @@
import { saveOrUpdateDepart } from '../depart.api';
import { useBasicFormSchema, orgCategoryOptions } from '../depart.data';
import TreeIcon from "@/components/Form/src/jeecg/components/TreeIcon/TreeIcon.vue";
import { getDepartPathNameByOrgCode } from "@/utils/common/compUtils";
const emit = defineEmits(['success', 'register']);
const props = defineProps({
@@ -23,10 +36,15 @@
// 当前的弹窗数据
const model = ref<object>({});
const title = computed(() => (isUpdate.value ? '编辑' : '新增'));
const treeData = ref<any>([]);
//上级岗位
const depPostValue = ref<any>([]);
//上级岗位名称映射
const orgNameMap = ref<Record<string, string>>({});
//注册表单
const [registerForm, { resetFields, setFieldsValue, validate, updateSchema }] = useForm({
schemas: useBasicFormSchema().basicFormSchema,
schemas: useBasicFormSchema(treeData).basicFormSchema,
showActionButtonGroup: false,
});
@@ -37,6 +55,17 @@
// 当前是否为添加子级
let isChild = unref(data?.isChild);
let categoryOptions = isChild ? orgCategoryOptions.child : orgCategoryOptions.root;
if(data.record?.orgCategory && data.record?.orgCategory === '2'){
categoryOptions = orgCategoryOptions.childDepartPost;
}
if(data.record?.orgCategory && data.record?.orgCategory === '3'){
categoryOptions = orgCategoryOptions.childPost;
}
if(data.record?.depPostParentId){
orgNameMap.value[data.record.depPostParentId] = await getDepartPathNameByOrgCode('', '', data.record.depPostParentId);
depPostValue.value = [data.record.depPostParentId];
}
// 隐藏不需要展示的字段
updateSchema([
{
@@ -62,11 +91,14 @@
if (typeof record !== 'object') {
record = {};
}
let orgCategory = data.record?.orgCategory;
let company = orgCategory === '1' || orgCategory === '4';
delete data.record?.orgCategory;
// 赋默认值
record = Object.assign(
{
departOrder: 0,
orgCategory: categoryOptions[0].value,
orgCategory: company?categoryOptions[1].value:categoryOptions[0].value,
},
record
);
@@ -79,6 +111,11 @@
try {
setModalProps({ confirmLoading: true });
let values = await validate();
if(depPostValue.value && depPostValue.value.length > 0){
values.depPostParentId = depPostValue.value[0];
}else{
values.depPostParentId = "";
}
//提交表单
await saveOrUpdateDepart(values, isUpdate.value);
//关闭弹窗
@@ -89,4 +126,29 @@
setModalProps({ confirmLoading: false });
}
}
/**
* 树选中事件
*
* @param info
* @param keys
*/
async function treeSelect(keys,info) {
if (info.checkable) {
//解决闪动问题
orgNameMap.value[info.id] = "";
depPostValue.value = [info.value];
orgNameMap.value[info.id] = await getDepartPathNameByOrgCode(info.orgCode,info.label,info.id);
} else {
depPostValue.value = [];
}
}
</script>
<style lang="less" scoped>
:deep(.ant-select-selector .ant-select-selection-item){
svg {
display: none !important;
}
}
</style>

View File

@@ -1,6 +1,17 @@
<template>
<a-spin :spinning="loading">
<BasicForm @register="registerForm" />
<BasicForm @register="registerForm" >
<template #depPostParentId="{ model, field }">
<a-tree-select v-model:value="depPostValue" :treeData="treeData" allowClear treeCheckable @select="treeSelect">
<template #title="{ orgCategory, title }">
<TreeIcon :orgCategory="orgCategory" :title="title"></TreeIcon>
</template>
<template #tagRender="{ option }">
<span style="margin-left: 10px">{{ orgNameMap[option.id] }}</span>
</template>
</a-tree-select>
</template>
</BasicForm>
<div class="j-box-bottom-button offset-20" style="margin-top: 30px">
<div class="j-box-bottom-button-float" :class="[`${prefixCls}`]">
<a-button preIcon="ant-design:sync-outlined" @click="onReset">重置</a-button>
@@ -14,8 +25,10 @@
import { watch, computed, inject, ref, unref, onMounted } from 'vue';
import { BasicForm, useForm } from '/@/components/Form/index';
import { saveOrUpdateDepart } from '../depart.api';
import { useBasicFormSchema, orgCategoryOptions } from '../depart.data';
import { useBasicFormSchema, orgCategoryOptions, positionChange } from '../depart.data';
import { useDesign } from '/@/hooks/web/useDesign';
import TreeIcon from "@/components/Form/src/jeecg/components/TreeIcon/TreeIcon.vue";
import { getDepartPathNameByOrgCode } from '@/utils/common/compUtils';
const { prefixCls } = useDesign('j-depart-form-content');
@@ -29,10 +42,15 @@
const isUpdate = ref<boolean>(true);
// 当前的弹窗数据
const model = ref<object>({});
const treeData = ref<any>([]);
//上级岗位
const depPostValue = ref<any>([]);
//上级岗位名称映射
const orgNameMap = ref<Record<string, string>>({});
//注册表单
const [registerForm, { resetFields, setFieldsValue, validate, updateSchema }] = useForm({
schemas: useBasicFormSchema().basicFormSchema,
schemas: useBasicFormSchema(treeData).basicFormSchema,
showActionButtonGroup: false,
});
@@ -59,6 +77,11 @@
record = {};
}
model.value = record;
if (record.depPostParentId) {
orgNameMap.value[record.depPostParentId] = await getDepartPathNameByOrgCode('', '', record.depPostParentId);
depPostValue.value = [record.depPostParentId];
}
positionChange(record.positionId, record, treeData);
await resetFields();
await setFieldsValue({ ...record });
},
@@ -104,6 +127,11 @@
loading.value = true;
let values = await validate();
values = Object.assign({}, model.value, values);
if (depPostValue.value && depPostValue.value.length > 0) {
values.depPostParentId = depPostValue.value[0];
} else {
values.depPostParentId = '';
}
//提交表单
await saveOrUpdateDepart(values, isUpdate.value);
//刷新列表
@@ -113,6 +141,22 @@
loading.value = false;
}
}
/**
* 树选中事件
*
* @param info
* @param keys
*/
async function treeSelect(keys, info) {
if (info.checkable) {
orgNameMap.value[info.id] = '';
depPostValue.value = [info.value];
orgNameMap.value[info.id] = await getDepartPathNameByOrgCode(info.orgCode, info.label, info.id);
} else {
depPostValue.value = [];
}
}
</script>
<style lang="less">
// update-begin-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
@@ -126,3 +170,10 @@
/*end 兼容暗夜模式*/
// update-end-author:liusq date:20230625 for: [issues/563]暗色主题部分失效
</style>
<style lang="less" scoped>
:deep(.ant-select-selector .ant-select-selection-item){
svg{
display: none !important;
}
}
</style>

View File

@@ -1,14 +1,12 @@
<template>
<a-card :bordered="false" style="height: 100%">
<div class="j-table-operator" style="width: 100%">
<div class="j-table-operator" style="width: 100%;display: flex;align-items: center">
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="onAddDepart">新增</a-button>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="onAddChildDepart()">添加下级</a-button>
<a-upload name="file" :showUploadList="false" :customRequest="onImportXls">
<a-upload name="file" :showUploadList="false" :customRequest="onImportXls" v-if="!isTenantDepart">
<a-button type="primary" preIcon="ant-design:import-outlined">导入</a-button>
</a-upload>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls">导出</a-button>
<a-button type="primary" preIcon="ant-design:sync-outlined">同步企微?</a-button>
<a-button type="primary" preIcon="ant-design:sync-outlined">同步钉钉?</a-button>
<a-button type="primary" preIcon="ant-design:export-outlined" @click="onExportXls" v-if="!isTenantDepart">导出</a-button>
<template v-if="checkedKeys.length > 0">
<a-dropdown>
<template #overlay>
@@ -25,6 +23,10 @@
</a-button>
</a-dropdown>
</template>
<Icon icon="ant-design:question-circle-outlined" style="margin-left: 10px;cursor: pointer" @click="tipShow = true"></Icon>
<div v-if="loginTenantName" style="margin-left: 10px;"
>当前登录租户: <span class="tenant-name">{{ loginTenantName }}</span>
</div>
</div>
<a-alert type="info" show-icon class="alert" style="margin-bottom: 8px">
<template #message>
@@ -54,8 +56,9 @@
v-model:expandedKeys="expandedKeys"
@check="onCheck"
@select="onSelect"
style="overflow-y: auto;height: calc(100vh - 330px);"
>
<template #title="{ key: treeKey, title, dataRef }">
<template #title="{ key: treeKey, title, dataRef, data }">
<a-dropdown :trigger="['contextmenu']">
<Popconfirm
:open="visibleTreeKey === treeKey"
@@ -66,12 +69,12 @@
@confirm="onDelete(dataRef)"
@openChange="onVisibleChange"
>
<span>{{ title }}</span>
<TreeIcon :orgCategory="dataRef.orgCategory" :title="title"></TreeIcon>
</Popconfirm>
<template #overlay>
<a-menu @click="">
<a-menu-item key="1" @click="onAddChildDepart(dataRef)">添加</a-menu-item>
<a-menu-item key="1" @click="onAddChildDepart(dataRef)" v-if="data.orgCategory !== '3'">添加</a-menu-item>
<a-menu-item key="2" @click="visibleTreeKey = treeKey">
<span style="color: red">删除</span>
</a-menu-item>
@@ -85,22 +88,44 @@
</a-spin>
<DepartFormModal :rootTreeData="treeData" @register="registerModal" @success="loadRootTreeData" />
</a-card>
<a-modal v-model:open="tipShow" :footer="null" title="部门规则说明" :width="800">
<ul class="departmentalRulesTip">
<li>当前部门机构设置支持集团组织架构第一级默认为公司下级可创建子公司部门和岗位</li>
<li><br/></li>
<li>1岗位下不能添加下级</li>
<li>2部门下不能直接添加子公司</li>
<li>3子公司下可继续添加子公司</li>
<li>4岗位需配置职务级别岗位的级别高低和上下级关系均以职务级别及上级岗位设置为准</li>
<li>5董事长岗位仅可选择上级公司子公司或总公司各部门的所有岗位为上级岗位</li>
<li>6非董事长岗位仅可选择当前父级部门及本部门内级别更高的岗位为上级岗位</li>
<li><br/></li>
<li><b>特别说明</b>董事长相关逻辑为固定写死职务等级董事长的表述请勿修改</li>
</ul>
<div style="height: 10px"></div>
</a-modal>
</template>
<script lang="ts" setup>
import { inject, nextTick, ref, unref } from 'vue';
import { inject, nextTick, ref, unref, defineEmits } from 'vue';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/@/hooks/web/useMessage';
import { useMethods } from '/@/hooks/system/useMethods';
import { Api, deleteBatchDepart, queryDepartTreeSync } from '../depart.api';
import { Api, deleteBatchDepart, queryDepartAndPostTreeSync } from '../depart.api';
import { searchByKeywords } from '/@/views/system/departUser/depart.user.api';
import DepartFormModal from '/@/views/system/depart/components/DepartFormModal.vue';
import { Popconfirm } from 'ant-design-vue';
import TreeIcon from "@/components/Form/src/jeecg/components/TreeIcon/TreeIcon.vue";
const prefixCls = inject('prefixCls');
const emit = defineEmits(['select', 'rootTreeData']);
const { createMessage } = useMessage();
const { handleImportXls, handleExportXls } = useMethods();
const props = defineProps({
//是否为租户部门
isTenantDepart: { default: false, type: Boolean },
//当前登录租户
loginTenantName: { default: "", type: String },
})
const loading = ref<boolean>(false);
// 部门树列表数据
@@ -121,6 +146,8 @@
const visibleTreeKey = ref<any>(null);
// 搜索关键字
const searchKeyword = ref('');
// 提示弹窗是否显示
const tipShow = ref<boolean>(false);
// 注册 modal
const [registerModal, { openModal }] = useModal();
@@ -130,7 +157,7 @@
try {
loading.value = true;
treeData.value = [];
const result = await queryDepartTreeSync();
const result = await queryDepartAndPostTreeSync();
if (Array.isArray(result)) {
treeData.value = result;
}
@@ -158,7 +185,7 @@
// 加载子级部门信息
async function loadChildrenTreeData(treeNode) {
try {
const result = await queryDepartTreeSync({
const result = await queryDepartAndPostTreeSync({
pid: treeNode.dataRef.id,
});
if (result.length == 0) {
@@ -231,7 +258,11 @@
createMessage.warning('请先选择一个部门');
return;
}
const record = { parentId: data.id };
if(data.orgCategory === '3'){
createMessage.warning('岗位下无法添加子级!');
return;
}
const record = { parentId: data.id, orgCategory: data.orgCategory };
openModal(true, { isUpdate: false, isChild: true, record });
}
@@ -241,7 +272,7 @@
try {
loading.value = true;
treeData.value = [];
let result = await searchByKeywords({ keyWord: value });
let result = await searchByKeywords({ keyWord: value, orgCategory: "1,2,3,4" });
if (Array.isArray(result)) {
treeData.value = result;
}
@@ -336,3 +367,18 @@
loadRootTreeData,
});
</script>
<style lang="less" scoped>
.departmentalRulesTip{
margin: 20px;
background-color: #f8f9fb;
color: #99a1a9;
border-radius: 4px;
padding: 12px;
}
.tenant-name {
text-decoration: underline;
margin: 5px;
font-size: 15px;
}
</style>

View File

@@ -0,0 +1,46 @@
<template>
<a-spin :spinning="loading">
<template v-if="treeData && treeData.length > 0">
<BasicTree ref="basicTree" :treeData="treeData" :checkStrictly="true" style="height: 500px; overflow: auto"></BasicTree>
</template>
<a-empty v-else description="无岗位消息" />
</a-spin>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { BasicTree } from '/@/components/Tree/index';
import { getRankRelation } from '../depart.api';
const props = defineProps({
data: { type: Object, default: () => ({}) },
});
// 当前选中的部门ID可能会为空代表未选择部门
const departId = computed(() => props.data?.id);
const basicTree = ref();
const loading = ref<boolean>(false);
//树的全部节点信息
const treeData = ref<any[]>([]);
watch(departId, (val) => loadData(val), { immediate: true });
async function loadData(val) {
try {
loading.value = true;
await getRankRelation({ departId: val }).then((res) => {
if (res.success) {
treeData.value = res.result;
}
});
} finally {
loading.value = false;
}
}
</script>
<style lang="less" scoped>
.depart-rule-tree :deep(.scrollbar__bar) {
pointer-events: none;
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<!--引用表格-->
<BasicTable @register="registerTable" :rowSelection="rowSelection">
<!--插槽:table标题-->
<template #tableTitle>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="createUser" :disabled="!orgCode">新建用户</a-button>
<a-button type="primary" preIcon="ant-design:plus-outlined" @click="selectAddUser" :disabled="!orgCode || props.data?.orgCategory === '3'"
>添加已有用户</a-button
>
</template>
<!-- 插槽行内操作按钮 -->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" />
</template>
</BasicTable>
<UserDrawer @register="registerDrawer" @success="onUserDrawerSuccess" />
<UserSelectModal ref="userSelectModalRef" rowKey="id" @register="registerSelUserModal" @getSelectResult="onSelectUserOk" />
</template>
<script lang="ts" setup>
import { computed, inject, ref, watch } from 'vue';
import { ActionItem, BasicTable, TableAction } from '/@/components/Table';
import { useModal } from '/@/components/Modal';
import { useDrawer } from '/@/components/Drawer';
import { useListPage } from '/@/hooks/system/useListPage';
import UserDrawer from '/@/views/system/user/UserDrawer.vue';
import UserSelectModal from '/@/components/Form/src/jeecg/components/modal/UserSelectModal.vue';
import { queryByOrgCodeForAddressList } from '../depart.api';
import { ColEx } from '/@/components/Form/src/types';
import { userColumns } from '@/views/system/depart/depart.data';
import { linkDepartUserBatch } from '@/views/system/departUser/depart.user.api';
const prefixCls = inject('prefixCls');
const props = defineProps({
data: { require: true, type: Object },
});
const userSelectModalRef: any = ref(null);
// 当前选中的部门code可能会为空代表未选择部门
const orgCode = computed(() => props.data?.orgCode);
// 当前部门id
const departId = computed(() => props.data?.id);
// 自适应列配置
const adaptiveColProps: Partial<ColEx> = {
xs: 24, // <576px
sm: 24, // ≥576px
md: 24, // ≥768px
lg: 12, // ≥992px
xl: 12, // ≥1200px
xxl: 8, // ≥1600px
};
// 列表页面公共参数、方法
const { tableContext, createMessage } = useListPage({
tableProps: {
api: queryByOrgCodeForAddressList,
columns: userColumns,
canResize: false,
rowKey: 'id',
formConfig: {
// schemas: userInfoSearchFormSchema,
baseColProps: adaptiveColProps,
labelAlign: 'left',
labelCol: {
xs: 24,
sm: 24,
md: 24,
lg: 9,
xl: 7,
xxl: 5,
},
// 操作按钮配置
actionColOptions: {
...adaptiveColProps,
style: { textAlign: 'left' },
},
},
tableSetting: { cacheKey: 'depart_user_userInfo' },
// 请求之前对参数做处理
beforeFetch(params) {
return Object.assign(params, { orgCode: orgCode.value });
},
immediate: !!orgCode.value,
},
});
// 注册 ListTable
const [registerTable, { reload, setProps, setLoading, updateTableDataRecord }, { rowSelection, selectedRowKeys }] = tableContext;
watch(
() => props.data,
() => reload()
);
//注册drawer
const [registerDrawer, { openDrawer, setDrawerProps }] = useDrawer();
const [registerUserAuthDrawer, userAuthDrawer] = useDrawer();
// 注册用户选择 modal
const [registerSelUserModal, selUserModal] = useModal();
// 清空选择的行
function clearSelection() {
selectedRowKeys.value = [];
}
// 创建用户
async function createUser() {
if (!departId.value) {
createMessage.warning('请先选择一个部门');
} else {
let mainDepPostId = '';
let selecteddeparts = departId.value;
if (props.data?.orgCategory === '3') {
mainDepPostId = departId.value;
selecteddeparts = props.data.parentId;
}
openDrawer(true, {
isUpdate: false,
// 初始化负责部门
nextDepartOptions: { value: props.data?.key, label: props.data?.title },
//初始化岗位
record: {
mainDepPostId: mainDepPostId,
activitiSync: 1,
userIdentity: 1,
selecteddeparts: selecteddeparts,
},
});
}
}
// 查看用户详情
function showUserDetail(record) {
openDrawer(true, {
record,
isUpdate: true,
showFooter: false,
});
}
// 编辑用户信息
function editUserInfo(record) {
openDrawer(true, { isUpdate: true, record, departDisabled: true, departPostDisabled: true });
}
// 选择添加已有用户
function selectAddUser() {
userSelectModalRef.value.rowSelection.selectedRowKeys = [];
selUserModal.openModal();
}
// 选择用户成功
async function onSelectUserOk(options, userIdList) {
if (userIdList.length > 0) {
try {
setLoading(true);
await linkDepartUserBatch(departId.value, userIdList);
reload();
} finally {
setLoading(false);
}
}
}
/**
* 用户抽屉表单成功回调
*/
function onUserDrawerSuccess({ isUpdate, values }) {
isUpdate ? updateTableDataRecord(values.id, values) : reload();
}
/**
* 操作栏
*/
function getTableAction(record): ActionItem[] {
return [
{ label: '编辑', onClick: editUserInfo.bind(null, record) },
{ label: '详情', onClick: showUserDetail.bind(null, record) },
];
}
</script>

View File

@@ -24,6 +24,14 @@ export enum Api {
getUpdateDepartInfo = '/sys/user/getUpdateDepartInfo',
doUpdateDepartInfo = '/sys/user/doUpdateDepartInfo',
changeDepartChargePerson = '/sys/user/changeDepartChargePerson',
//根据部门id获取岗位信息
getPositionByDepartId = '/sys/sysDepart/getPositionByDepartId',
//根据部门id获取岗位上下级关系
getRankRelation = '/sys/sysDepart/getRankRelation',
//异步获取部门和岗位
queryDepartAndPostTreeSync = '/sys/sysDepart/queryDepartAndPostTreeSync',
//获取部门和岗位下的成员
queryByOrgCodeForAddressList = '/sys/user/queryByOrgCodeForAddressList',
}
/**
@@ -31,6 +39,11 @@ export enum Api {
*/
export const queryDepartTreeSync = (params?) => defHttp.get({ url: Api.queryDepartTreeSync, params });
/**
* 获取部门和岗位树列表
*/
export const queryDepartAndPostTreeSync = (params?) => defHttp.get({ url: Api.queryDepartAndPostTreeSync, params });
/**
* 保存或者更新部门角色
*/
@@ -120,3 +133,21 @@ export const deleteDepart = (id) => defHttp.delete({ url: Api.delete, params:{ i
* @param params
*/
export const changeDepartChargePerson = (params) => defHttp.put({ url: Api.changeDepartChargePerson, params });
/**
* 根据部门id获取岗位信息
*/
export const getPositionByDepartId = (params) => defHttp.get({ url: Api.getPositionByDepartId, params }, { isTransformResponse: false });
/**
* 根据部门id获取岗位上下级关系
* @param params
*/
export const getRankRelation = (params) => defHttp.get({ url: Api.getRankRelation, params }, { isTransformResponse: false });
/**
* 根据部门或岗位编码获取通讯录成员
*
* @param params
*/
export const queryByOrgCodeForAddressList = (params) => defHttp.get({ url: Api.queryByOrgCodeForAddressList, params });

View File

@@ -1,7 +1,16 @@
import { FormSchema } from '/@/components/Form';
import { getPositionByDepartId } from "./depart.api";
import { useMessage } from "@/hooks/web/useMessage";
import { BasicColumn } from "@/components/Table";
import { getDepartPathNameByOrgCode } from '@/utils/common/compUtils';
import { h, ref } from 'vue';
const { createMessage: $message } = useMessage();
//部门名称
const departNamePath = ref<Record<string, string>>({});
// 部门基础表单
export function useBasicFormSchema() {
export function useBasicFormSchema(treeData) {
const basicFormSchema: FormSchema[] = [
{
field: 'departName',
@@ -19,7 +28,21 @@ export function useBasicFormSchema() {
componentProps: {
treeData: [],
placeholder: '无',
treeCheckAble: true,
multiple: true,
dropdownStyle: { maxHeight: '200px', overflow: 'auto' },
tagRender: (options) => {
const { value, label, option } = options;
if (departNamePath.value[value]) {
return h(
'span', { style: { marginLeft: '10px' } },
departNamePath.value[value]
);
}
getDepartPathNameByOrgCode('', label, option.id).then((data) => {
departNamePath.value[value] = data;
});
},
},
},
{
@@ -36,6 +59,34 @@ export function useBasicFormSchema() {
component: 'RadioButtonGroup',
componentProps: { options: [] },
},
{
field: 'positionId',
label: '职务级别',
component: 'JDictSelectTag',
componentProps: ({ formModel, formActionType }) => {
return {
dictCode: "sys_position,name,id, 1=1 order by post_level asc",
getPopupContainer: ()=> document.body,
onChange: (value) => {
formModel.depPostParentId = "";
return positionChange(value, formModel, treeData);
},
}
},
ifShow:({ values })=>{
return values.orgCategory === '3'
},
required: true,
},
{
field: 'depPostParentId',
label: '上级岗位',
component: 'TreeSelect',
ifShow:({ values })=>{
return values.orgCategory === '3'
},
slot: 'depPostParentId',
},
{
field: 'departOrder',
label: '排序',
@@ -49,6 +100,9 @@ export function useBasicFormSchema() {
componentProps: {
placeholder: '请输入电话',
},
ifShow:({ values })=>{
return values.orgCategory !== '3'
},
},
{
field: 'fax',
@@ -57,6 +111,9 @@ export function useBasicFormSchema() {
componentProps: {
placeholder: '请输入传真',
},
ifShow:({ values })=>{
return values.orgCategory !== '3'
},
},
{
field: 'address',
@@ -65,6 +122,9 @@ export function useBasicFormSchema() {
componentProps: {
placeholder: '请输入地址',
},
ifShow:({ values })=>{
return values.orgCategory !== '3'
},
},
{
field: 'memo',
@@ -73,6 +133,9 @@ export function useBasicFormSchema() {
componentProps: {
placeholder: '请输入备注',
},
ifShow:({ values })=>{
return values.orgCategory !== '3'
},
},
{
field: 'id',
@@ -90,7 +153,59 @@ export const orgCategoryOptions = {
root: [{ value: '1', label: '公司' }],
// 子级部门
child: [
{ value: '4', label: '子公司' },
{ value: '2', label: '部门' },
{ value: '3', label: '岗位' },
],
//部门岗位
childDepartPost: [
{ value: '2', label: '部门' },
{ value: '3', label: '岗位' },
],
//岗位
childPost: [
{ value: '3', label: '岗位' },
]
};
/**
* 用户列表
*/
export const userColumns: BasicColumn[] = [
{
title: '姓名',
dataIndex: 'realname',
width: 150,
},
{
title: '手机',
width: 150,
dataIndex: 'phone',
},
{
title: '主岗位',
dataIndex: 'mainDepPostId_dictText',
width: 200,
},
];
/**
* 职位改变事件
* @param value
* @param model
* @param treeData
*/
export function positionChange(value, model, treeData) {
if(value && model.parentId){
getPositionByDepartId({ parentId: model.parentId, departId: model.id ? model.id:'', positionId: value }).then((res) =>{
if(res.success){
treeData.value = res.result;
}else{
treeData.value = [];
$message.warning(res.message);
}
});
} else {
treeData.value = [];
}
}

View File

@@ -1,9 +1,9 @@
<template>
<a-row :class="['p-4', `${prefixCls}--box`]" type="flex" :gutter="10">
<a-col :xl="12" :lg="24" :md="24" style="margin-bottom: 10px">
<a-col :xl="10" :lg="24" :md="24" style="margin-bottom: 10px">
<DepartLeftTree ref="leftTree" @select="onTreeSelect" @rootTreeData="onRootTreeData" />
</a-col>
<a-col :xl="12" :lg="24" :md="24" style="margin-bottom: 10px">
<a-col :xl="14" :lg="24" :md="24" style="margin-bottom: 10px">
<div style="height: 100%;" :class="[`${prefixCls}`]">
<a-tabs v-show="departData != null" defaultActiveKey="base-info">
<a-tab-pane tab="基本信息" key="base-info" forceRender style="position: relative">
@@ -16,6 +16,16 @@
<DepartRuleTab :data="departData" />
</div>
</a-tab-pane>
<a-tab-pane tab="职级汇报关系" key="rank">
<div style="padding: 0 20px 20px">
<DepartRankRelation :data="departData" />
</div>
</a-tab-pane>
<a-tab-pane tab="用户列表" key="user">
<div style="padding: 0 20px 20px">
<DepartUserList :data="departData" :key="reRender"></DepartUserList>
</div>
</a-tab-pane>
</a-tabs>
<div v-show="departData == null" style="padding-top: 40px">
<a-empty description="尚未选择部门" />
@@ -31,6 +41,8 @@
import DepartLeftTree from './components/DepartLeftTree.vue';
import DepartFormTab from './components/DepartFormTab.vue';
import DepartRuleTab from './components/DepartRuleTab.vue';
import DepartRankRelation from './components/DepartRankRelation.vue';
import DepartUserList from './components/DepartUserList.vue';
const { prefixCls } = useDesign('depart-manage');
provide('prefixCls', prefixCls);
@@ -41,10 +53,15 @@
// 当前选中的部门信息
const departData = ref({});
const rootTreeData = ref<any[]>([]);
const reRender = ref(-1);
// 左侧树选择后触发
function onTreeSelect(data) {
console.log('onTreeSelect: ', data);
if (reRender.value == -1) {
// 重新渲染组件
reRender.value = Math.random();
}
departData.value = data;
}