|
|
@@ -0,0 +1,321 @@
|
|
|
+<template>
|
|
|
+ <el-dialog
|
|
|
+ :model-value="visible"
|
|
|
+ title="选择人员"
|
|
|
+ :width="600"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ :close-on-press-escape="false"
|
|
|
+ :destroy-on-close="true"
|
|
|
+ :append-to-body="true"
|
|
|
+ @open="onOpen"
|
|
|
+ @close="onClose"
|
|
|
+ >
|
|
|
+ <div class="selector">
|
|
|
+ <div class="selector-left">
|
|
|
+ <cacp-autocomplete
|
|
|
+ v-model="selectedKeys"
|
|
|
+ :multiple="multiple"
|
|
|
+ value-key="fullPathName"
|
|
|
+ :fetch-suggestions="querySearch"
|
|
|
+ clearable
|
|
|
+ placeholder="请输入关键字"
|
|
|
+ @select="onSelect"
|
|
|
+ ></cacp-autocomplete>
|
|
|
+ <div class="selector-tree">
|
|
|
+ <el-scrollbar>
|
|
|
+ <el-tree
|
|
|
+ ref="selectorTreeRef"
|
|
|
+ :props="treeProps"
|
|
|
+ node-key="fullPathName"
|
|
|
+ :show-checkbox="true"
|
|
|
+ :check-strictly="true"
|
|
|
+ :default-checked-keys="selectedKeys"
|
|
|
+ :load="onLazyLoad"
|
|
|
+ lazy
|
|
|
+ @check-change="onCheckChange"
|
|
|
+ class="cacp-pd-s"
|
|
|
+ >
|
|
|
+ <template #default="{ data }">
|
|
|
+ <template v-if="data.oguType === 'ORGANIZATION'">
|
|
|
+ <el-icon class="cacp-mr-s" :size="16">
|
|
|
+ <Grid />
|
|
|
+ </el-icon>
|
|
|
+ <span>{{ data.oguName }}</span>
|
|
|
+ </template>
|
|
|
+ <template v-else-if="data.oguType === 'USER'">
|
|
|
+ <el-icon v-if="data.sideline" class="cacp-mr-s" :size="16" title="兼职">
|
|
|
+ <User />
|
|
|
+ </el-icon>
|
|
|
+ <el-icon v-else class="cacp-mr-s" :size="16" title="主职">
|
|
|
+ <UserFilled />
|
|
|
+ </el-icon>
|
|
|
+ <span>{{ data.oguName }}</span>
|
|
|
+ </template>
|
|
|
+ </template>
|
|
|
+ </el-tree>
|
|
|
+ </el-scrollbar>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="selector-right">
|
|
|
+ <div class="selector-toolbar">
|
|
|
+ <span class="count">已选 {{ count }} 个</span><el-link type="primary" @click.prevent="onClear">清空</el-link>
|
|
|
+ </div>
|
|
|
+ <div class="selector-list">
|
|
|
+ <selector-list :dataList="selectedList" @on-delete="onRemove"></selector-list>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="onClose">取消</el-button>
|
|
|
+ <el-button type="primary" @click="onSave">确定</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+</template>
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, computed } from 'vue'
|
|
|
+import type { ElTree } from 'element-plus'
|
|
|
+import type TreeNode from 'element-plus/es/components/tree/src/model/node'
|
|
|
+import { cloneDeep } from 'lodash-es'
|
|
|
+import * as apis from '@/apis/auth'
|
|
|
+import SelectorList from './SelectorList.vue'
|
|
|
+import type { BaseCacpOgu, CacpUser } from '@cacp/ui'
|
|
|
+import { SuccessResultCode } from '@cacp/ui'
|
|
|
+type TreeNodeDataType = BaseCacpOgu & { disabled: boolean; isLeaf: boolean }
|
|
|
+const selectorTreeRef = ref<InstanceType<typeof ElTree>>()
|
|
|
+const props = withDefaults(
|
|
|
+ defineProps<{
|
|
|
+ visible: boolean
|
|
|
+ multiple?: boolean
|
|
|
+ value?: Array<CacpUser>
|
|
|
+ rootPath?: string
|
|
|
+ }>(),
|
|
|
+ {
|
|
|
+ visible: false,
|
|
|
+ multiple: false,
|
|
|
+ value: () => [],
|
|
|
+ rootPath: ''
|
|
|
+ }
|
|
|
+)
|
|
|
+const emits = defineEmits<{
|
|
|
+ (e: 'on-close'): void
|
|
|
+ (e: 'on-save', value: Array<CacpUser>): void
|
|
|
+}>()
|
|
|
+const treeProps = {
|
|
|
+ label: 'oguName',
|
|
|
+ isLeaf: 'isLeaf'
|
|
|
+}
|
|
|
+const rootDeptPath = ref<string>(props.rootPath ?? '')
|
|
|
+const cachedList = ref<Array<CacpUser>>([])
|
|
|
+const selectedKeys = ref<Array<string>>([])
|
|
|
+const selectedList = computed<Array<CacpUser>>(() => {
|
|
|
+ return cachedList.value.filter((o) => selectedKeys.value.includes(o.fullPathName))
|
|
|
+})
|
|
|
+const count = computed<number>(() => {
|
|
|
+ return selectedKeys.value.length
|
|
|
+})
|
|
|
+
|
|
|
+async function onOpen() {
|
|
|
+ selectedKeys.value = props.value ? props.value.map((o) => o.fullPathName) : []
|
|
|
+ cachedList.value = props.value ? cloneDeep(props.value) : []
|
|
|
+}
|
|
|
+function onClose(): void {
|
|
|
+ emits('on-close')
|
|
|
+}
|
|
|
+function onSave(): void {
|
|
|
+ emits('on-save', selectedList.value)
|
|
|
+}
|
|
|
+async function onLazyLoad(
|
|
|
+ node: TreeNode,
|
|
|
+ resolve: (data: Array<TreeNodeDataType>) => void,
|
|
|
+ reject: () => void
|
|
|
+): Promise<void> {
|
|
|
+ if (node.level === 0) {
|
|
|
+ let root: TreeNodeDataType
|
|
|
+ if (props.rootPath) {
|
|
|
+ const result = await apis.getOrganizationByPath(props.rootPath)
|
|
|
+ if (result.code === SuccessResultCode && result.data) {
|
|
|
+ root = Object.assign({}, result.data, { disabled: true, isLeaf: false })
|
|
|
+ rootDeptPath.value = root.fullPathName
|
|
|
+ resolve([root])
|
|
|
+ } else {
|
|
|
+ if (reject) {
|
|
|
+ reject()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const result = await apis.getRootOrganization()
|
|
|
+ if (result.code === SuccessResultCode && result.data) {
|
|
|
+ root = Object.assign({}, result.data, { disabled: true, isLeaf: false })
|
|
|
+ rootDeptPath.value = root.fullPathName
|
|
|
+ resolve([root])
|
|
|
+ } else {
|
|
|
+ if (reject) {
|
|
|
+ reject()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const parent = node.data
|
|
|
+ const resOrg = await apis.getOrganizationListByParent(parent.fullPathName, true)
|
|
|
+ if (resOrg.code !== SuccessResultCode) {
|
|
|
+ reject()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const resUser = await apis.getUserListByParent(parent.fullPathName, true)
|
|
|
+ if (resUser.code !== SuccessResultCode) {
|
|
|
+ if (reject) {
|
|
|
+ reject()
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const orgData = resOrg.data?.map((o) => {
|
|
|
+ return Object.assign({}, o, { disabled: true, isLeaf: false })
|
|
|
+ })
|
|
|
+ const userData = resUser.data?.map((o) => {
|
|
|
+ return Object.assign({}, o, { disabled: false, isLeaf: true })
|
|
|
+ })
|
|
|
+ const children: Array<TreeNodeDataType> = [...(orgData ?? []), ...(userData ?? [])]
|
|
|
+ resolve(children)
|
|
|
+}
|
|
|
+function onCheckChange(data: TreeNodeDataType, checked: boolean) {
|
|
|
+ if (checked) {
|
|
|
+ const user: CacpUser = Object.fromEntries(
|
|
|
+ Object.entries(data)
|
|
|
+ .filter(([k, v]: [string, any]) => k !== 'isLeaf' && k !== 'disabled')
|
|
|
+ .map(([k, v]: [string, any]) => {
|
|
|
+ return [k, v]
|
|
|
+ })
|
|
|
+ ) as CacpUser
|
|
|
+ handleCachedList([user])
|
|
|
+ if (!props.multiple) {
|
|
|
+ selectedKeys.value.splice(0, 1, data.fullPathName)
|
|
|
+ } else {
|
|
|
+ if (!selectedKeys.value.includes(data.fullPathName)) {
|
|
|
+ selectedKeys.value.push(data.fullPathName)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const idx = selectedKeys.value.indexOf(data.fullPathName)
|
|
|
+ if (idx > -1) {
|
|
|
+ selectedKeys.value.splice(idx, 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ handleCheckStatus()
|
|
|
+}
|
|
|
+function onRemove(fullPathName: string): void {
|
|
|
+ const idx = selectedKeys.value.indexOf(fullPathName)
|
|
|
+ if (idx > -1) {
|
|
|
+ selectedKeys.value.splice(idx, 1)
|
|
|
+ }
|
|
|
+ handleCheckStatus()
|
|
|
+}
|
|
|
+
|
|
|
+function onClear(): void {
|
|
|
+ selectedKeys.value = []
|
|
|
+ handleCheckStatus()
|
|
|
+}
|
|
|
+
|
|
|
+function handleCachedList(list: Array<CacpUser>): void {
|
|
|
+ for (const org of list) {
|
|
|
+ const idx = cachedList.value.findIndex((o) => o.fullPathName === org.fullPathName)
|
|
|
+ if (idx > -1) {
|
|
|
+ cachedList.value.splice(idx, 1)
|
|
|
+ }
|
|
|
+ cachedList.value.push(org)
|
|
|
+ }
|
|
|
+}
|
|
|
+function handleCheckStatus(): void {
|
|
|
+ if (selectorTreeRef.value) {
|
|
|
+ selectorTreeRef.value.setCheckedKeys(selectedKeys.value, false)
|
|
|
+ }
|
|
|
+}
|
|
|
+async function querySearch(query: string, cb: any) {
|
|
|
+ if (query !== '') {
|
|
|
+ const res = await apis.queryUserListByCondition(rootDeptPath.value, query)
|
|
|
+ if (res.code === SuccessResultCode) {
|
|
|
+ cb(res.data ?? [])
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function onSelect(data: CacpUser, checked: boolean) {
|
|
|
+ if (checked) {
|
|
|
+ handleCachedList([data])
|
|
|
+ if (!props.multiple) {
|
|
|
+ selectedKeys.value.splice(0, 1, data.fullPathName)
|
|
|
+ } else {
|
|
|
+ if (!selectedKeys.value.includes(data.fullPathName)) {
|
|
|
+ selectedKeys.value.push(data.fullPathName)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const idx = selectedKeys.value.indexOf(data.fullPathName)
|
|
|
+ if (idx > -1) {
|
|
|
+ selectedKeys.value.splice(idx, 1)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ handleCheckStatus()
|
|
|
+}
|
|
|
+</script>
|
|
|
+<style lang="less" scoped>
|
|
|
+.selector {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ border: 1px solid var(--el-border-color);
|
|
|
+ border-radius: var(--el-border-radius-base);
|
|
|
+ min-height: 260px;
|
|
|
+ max-height: 400px;
|
|
|
+ align-items: stretch;
|
|
|
+ &-left {
|
|
|
+ width: 280px;
|
|
|
+ border-right: 1px solid var(--el-border-color);
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ overflow: hidden;
|
|
|
+ :deep(.el-input-group__append),
|
|
|
+ :deep(.el-input__wrapper) {
|
|
|
+ box-shadow: none;
|
|
|
+ border-radius: 0;
|
|
|
+ }
|
|
|
+ // :deep(.el-input__wrapper) {
|
|
|
+ // padding-right: 0;
|
|
|
+ // }
|
|
|
+ :deep(.el-input) {
|
|
|
+ height: 32px;
|
|
|
+ border-bottom: 1px solid var(--el-input-border-color);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &-search {
|
|
|
+ line-height: 32px;
|
|
|
+ }
|
|
|
+ &-tree {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ }
|
|
|
+ &-middle {
|
|
|
+ width: 320px;
|
|
|
+ border-right: 1px solid var(--el-border-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ &-right {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ }
|
|
|
+ &-list {
|
|
|
+ flex: 1;
|
|
|
+ }
|
|
|
+ &-toolbar {
|
|
|
+ line-height: 32px;
|
|
|
+ height: 32px;
|
|
|
+ padding: 0 var(--cacp-padding-space-s);
|
|
|
+ font-size: var(--font-size-s);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ border-bottom: 1px solid var(--el-border-color);
|
|
|
+ justify-content: space-between;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|