feat: 优化选人组件和选择组织组件
This commit is contained in:
@ -10,6 +10,10 @@
|
||||
- 设计器支持响应式布局,因为设计器架构问题,并未默认打开,对于表单内字段,除了附件、多行文本框等占用宽度较大的组件外,都建议开启响应式布局
|
||||
- 表单和表格需要合理调整字段宽度,响应式布局下需要使用定宽模式,一般情况下,字段的宽度取平均字长 + 2个汉字的宽度为宜,不要留太长的label,也要避免出现label换行
|
||||
|
||||
## Q&A
|
||||
### 为什么表单所有字段都成了必填
|
||||
默认情况下,绑定流程后,新建节点的所有字段都被设置为必填,需要在流程的开始节点-表单设置中去掉非必填的项。
|
||||
|
||||
## 在Tab页中打开表单/流程
|
||||
如果你需要自己编程实现Tab页跳转,或者升级旧版框架的页面,可以参考下面步骤:
|
||||
```typescript
|
||||
@ -63,7 +67,7 @@ function dbClickRow(record) {
|
||||
}
|
||||
}
|
||||
```
|
||||
同时,因为外层封装页面需要表单加载后的元数据,需要通过事件将表单数据传出。
|
||||
同时,因为外层封装页面需要表单加载后的元数据,需要在Form.vue中通过事件将表单数据传出。
|
||||
```javascript
|
||||
// 这行是原来有的
|
||||
import { formProps, formEventConfigs } from './config';
|
||||
@ -146,3 +150,20 @@ export const formProps: FormProps = {
|
||||
</template>
|
||||
```
|
||||
注意,根据antd-vue的文档,bodyCell里除了要修改的列,不能写其他内容,否则会将内容覆盖到所有列上。
|
||||
## 如何在按钮栏中刚增加按钮
|
||||
|
||||
|
||||
## 如何修改选项卡标题
|
||||
```javascript
|
||||
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const tabStore = useMultipleTabStore();
|
||||
const router = useRouter();
|
||||
const currentRoute = router.currentRoute.value;
|
||||
const fullPath = currentRoute.fullPath;
|
||||
tabStore.changeTitle(fullPath, `选项卡标题`);
|
||||
|
||||
// 顺便tabStore也支持关闭选项卡
|
||||
tabStore.closeTab(currentRoute, router);
|
||||
```
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
enum Api {
|
||||
Page = '/organization/department/page',
|
||||
Tree = '/organization/department/tree',
|
||||
Trees = '/organization/department/trees',
|
||||
EnabledTree = '/organization/department/enabled-tree',
|
||||
Info = '/organization/department/info',
|
||||
Department = '/organization/department',
|
||||
@ -23,7 +24,7 @@ enum Api {
|
||||
* @description: 查询部门树
|
||||
*/
|
||||
export async function getDepartmentTree(
|
||||
params?: DepartmentTreeParams,
|
||||
params?: any,
|
||||
mode: ErrorMessageMode = 'modal',
|
||||
) {
|
||||
return defHttp.get<DepartmentTreeModel>(
|
||||
@ -36,6 +37,23 @@ export async function getDepartmentTree(
|
||||
},
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @description: 查询部门树(新)
|
||||
*/
|
||||
export async function getDepartmentTrees(
|
||||
params?: any,
|
||||
mode: ErrorMessageMode = 'modal',
|
||||
) {
|
||||
return defHttp.get<DepartmentTreeModel>(
|
||||
{
|
||||
url: Api.Trees,
|
||||
params,
|
||||
},
|
||||
{
|
||||
errorMessageMode: mode,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 查询部门树
|
||||
|
||||
@ -49,6 +49,23 @@ export async function getUserPageList(
|
||||
},
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @description: 查询用户分页列表(新)
|
||||
*/
|
||||
export async function getUserPageListNew(
|
||||
params: any,
|
||||
mode: ErrorMessageMode = 'modal',
|
||||
) {
|
||||
return defHttp.get<UserPageListResultModel>(
|
||||
{
|
||||
url: Api.Page,
|
||||
params,
|
||||
},
|
||||
{
|
||||
errorMessageMode: mode,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 删除用户(批量删除)
|
||||
|
||||
@ -756,7 +756,7 @@
|
||||
import ApiCheckboxGroup from '/@/components/Form/src/components/ApiCheckboxGroup.vue';
|
||||
import SelectArea from '/@/components/Form/src/components/SelectArea.vue';
|
||||
import CommonInfo from '/@/components/Form/src/components/CommonInfo.vue';
|
||||
import SelectDepartment from '/@/components/Form/src/components/SelectDepartment.vue';
|
||||
import SelectDepartment from '/@/components/Form/src/components/SelectDepartmentV2.vue';
|
||||
import SelectMap from '/@/components/Form/src/components/SelectMap.vue';
|
||||
import XjrQrcode from '/@/components/Form/src/components/QrCode.vue';
|
||||
import ApiComplete from '/@/components/Form/src/components/ApiComplete.vue';
|
||||
|
||||
@ -455,6 +455,11 @@
|
||||
<a-form-item v-if="hasKey('reverse')" :label="t('反向坐标轴')">
|
||||
<a-switch v-model:checked="data.options.reverse" />
|
||||
</a-form-item>
|
||||
<template v-if="data.type === 'organization'">
|
||||
<a-form-item v-if="hasKey('parentNode')" label="父级节点">
|
||||
<SelectDepartmentV2 v-model:value="data.options.parentNode"></SelectDepartmentV2>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<template v-if="data.type === 'image'">
|
||||
<a-form-item :label="t('默认值')">
|
||||
@ -756,6 +761,7 @@
|
||||
// import { getAppEnvConfig } from '/@/utils/env';
|
||||
import { ColorPicker } from '/@/components/ColorPicker';
|
||||
import { TimePicker } from '/@/components/TimePicker';
|
||||
import SelectDepartmentV2 from '/@/components/Form/src/components/SelectDepartmentV2.vue';
|
||||
import { XjrDatePicker } from '/@/components/DatePicker';
|
||||
import { DicTreeSelect } from '/@/components/DicTreeSelect';
|
||||
import Sortable from 'sortablejs';
|
||||
@ -785,7 +791,8 @@
|
||||
XjrDatePicker,
|
||||
DicTreeSelect,
|
||||
ApiAssoConfig,
|
||||
DicAssoConfig
|
||||
DicAssoConfig,
|
||||
SelectDepartmentV2
|
||||
},
|
||||
props: {
|
||||
//所选组件配置
|
||||
|
||||
@ -1162,6 +1162,7 @@ export const infoComponents = [
|
||||
options: {
|
||||
labelWidthMode: 'fix',
|
||||
labelFixWidth: 120,
|
||||
parentNode: '',
|
||||
responsive: false,
|
||||
span: '',
|
||||
width: '100%',
|
||||
|
||||
@ -21,7 +21,9 @@ import ApiTree from './components/ApiTree.vue';
|
||||
import ApiTreeSelect from './components/ApiTreeSelect.vue';
|
||||
import ApiCascader from './components/ApiCascader.vue';
|
||||
import SelectDepartment from './components/SelectDepartment.vue';
|
||||
import SelectDepartmentV2 from './components/SelectDepartmentV2.vue';
|
||||
import SelectUser from './components/SelectUser.vue';
|
||||
import SelectUserV2 from './components/SelectUserV2.vue';
|
||||
import CommonInfo from './components/CommonInfo.vue';
|
||||
import SelectArea from './components/SelectArea.vue';
|
||||
import AutoCodeRule from './components/AutoCodeRule.vue';
|
||||
@ -97,8 +99,10 @@ componentMap.set('CheckboxGroup', Checkbox.Group);
|
||||
componentMap.set('ApiCascader', ApiCascader);
|
||||
componentMap.set('Slider', Slider);
|
||||
componentMap.set('Rate', Rate);
|
||||
componentMap.set('Dept', SelectDepartment);
|
||||
componentMap.set('User', SelectUser);
|
||||
componentMap.set('Dept', SelectDepartmentV2);
|
||||
componentMap.set('DeptV2', SelectDepartmentV2);
|
||||
componentMap.set('User', SelectUserV2);
|
||||
componentMap.set('UserV2', SelectUserV2);
|
||||
componentMap.set('Info', CommonInfo);
|
||||
componentMap.set('Area', SelectArea);
|
||||
componentMap.set('SubForm', SubForm);
|
||||
|
||||
102
src/components/Form/src/components/SelectDepartmentTreeV2.vue
Normal file
102
src/components/Form/src/components/SelectDepartmentTreeV2.vue
Normal file
@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<div class="select-department-tree">
|
||||
<a-tree v-model:expandedKeys="expandedKeys" v-model:selectedKeys="selectedKeys" v-model:checkedKeys="checkedKeys"
|
||||
:selectable="props.selectable" :multiple="props.multiple" v-model:checkable="checkable"
|
||||
v-model:tree-data="treeData" :load-data="loadData" @select="selectData">
|
||||
<template #title="{ title, key }">
|
||||
<span v-if="key === '0-0-1-0'" style="color: #1890ff">{{ title }}</span>
|
||||
<template v-else>{{ title }}</template>
|
||||
</template>
|
||||
</a-tree>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { getDepartmentTrees } from '/@/api/system/department';
|
||||
const props = defineProps({
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
parentNode: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(['change-value', 'query-completed']);
|
||||
const expandedKeys = ref<string[]>([]);
|
||||
const selectedKeys = ref<string[]>([]);
|
||||
const checkedKeys = ref<string[]>([]);
|
||||
const checkable = ref(false)
|
||||
const treeData = ref([])
|
||||
const params = ref({
|
||||
id: '',
|
||||
code: '',
|
||||
name: '',
|
||||
parentNode: false
|
||||
})
|
||||
onMounted(() => {
|
||||
selectedKeys.value = [props.value]
|
||||
getList()
|
||||
})
|
||||
const loadData = (node) => {
|
||||
return new Promise(async (resolve: (value?: unknown) => void) => {
|
||||
if (node.dataRef.children.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
let param = {
|
||||
id: node.id,
|
||||
code: '',
|
||||
name: '',
|
||||
parentNode: false
|
||||
}
|
||||
let list = resetTreeList(await getDepartmentTrees(param))
|
||||
node.dataRef.children = list[0].children
|
||||
treeData.value = [...treeData.value];
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
async function getList() {
|
||||
if (props.parentNode) {
|
||||
params.value.id = props.parentNode
|
||||
}
|
||||
let list = resetTreeList(await getDepartmentTrees(params.value))
|
||||
treeData.value = list
|
||||
emit('query-completed');
|
||||
}
|
||||
function resetTreeList(list) {
|
||||
const result = list.map(item => {
|
||||
return {
|
||||
...item,
|
||||
...{
|
||||
key: item.id,
|
||||
children: resetTreeList(item.children),
|
||||
title: item.name
|
||||
}
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
function selectData(selectedKeys, e) {
|
||||
emit('change-value', e.selectedNodes);
|
||||
}
|
||||
watch(expandedKeys, () => {
|
||||
console.log('expandedKeys', expandedKeys);
|
||||
});
|
||||
watch(selectedKeys, () => {
|
||||
console.log('selectedKeys', selectedKeys);
|
||||
});
|
||||
watch(checkedKeys, () => {
|
||||
console.log('checkedKeys', checkedKeys);
|
||||
});
|
||||
</script>
|
||||
199
src/components/Form/src/components/SelectDepartmentV2.vue
Normal file
199
src/components/Form/src/components/SelectDepartmentV2.vue
Normal file
@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div style="width: 100%" @click="show">
|
||||
<a-input v-model:value="departNames" :bordered="bordered" :disabled="disabled" :placeholder="placeholder"
|
||||
:size="size" readonly>
|
||||
<template v-if="prefix" #prefix>
|
||||
<Icon :icon="prefix" />
|
||||
</template>
|
||||
<template v-if="suffix" #suffix>
|
||||
<Icon :icon="suffix" />
|
||||
</template>
|
||||
</a-input>
|
||||
<ModalPanel :visible="visible" :width="800" :title="t('添加部门')" @submit="submit" @close="close">
|
||||
<div class="choose-dep-box">
|
||||
<div class="choose-dep">
|
||||
<a-spin class="loading-box" :spinning="loading" />
|
||||
<SelectDepartmentTreeV2 v-if="visible" @changeValue="departChange" :value="props.value"
|
||||
@queryCompleted="queryCompleted" :parentNode="parentNode"></SelectDepartmentTreeV2>
|
||||
</div>
|
||||
<div class="choosen-dep">
|
||||
<div class="choosen-item" v-for="item in selectedNodes">
|
||||
<div class="choosen-label">{{ item.name }}</div>
|
||||
<close-outlined class="close" @click="deleteItem" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ModalPanel>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import SelectDepartmentTreeV2 from './SelectDepartmentTreeV2.vue'
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { ModalPanel } from '/@/components/ModalPanel/index';
|
||||
const { t } = useI18n();
|
||||
const emits = defineEmits(['change', 'changeNames', 'close', 'options-change', 'update:value']);
|
||||
import { getDepartmentTrees } from '/@/api/system/department';
|
||||
import { CloseOutlined } from '@ant-design/icons-vue';
|
||||
const visible = ref(false)
|
||||
const departNames = ref<string>();
|
||||
|
||||
const props = defineProps({
|
||||
value: String,
|
||||
prefix: String,
|
||||
suffix: String,
|
||||
placeholder: String,
|
||||
readonly: Boolean,
|
||||
disabled: Boolean,
|
||||
parentNode: String,
|
||||
size: String,
|
||||
bordered: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
componentProps: {
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
});
|
||||
const selectedNodes = ref([])
|
||||
const loading = ref(true)
|
||||
|
||||
// Embedded in the form, just use the hook binding to perform form verification
|
||||
|
||||
onMounted(() => {
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.value,
|
||||
(val: any) => {
|
||||
if (val) {
|
||||
getDefaultList(val)
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
async function getDefaultList(id) {
|
||||
let param = {
|
||||
id: id,
|
||||
code: '',
|
||||
name: '',
|
||||
parentNode: false
|
||||
}
|
||||
let list = resetTreeList(await getDepartmentTrees(param))
|
||||
selectedNodes.value = list
|
||||
let nameList = selectedNodes.value.map(item => item.name)
|
||||
const names = nameList.join(',')
|
||||
departNames.value = names;
|
||||
// handleSelectedNodes()
|
||||
}
|
||||
function resetTreeList(list) {
|
||||
const result = list.map(item => {
|
||||
return {
|
||||
...item,
|
||||
...{
|
||||
key: item.id,
|
||||
children: resetTreeList(item.children),
|
||||
title: item.name
|
||||
}
|
||||
}
|
||||
})
|
||||
return result
|
||||
}
|
||||
function queryCompleted() {
|
||||
loading.value = false
|
||||
}
|
||||
function departChange(nodes) {
|
||||
selectedNodes.value = nodes
|
||||
}
|
||||
function show() {
|
||||
visible.value = true
|
||||
loading.value = true
|
||||
if (props.value) {
|
||||
getDefaultList(props.value)
|
||||
}
|
||||
}
|
||||
function handleSelectedNodes() {
|
||||
let nameList = selectedNodes.value.map(item => item.name)
|
||||
let idList = selectedNodes.value.map(item => item.id)
|
||||
const names = nameList.join(',')
|
||||
const ids = idList.join(',')
|
||||
emits('update:value', ids);
|
||||
departNames.value = names;
|
||||
}
|
||||
function deleteItem() {
|
||||
selectedNodes.value = []
|
||||
}
|
||||
function submit() {
|
||||
handleSelectedNodes()
|
||||
close();
|
||||
}
|
||||
function close() {
|
||||
visible.value = false;
|
||||
selectedNodes.value = []
|
||||
emits('close');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.choose-dep-box {
|
||||
display: flex;
|
||||
height: 95%;
|
||||
|
||||
.choose-dep {
|
||||
border: 1px solid #eaeaea;
|
||||
margin: 8px;
|
||||
width: 60%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: scroll;
|
||||
|
||||
.loading-box {
|
||||
position: absolute;
|
||||
left: 49%;
|
||||
top: 49%;
|
||||
}
|
||||
}
|
||||
|
||||
.choosen-dep {
|
||||
border: 1px solid #eaeaea;
|
||||
margin: 8px;
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
padding: 4px;
|
||||
|
||||
.choosen-item {
|
||||
padding: 2px 4px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.choosen-label {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.close {
|
||||
visibility: hidden
|
||||
}
|
||||
}
|
||||
|
||||
.choosen-item:hover {
|
||||
background-color: #eaeaea;
|
||||
|
||||
.close {
|
||||
visibility: visible
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
137
src/components/Form/src/components/SelectUserListV2.vue
Normal file
137
src/components/Form/src/components/SelectUserListV2.vue
Normal file
@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div class="user-select-list">
|
||||
<div class="user-select-item" v-for="item in data" @click="selectItem(item)">
|
||||
<div class="user-select-item-left">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<div class="user-select-item-right">
|
||||
<div class="select-circle" :class="item.selected ? 'selected' : ''" v-if="multiple && !viewList">
|
||||
<check-outlined v-if="item.selected" />
|
||||
</div>
|
||||
<div class="delete-circle" v-if="canDel" @click="delItem(item)">
|
||||
<CloseOutlined />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-box" v-if="!data.length">
|
||||
<a-empty :image="simpleImage">
|
||||
<template #description>
|
||||
<div v-html="emptyDescription"></div>
|
||||
</template>
|
||||
</a-empty>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref } from 'vue';
|
||||
import { CheckOutlined, CloseOutlined } from '@ant-design/icons-vue';
|
||||
import { Empty } from 'ant-design-vue';
|
||||
import { string } from 'vue-types';
|
||||
const emits = defineEmits(['update:value', 'selectId', 'change', 'delId']);
|
||||
const simpleImage = ref(Empty.PRESENTED_IMAGE_SIMPLE)
|
||||
|
||||
const props = defineProps({
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
emptyDescription: {
|
||||
type: String,
|
||||
default: '暂无数据'
|
||||
},
|
||||
data: {
|
||||
type: Array,
|
||||
default: []
|
||||
},
|
||||
canDel: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
viewList: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
})
|
||||
function selectItem(item) {
|
||||
emits('selectId', item.id)
|
||||
}
|
||||
function delItem(item) {
|
||||
emits('delId', item.id)
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.user-select-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
|
||||
.empty-box {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
:deep(.ant-empty-normal) {
|
||||
// margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.user-select-item {
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
margin: 5px 0;
|
||||
color: #111111;
|
||||
|
||||
.user-select-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-select-item-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.select-circle {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid rgba(144, 147, 153, 0.7);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.delete-circle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.selected {
|
||||
border: none;
|
||||
background-color: #5d9cec;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: white;
|
||||
font-size: 10px
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-select-item:hover {
|
||||
background-color: #f6f6f6;
|
||||
|
||||
.delete-circle {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
423
src/components/Form/src/components/SelectUserV2.vue
Normal file
423
src/components/Form/src/components/SelectUserV2.vue
Normal file
@ -0,0 +1,423 @@
|
||||
<template>
|
||||
<div :class="{ disabled }" class="form-select-user" @click="show">
|
||||
<a-input v-model:value="userNames" :disabled="disabled" :placeholder="placeholder" :size="size" readonly>
|
||||
<template v-if="prefix" #prefix>
|
||||
<Icon :icon="prefix" />
|
||||
</template>
|
||||
<template v-if="suffix" #suffix>
|
||||
<Icon :icon="suffix" />
|
||||
</template>
|
||||
</a-input>
|
||||
<ModalPanel :visible="visible" :width="900" :title="t('选择人员')" @submit="submit" @close="close"
|
||||
class="select-user-model">
|
||||
<div class="select-user">
|
||||
<div class="select-user-left">
|
||||
<a-tabs v-model:activeKey="activeKey" :tabBarGutter="20">
|
||||
<a-tab-pane key="department" tab="组织架构">
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="allPerson" tab="所有人">
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<div class="person-search">
|
||||
<a-form-item label="" name="name">
|
||||
<a-input v-model:value="selectPersonNames" :placeholder="searchPlaceholder" :size="size"
|
||||
class="search-input">
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div class="select-user-box" v-show="activeKey === 'department'">
|
||||
<div class="department-tree">
|
||||
<a-spin class="loading-box" :spinning="treeLoading" />
|
||||
<div class="department-tree-box">
|
||||
<!-- <div class="department-tree-title sub-title">
|
||||
组织
|
||||
</div> -->
|
||||
<SelectDepartmentTreeV2 @changeValue="departChange" class="tree-select"
|
||||
v-if="visible && activeKey === 'department'" @queryCompleted="departCompleted">
|
||||
</SelectDepartmentTreeV2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-select-box">
|
||||
<!-- <div class="user-select-title sub-title">
|
||||
可选列表
|
||||
</div> -->
|
||||
<SelectUserListV2 :data="searchDepartMemberList" @selectId="changeDepMemberSelect"
|
||||
emptyDescription="暂无人员">
|
||||
</SelectUserListV2>
|
||||
<div class="user-select-pagination" v-if="false">
|
||||
<a-pagination v-model:current="searchDepartMemberParams.limit" :pageSize="searchDepartMemberParams.size"
|
||||
:total="searchDepartMemberTotal" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="all-user-select-box" v-show="activeKey === 'allPerson'">
|
||||
<!-- <div class="all-user-select-title sub-title">
|
||||
可选列表
|
||||
</div> -->
|
||||
<SelectUserListV2 :data="searchAllMemberList" @selectId="changeMemberSelect"></SelectUserListV2>
|
||||
<div class="all-user-select-pagination" v-if="searchAllMemberTotal > 25">
|
||||
<a-form-item label="" name="pagination">
|
||||
<a-pagination size="small" showLessItems :total="searchAllMemberTotal" :show-size-changer="false"
|
||||
v-model:current="searchAllMemberParams.limit" :pageSize="searchAllMemberParams.size"
|
||||
@change="changeAllMemberPage" hideOnSinglePage :show-total="total => `共${total}人`" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="select-user-right">
|
||||
<!-- <div class="selected-user-title sub-title">
|
||||
已选列表
|
||||
</div> -->
|
||||
<SelectUserListV2 :data="selectedMemberList" viewList canDel @delId="delMember"
|
||||
emptyDescription="暂无已选择人员,<br> 请从左侧添加人员">
|
||||
</SelectUserListV2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ModalPanel>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { ModalPanel } from '/@/components/ModalPanel/index';
|
||||
import { getUserPageListNew, getUserMulti } from '/@/api/system/user';
|
||||
import SelectDepartmentTreeV2 from './SelectDepartmentTreeV2.vue'
|
||||
import SelectUserListV2 from './SelectUserListV2.vue';
|
||||
const emits = defineEmits(['update:value', 'selectedId', 'change']);
|
||||
const { t } = useI18n();
|
||||
const userNames = ref('')
|
||||
const treeLoading = ref(true)
|
||||
const visible = ref(false)
|
||||
const activeKey = ref('department')
|
||||
const selectedMemberList = ref([])
|
||||
const selectPersonNames = ref('')
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
prefix: String,
|
||||
suffix: String,
|
||||
placeholder: String,
|
||||
readonly: Boolean,
|
||||
disabled: Boolean,
|
||||
size: String,
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
let timeoutId = null;
|
||||
let resetMemberList = []
|
||||
const searchPlaceholder = '请输入姓名搜索'
|
||||
// 防止输入多次调用搜索接口导致问题,使用防抖
|
||||
watch(selectPersonNames, (newValue) => {
|
||||
if (newValue && activeKey.value !== 'allPerson') {
|
||||
activeKey.value = 'allPerson'
|
||||
}
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
// 调用搜索接口
|
||||
console.log('执行搜索API操作, 查询参数为:', newValue);
|
||||
searchNameChanged(newValue)
|
||||
}, 1000);
|
||||
|
||||
});
|
||||
watch(activeKey, (newValue) => {
|
||||
searchAllMemberList.value = []
|
||||
searchAllMemberTotal.value = 0
|
||||
searchDepartMemberList.value = []
|
||||
treeLoading.value = true
|
||||
if (newValue !== 'allPerson') {
|
||||
selectPersonNames.value = ''
|
||||
} else {
|
||||
if (!timeoutId) {
|
||||
searchNameChanged('')
|
||||
}
|
||||
}
|
||||
})
|
||||
watch(props, async () => {
|
||||
if (props.value && !resetMemberList.length) {
|
||||
const list = await getUserMulti(props.value)
|
||||
selectedMemberList.value = list
|
||||
resetMemberList = cloneDeep(list)
|
||||
const nameList = resetMemberList.map(item => {
|
||||
return item.name
|
||||
})
|
||||
userNames.value = nameList.join(',')
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
})
|
||||
const searchDepartMemberParams = ref({
|
||||
limit: 1,
|
||||
size: 10000,
|
||||
departmentId: ''
|
||||
})
|
||||
const searchDepartMemberTotal = ref(0)
|
||||
const searchDepartMemberList = ref()
|
||||
const searchAllMemberParams = ref({
|
||||
limit: 1,
|
||||
size: 25,
|
||||
isSearchAll: true,
|
||||
name: ''
|
||||
})
|
||||
const searchAllMemberTotal = ref(0)
|
||||
const searchAllMemberList = ref()
|
||||
async function departChange(e) {
|
||||
searchDepartMemberParams.value.departmentId = e[0].id
|
||||
let res = await getUserList(searchDepartMemberParams.value)
|
||||
searchDepartMemberTotal.value = res.total
|
||||
searchDepartMemberList.value = setSelected(res.list)
|
||||
}
|
||||
async function searchNameChanged(val) {
|
||||
searchAllMemberParams.value.limit = 1
|
||||
searchAllMemberParams.value.name = val
|
||||
getAllMemberList()
|
||||
}
|
||||
async function getAllMemberList() {
|
||||
let res = await getUserList(searchAllMemberParams.value)
|
||||
searchAllMemberTotal.value = res.total
|
||||
searchAllMemberList.value = setSelected(res.list)
|
||||
|
||||
}
|
||||
function setSelected(list) {
|
||||
const idList = selectedMemberList.value.map(item => item.id)
|
||||
const ids = idList.join(',')
|
||||
list.forEach(item => {
|
||||
if (ids?.includes(item.id)) {
|
||||
item.selected = true
|
||||
} else {
|
||||
item.selected = false
|
||||
}
|
||||
})
|
||||
return list
|
||||
}
|
||||
async function getUserList(params) {
|
||||
return await getUserPageListNew(params)
|
||||
}
|
||||
function departCompleted() {
|
||||
treeLoading.value = false
|
||||
}
|
||||
function changeDepMemberSelect(id) {
|
||||
let list = searchDepartMemberList.value
|
||||
searchDepartMemberList.value = handleListSelected(list, id)
|
||||
}
|
||||
function changeMemberSelect(id) {
|
||||
let list = searchAllMemberList.value
|
||||
searchAllMemberList.value = handleListSelected(list, id)
|
||||
}
|
||||
function handleListSelected(list, id) {
|
||||
let selected = false
|
||||
let selectedItem
|
||||
list.forEach(item => {
|
||||
if (item.id === id) {
|
||||
item.selected = !!!item.selected
|
||||
selected = item.selected
|
||||
selectedItem = item
|
||||
}
|
||||
})
|
||||
let memberList = selectedMemberList.value
|
||||
if (selected) {
|
||||
memberList.push(selectedItem)
|
||||
selectedMemberList.value = memberList
|
||||
} else {
|
||||
selectedMemberList.value = memberList.filter(m => m.id !== id)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
function changeAllMemberPage(page) {
|
||||
searchAllMemberParams.value.limit = page
|
||||
getAllMemberList()
|
||||
}
|
||||
function show() {
|
||||
visible.value = true
|
||||
treeLoading.value = true
|
||||
}
|
||||
function delMember(id) {
|
||||
if (activeKey.value === 'department') {
|
||||
changeDepMemberSelect(id)
|
||||
} else {
|
||||
changeMemberSelect(id)
|
||||
}
|
||||
}
|
||||
function submit() {
|
||||
let idsList = []
|
||||
let namesList = []
|
||||
selectedMemberList.value.forEach(item => {
|
||||
idsList.push(item.id)
|
||||
namesList.push(item.name)
|
||||
})
|
||||
const ids = idsList.join(',')
|
||||
const names = namesList.join(',')
|
||||
resetMemberList = cloneDeep(selectedMemberList.value)
|
||||
userNames.value = names
|
||||
emits('update:value', ids);
|
||||
emits('selectedId', ids);
|
||||
emits('change', ids, selectedMemberList.value);
|
||||
close();
|
||||
}
|
||||
function close() {
|
||||
selectedMemberList.value = cloneDeep(resetMemberList)
|
||||
visible.value = false;
|
||||
activeKey.value = 'department'
|
||||
searchDepartMemberList.value = []
|
||||
searchAllMemberList.value = []
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
.select-user-model {
|
||||
.content {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="less" scoped>
|
||||
.sub-title {
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: rgba(144, 147, 153, 0.7);
|
||||
}
|
||||
|
||||
|
||||
.select-user {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
// border: 1px solid #eaeaea;
|
||||
|
||||
.select-user-left {
|
||||
width: 75%;
|
||||
height: 100%;
|
||||
border-right: 1px solid #eaeaea;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.ant-tabs-nav) {
|
||||
margin-bottom: 0;
|
||||
|
||||
.ant-tabs-nav-wrap {
|
||||
padding-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.person-search {
|
||||
padding: 4px;
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.select-user-box {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
border-top: 1px solid #eaeaea;
|
||||
|
||||
.department-tree {
|
||||
position: relative;
|
||||
width: 65%;
|
||||
height: 100%;
|
||||
border-right: 1px solid #eaeaea;
|
||||
|
||||
.loading-box {
|
||||
position: absolute;
|
||||
top: 49%;
|
||||
left: 49%;
|
||||
}
|
||||
|
||||
.department-tree-box {
|
||||
// overflow: scroll;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4px 8px;
|
||||
|
||||
.department-tree-title {}
|
||||
|
||||
.tree-select {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.user-select-box {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.user-select-title {}
|
||||
|
||||
.user-select-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: scroll;
|
||||
|
||||
.user-select-item {
|
||||
height: 30px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-select-pagination {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.all-user-select-box {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.all-user-select-title {}
|
||||
|
||||
.all-user-select-pagination {
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
.select-user-right {
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.selected-user-title {}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -125,7 +125,9 @@ export type ComponentType =
|
||||
| 'ChildTable'
|
||||
| 'Divider'
|
||||
| 'Dept'
|
||||
| 'DeptV2'
|
||||
| 'User'
|
||||
| 'UserV2'
|
||||
| 'Info'
|
||||
| 'Area'
|
||||
| 'SubForm'
|
||||
|
||||
Reference in New Issue
Block a user