<script lang="ts" setup>
|
import type { ScrollActionType, TreeActionType } from '@jnpf/ui';
|
|
import { computed, nextTick, onMounted, reactive, ref, toRefs, unref } from 'vue';
|
|
import { useGlobSetting } from '@jnpf/hooks';
|
import { BasicTree, ScrollContainer } from '@jnpf/ui';
|
|
import { Search } from '@vben/icons';
|
|
import { getOrgAndPosSelector } from '#/api/permission/organize';
|
import { getUserList } from '#/api/permission/user';
|
import { $t } from '#/locales';
|
|
defineOptions({ name: 'Contacts' });
|
defineProps({
|
isTwinkle: { type: Boolean, default: false },
|
});
|
|
const emit = defineEmits(['toReply']);
|
|
interface State {
|
finish: boolean;
|
lastList: any[];
|
lastLoading: boolean;
|
orgLoading: boolean;
|
loading: boolean;
|
orgQuery: any;
|
query: any;
|
treeData: any[];
|
activeData: any;
|
isSearch: boolean;
|
}
|
|
const state = reactive<State>({
|
finish: false,
|
lastList: [],
|
lastLoading: false,
|
orgLoading: false,
|
loading: false,
|
orgQuery: {
|
id: '0',
|
type: 'organize',
|
},
|
query: {
|
currentPage: 1,
|
hasPage: 1,
|
keyword: '',
|
pageSize: 20,
|
enabledMark: 1,
|
organizeId: '',
|
positionId: '',
|
},
|
isSearch: false,
|
treeData: [],
|
activeData: {},
|
});
|
const { isSearch, query, lastList, lastLoading, activeData } = toRefs(state);
|
const orgTreeRef = ref<Nullable<TreeActionType>>(null);
|
const infiniteBody = ref<Nullable<ScrollActionType>>(null);
|
const globSetting = useGlobSetting();
|
const apiUrl = ref(globSetting.apiURL);
|
|
const getOrgTreeBindValue = computed(() => {
|
const key = Date.now();
|
const data: any = {
|
key,
|
loadData: onLoadData,
|
loading: state.orgLoading,
|
onSelect: handleOrgSelect,
|
treeData: state.treeData,
|
};
|
return data;
|
});
|
|
function handleSearch() {
|
if (state.lastLoading) return;
|
state.activeData = {};
|
state.isSearch = !!state.query.keyword;
|
state.query.organizeId = '';
|
state.query.positionId = '';
|
state.query.currentPage = 1;
|
state.finish = false;
|
state.lastList = [];
|
if (state.isSearch) {
|
nextTick(() => {
|
bindScroll();
|
});
|
getLastList();
|
} else {
|
state.orgQuery.id = '0';
|
state.orgQuery.type = 'organize';
|
getOrgList();
|
}
|
}
|
function bindScroll() {
|
const bodyRef = infiniteBody.value;
|
const vBody = bodyRef?.getScrollWrap();
|
vBody?.addEventListener('scroll', () => {
|
if (vBody.scrollTop > 0 && vBody.scrollHeight - vBody.clientHeight - vBody.scrollTop <= 200 && !state.lastLoading && !state.finish) {
|
state.query.currentPage += 1;
|
getLastList();
|
}
|
});
|
}
|
function getLastList() {
|
state.lastLoading = true;
|
getUserList(state.query)
|
.then((res) => {
|
const list = res.data.list;
|
state.finish = list.length < state.query.pageSize;
|
state.lastList = [...state.lastList, ...list];
|
state.lastLoading = false;
|
})
|
.catch(() => {
|
state.lastLoading = false;
|
});
|
}
|
function getOrgTree() {
|
const tree = unref(orgTreeRef);
|
if (!tree) {
|
throw new Error('tree is null!');
|
}
|
return tree;
|
}
|
function handleOrgSelect(keys) {
|
if (keys.length === 0 || state.activeData.id === keys[0]) return;
|
const data: any = getOrgTree().getSelectedNode(keys[0]);
|
state.activeData = data;
|
if (data.type === 'position') {
|
state.query.positionId = data.id;
|
} else {
|
state.query.organizeId = data.id;
|
state.query.positionId = '';
|
}
|
state.query.currentPage = 1;
|
state.finish = false;
|
state.lastList = [];
|
getLastList();
|
bindScroll();
|
}
|
function onLoadData(node) {
|
state.orgQuery.id = node.id;
|
state.orgQuery.type = node.type;
|
return new Promise((resolve: (value?: unknown) => void) => {
|
getOrgAndPosSelector(state.orgQuery).then((res) => {
|
const list = res.data.list;
|
getOrgTree().updateNodeByKey(node.eventKey, { isLeaf: list.length === 0, children: list });
|
resolve();
|
});
|
});
|
}
|
function handleNodeClick(item) {
|
if (state.activeData.id === item.id) return;
|
state.activeData = { ...item, type: 'user' };
|
}
|
function toReply() {
|
const data = {
|
account: state.activeData.account,
|
headIcon: state.activeData.headIcon,
|
id: state.activeData.id,
|
realName: state.activeData.realName,
|
latestDate: 0,
|
latestMessage: '',
|
messageType: 'text',
|
unreadMessage: 0,
|
};
|
emit('toReply', data);
|
}
|
function getOrgList() {
|
state.orgLoading = true;
|
getOrgAndPosSelector(state.orgQuery)
|
.then((res) => {
|
state.treeData = res.data.list;
|
if (state.treeData.length > 0 && state.orgQuery.id == '0') {
|
nextTick(() => {
|
const keys = [state.treeData[0].id];
|
getOrgTree().setSelectedKeys(keys);
|
handleOrgSelect(keys);
|
});
|
}
|
state.orgLoading = false;
|
})
|
.catch(() => {
|
state.orgLoading = false;
|
});
|
}
|
|
onMounted(() => {
|
getOrgList();
|
});
|
</script>
|
<template>
|
<div class="contacts-pane common-pane">
|
<div class="org-pane" v-if="!isSearch">
|
<div class="pane-header">
|
<a-input :placeholder="$t('common.drawerSearchText')" allow-clear v-model:value="query.keyword" :bordered="false" @keyup.enter="handleSearch">
|
<template #suffix>
|
<Search class="size-4" @click="handleSearch" />
|
</template>
|
</a-input>
|
</div>
|
<div class="pane-main">
|
<BasicTree ref="orgTreeRef" class="tree-main" v-bind="getOrgTreeBindValue" />
|
</div>
|
</div>
|
<div class="user-pane">
|
<div class="pane-header">
|
<span v-if="!isSearch">用户列表</span>
|
<a-input :placeholder="$t('common.drawerSearchText')" allow-clear v-model:value="query.keyword" :bordered="false" @keyup.enter="handleSearch" v-else>
|
<template #suffix>
|
<Search class="size-4" @click="handleSearch" />
|
</template>
|
</a-input>
|
</div>
|
<div class="pane-main">
|
<ScrollContainer class="user-list" ref="infiniteBody" v-loading="lastLoading && query.currentPage === 1">
|
<div
|
v-for="(item, i) in lastList"
|
:key="i"
|
class="user-list-item"
|
@click="handleNodeClick(item)"
|
:class="{ 'user-list-item-active': activeData.id === item.id }">
|
<div class="user-list-item-main">
|
<a-avatar :size="36" :src="apiUrl + item.headIcon" />
|
<div class="user-list-txt">
|
<p class="title">{{ item.realName }}/{{ item.account }}</p>
|
<p class="position">{{ item.position }}</p>
|
</div>
|
</div>
|
</div>
|
<jnpf-empty v-if="lastList.length === 0" />
|
</ScrollContainer>
|
</div>
|
</div>
|
<div class="info-pane">
|
<template v-if="activeData?.type">
|
<div class="user-info" v-if="activeData.type === 'user'">
|
<div class="user-info-header">
|
<div class="user-info-header-txt">
|
<div class="txt-name" :title="activeData.fullName">{{ activeData.fullName }}</div>
|
<div class="txt-position" :title="activeData.position">{{ activeData.position }}</div>
|
</div>
|
<a-avatar :size="70" :src="apiUrl + activeData.headIcon" />
|
</div>
|
<div class="user-info-cell">
|
<div class="user-info-cell-label">手机</div>
|
<div class="user-info-cell-value">{{ activeData.mobilePhone || '' }}</div>
|
</div>
|
<div class="user-info-cell">
|
<div class="user-info-cell-label">邮箱</div>
|
<div class="user-info-cell-value">{{ activeData.email || '' }}</div>
|
</div>
|
<div class="user-info-cell">
|
<div class="user-info-cell-label">组织</div>
|
<div class="user-info-cell-value">{{ activeData.organize || '' }}</div>
|
</div>
|
<div class="user-info-btns">
|
<a-button type="primary" @click="toReply()">发消息</a-button>
|
</div>
|
</div>
|
<div class="org-info user-info" v-else>
|
<div class="org-info-subTitle" v-if="activeData.type === 'position'">{{ activeData.organize }}</div>
|
<div>{{ activeData.fullName }}</div>
|
</div>
|
</template>
|
<jnpf-empty v-else />
|
</div>
|
</div>
|
</template>
|