You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
602 lines
16 KiB
602 lines
16 KiB
<template> |
|
<div class="app-container addrbook-page"> |
|
<!-- 左侧树区域 --> |
|
<div class="addrbook-left"> |
|
<div |
|
v-for="(item, idx) in leftTabList" |
|
:key="idx" |
|
class="latest-contacts" |
|
:class="{ active: item.flag && isLatestContacts }" |
|
@click="item.fn()" |
|
> |
|
<i :class="item.icon" /> |
|
<span>{{ item.label }}</span> |
|
</div> |
|
<el-tree |
|
ref="orgTree" |
|
:data="treeData" |
|
:props="treeProps" |
|
:default-expanded-keys="defaultExpandedKeys" |
|
highlight-current |
|
node-key="id" |
|
@node-click="handleNodeClick" |
|
/> |
|
</div> |
|
<!-- 中间成员列表 --> |
|
<div class="addrbook-center"> |
|
<div class="search-box"> |
|
<el-input |
|
v-model="searchText" |
|
placeholder="搜索联系人" |
|
suffix-icon="el-icon-search" |
|
clearable |
|
@keyup.enter.native="handleSearch" |
|
@clear="handleClearSearch" |
|
/> |
|
</div> |
|
<div class="center-header"> |
|
<span class="structure-name">{{ currentStructureName }}</span> |
|
<i |
|
class="el-icon-refresh refresh-btn" |
|
title="刷新" |
|
@click="handleRefresh" |
|
/> |
|
</div> |
|
<!-- 滚动事件只在常用联系人场景生效 --> |
|
<div v-loading="loading" class="member-list" @scroll="handleScroll"> |
|
<div |
|
v-for="member in memberList" |
|
:key="member.id" |
|
class="member-item" |
|
:class="{ |
|
active: currentUser.id === member.id, |
|
offline: !member.online, |
|
}" |
|
@click="setSelectUser(member)" |
|
> |
|
<el-avatar |
|
:size="32" |
|
:src="getAvatarUrl(member.avatar)" |
|
icon="el-icon-user-solid" |
|
/> |
|
<span class="member-name">{{ member.name }}</span> |
|
<span v-if="member.online" class="online-dot" /> |
|
</div> |
|
<el-empty |
|
v-if="!loading && !memberList.length" |
|
description="暂无成员" |
|
/> |
|
<!-- 分页底部提示,仅常用联系人展示 --> |
|
<div |
|
v-if="isLatestContacts && memberList.length > 0" |
|
class="load-more-tip" |
|
> |
|
<span v-if="loadMoreLoading">加载更多...</span> |
|
<span v-if="noMore && !loadMoreLoading">没有更多数据了</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<!-- 右侧详情 --> |
|
<div class="addrbook-right"> |
|
<div v-if="currentUser" class="detail-panel"> |
|
<div class="avatar-section"> |
|
<el-avatar |
|
:size="80" |
|
:src="getAvatarUrl(currentUser.avatar)" |
|
icon="el-icon-user-solid" |
|
/> |
|
</div> |
|
<div class="info-section"> |
|
<div v-for="info in userInfoList" :key="info.key" class="info-row"> |
|
<i :class="info.icon" /> |
|
<span class="label">{{ info.label }}</span> |
|
<el-tooltip |
|
:content=" |
|
info.key === 'online' |
|
? currentUser.online |
|
? '正常' |
|
: '离线' |
|
: currentUser[info.key] |
|
" |
|
placement="top" |
|
> |
|
<span |
|
class="value" |
|
:class=" |
|
info.key === 'online' |
|
? currentUser.online |
|
? 'online' |
|
: 'offline' |
|
: '' |
|
" |
|
> |
|
{{ |
|
info.key === "online" |
|
? currentUser.online |
|
? "正常" |
|
: "离线" |
|
: currentUser[info.key] |
|
}} |
|
</span> |
|
</el-tooltip> |
|
</div> |
|
</div> |
|
<div class="action-section"> |
|
<el-button |
|
type="primary" |
|
icon="el-icon-chat-dot-round" |
|
@click="sendMessage" |
|
> |
|
发消息 |
|
</el-button> |
|
</div> |
|
</div> |
|
<div v-else class="empty-detail"> |
|
<el-empty description="请选择联系人" /> |
|
</div> |
|
</div> |
|
</div> |
|
</template> |
|
|
|
<script> |
|
import { |
|
getMessagesLatestContacts, |
|
getGroupsList, |
|
getGroupsListUser, |
|
searchUsers, |
|
} from "@/api/contacts/index.js"; |
|
export default { |
|
name: "Contacts", |
|
data() { |
|
return { |
|
treeData: [], |
|
treeProps: { label: "name", children: "child" }, |
|
defaultExpandedKeys: [], |
|
currentGroupId: null, |
|
currentStructureName: "", |
|
memberList: [], |
|
currentUser: null, |
|
searchText: "", |
|
loading: false, |
|
isLatestContacts: false, |
|
userGroupId: null, |
|
// 分页配置(仅常用联系人/搜索使用) |
|
pageNum: 1, |
|
pageSize: 15, |
|
noMore: false, |
|
loadMoreLoading: false, |
|
// 左侧顶部菜单配置 |
|
leftTabList: [ |
|
{ |
|
label: "常用联系人", |
|
icon: "el-icon-time", |
|
flag: true, |
|
fn: () => this.showLatestContacts(), |
|
}, |
|
{ |
|
label: "组织架构", |
|
icon: "el-icon-office-building", |
|
flag: false, |
|
fn: () => this.fetchOrgTree(), |
|
}, |
|
], |
|
// 用户详情配置 |
|
userInfoList: [ |
|
{ label: "姓名", icon: "el-icon-user", key: "name" }, |
|
{ label: "邮箱", icon: "el-icon-message", key: "email" }, |
|
{ label: "部门", icon: "el-icon-office-building", key: "full_group" }, |
|
{ label: "职位", icon: "el-icon-s-custom", key: "role" }, |
|
{ label: "手机", icon: "el-icon-phone", key: "phone" }, |
|
{ label: "状态", icon: "el-icon-info", key: "online" }, |
|
], |
|
}; |
|
}, |
|
computed: { |
|
MINIO_BASE() { |
|
return this.$store.state.user.netConfig.MINIO_ENDPOINT_HTTPS || ""; |
|
}, |
|
}, |
|
mounted() { |
|
this.fetchOrgTree(); |
|
this.showLatestContacts(); |
|
}, |
|
methods: { |
|
// 统一获取头像地址 |
|
getAvatarUrl(avatar) { |
|
if (!avatar) return ""; |
|
return avatar.startsWith("http") ? avatar : `${this.MINIO_BASE}${avatar}`; |
|
}, |
|
// 选中联系人公共方法 |
|
setSelectUser(user) { |
|
this.currentUser = { ...user }; |
|
}, |
|
// 重置分页参数 |
|
resetPage() { |
|
this.pageNum = 1; |
|
this.noMore = false; |
|
this.memberList = []; |
|
}, |
|
// 滚动触底 |
|
handleScroll(e) { |
|
// 组织架构不触发分页加载,直接返回 |
|
if (!this.isLatestContacts) return; |
|
const el = e.target; |
|
const scrollTop = el.scrollTop; |
|
const scrollHeight = el.scrollHeight; |
|
const clientHeight = el.clientHeight; |
|
// 距离底部50px触发加载 |
|
if (scrollTop + clientHeight >= scrollHeight - 50) { |
|
this.loadNextPage(); |
|
} |
|
}, |
|
// 加载下一页 |
|
loadNextPage() { |
|
// 多重拦截:加载中 / 无更多数据 |
|
if (this.loadMoreLoading || this.noMore || this.loading) return; |
|
this.pageNum++; |
|
this.loadMemberList(false); |
|
}, |
|
// 统一加载列表 |
|
// isRefresh: true=第一页刷新 false=滚动追加 |
|
async loadMemberList(isRefresh = true) { |
|
if (isRefresh) { |
|
this.loading = true; |
|
this.resetPage(); |
|
} else { |
|
this.loadMoreLoading = true; |
|
} |
|
try { |
|
const keyword = this.searchText.trim(); |
|
let resList = []; |
|
// 搜索场景(分页) |
|
if (keyword) { |
|
const { data } = await searchUsers({ |
|
search_key: keyword, |
|
user_ids: [], |
|
page: this.pageNum, |
|
size: this.pageSize, |
|
}); |
|
resList = data.list || []; |
|
this.isLatestContacts = true; |
|
this.currentStructureName = "搜索结果"; |
|
this.currentGroupId = null; |
|
this.$refs.orgTree.setCurrentKey(null); |
|
} else if (this.isLatestContacts) { |
|
// 常用联系人(分页) |
|
const { data } = await getMessagesLatestContacts({ |
|
scene: 1, |
|
page: this.pageNum, |
|
size: this.pageSize, |
|
}); |
|
resList = data.list || []; |
|
} else { |
|
// 组织架构:不分页,一次性全量获取 |
|
const { data } = await getGroupsListUser({ |
|
group_id: this.currentGroupId, |
|
}); |
|
resList = data.list || []; |
|
} |
|
|
|
const formatList = resList.map((item) => ({ |
|
...item, |
|
online: [1, true].includes(item.online), |
|
})); |
|
|
|
if (isRefresh) { |
|
this.memberList = formatList; |
|
} else { |
|
this.memberList.push(...formatList); |
|
} |
|
|
|
// 仅常用/搜索场景判断是否还有下一页 |
|
if (this.isLatestContacts) { |
|
this.noMore = formatList.length < this.pageSize; |
|
} else { |
|
this.noMore = true; // 组织架构永远无更多 |
|
} |
|
|
|
// 首次加载自动选中第一条 |
|
if (isRefresh && this.memberList.length) { |
|
this.setSelectUser(this.memberList[0]); |
|
} |
|
} catch (err) { |
|
console.error("加载列表失败", err); |
|
if (isRefresh) this.memberList = []; |
|
} finally { |
|
this.loading = false; |
|
this.loadMoreLoading = false; |
|
} |
|
}, |
|
// 树形结构相关 |
|
async fetchOrgTree() { |
|
try { |
|
const { data } = await getGroupsList(); |
|
this.treeData = data.list; |
|
this.defaultExpandedKeys = this.treeData.map((node) => node.id); |
|
if (!this.userGroupId) return; |
|
const parentKeys = this.findParentKeys(this.treeData, this.userGroupId); |
|
this.$nextTick(() => { |
|
this.$refs.orgTree.setCurrentKey(this.userGroupId); |
|
const node = this.findNodeById(this.treeData, this.userGroupId); |
|
if (node) { |
|
this.currentStructureName = node.name; |
|
this.currentGroupId = node.id; |
|
this.loadMemberList(true); |
|
} |
|
}); |
|
} catch (err) { |
|
console.error("获取组织架构失败", err); |
|
} |
|
}, |
|
findParentKeys(tree, targetId, parents = []) { |
|
for (const node of tree) { |
|
if (node.id === targetId) return [...parents, node.id]; |
|
if (node.child && node.child.length) { |
|
const res = this.findParentKeys(node.child, targetId, [ |
|
...parents, |
|
node.id, |
|
]); |
|
if (res.length) return res; |
|
} |
|
} |
|
return []; |
|
}, |
|
findNodeById(tree, id) { |
|
for (const node of tree) { |
|
if (node.id === id) return node; |
|
if (node.child) { |
|
const res = this.findNodeById(node.child, id); |
|
if (res) return res; |
|
} |
|
} |
|
return null; |
|
}, |
|
// 树点击(组织架构) |
|
handleNodeClick(data) { |
|
this.isLatestContacts = false; |
|
this.currentStructureName = data.name; |
|
this.currentGroupId = data.id; |
|
this.currentUser = null; |
|
this.loadMemberList(true); |
|
}, |
|
// 兼容旧接口 |
|
fetchMembers(groupId) { |
|
this.currentGroupId = groupId; |
|
this.loadMemberList(true); |
|
}, |
|
// 常用联系人入口 |
|
showLatestContacts() { |
|
this.isLatestContacts = true; |
|
this.currentStructureName = "最近联系人"; |
|
this.currentGroupId = null; |
|
this.currentUser = null; |
|
this.$refs.orgTree.setCurrentKey(null); |
|
this.loadMemberList(true); |
|
}, |
|
// 搜索 |
|
handleSearch() { |
|
const keyword = this.searchText.trim(); |
|
if (!keyword) return this.handleClearSearch(); |
|
this.loadMemberList(true); |
|
}, |
|
// 清空搜索 |
|
handleClearSearch() { |
|
this.searchText = ""; |
|
if (this.isLatestContacts) { |
|
this.showLatestContacts(); |
|
} else if (this.currentGroupId) { |
|
this.loadMemberList(true); |
|
} else if (this.userGroupId) { |
|
this.currentGroupId = this.userGroupId; |
|
this.loadMemberList(true); |
|
} |
|
}, |
|
// 刷新 |
|
handleRefresh() { |
|
this.loadMemberList(true); |
|
}, |
|
// 跳转发消息 |
|
sendMessage() { |
|
if (!this.currentUser) return; |
|
this.$router.push({ |
|
path: "/message", |
|
query: { |
|
contactId: this.currentUser.id, |
|
contactName: this.currentUser.name, |
|
scene: 1, |
|
}, |
|
}); |
|
}, |
|
}, |
|
}; |
|
</script> |
|
|
|
<style lang="scss" scoped> |
|
.addrbook-page { |
|
display: flex; |
|
height: 100%; |
|
} |
|
// 左侧 |
|
.addrbook-left { |
|
width: 240px; |
|
border-right: 1px solid #ebeef5; |
|
overflow-y: auto; |
|
.latest-contacts { |
|
display: flex; |
|
align-items: center; |
|
gap: 8px; |
|
padding: 12px 16px; |
|
height: 61px; |
|
border-bottom: 1px solid #ebeef5; |
|
cursor: pointer; |
|
font-size: 14px; |
|
color: #606266; |
|
transition: all 0.2s; |
|
i { |
|
font-size: 16px; |
|
} |
|
&:hover, |
|
&.active { |
|
background: #e5f1fb; |
|
color: #409eff; |
|
} |
|
} |
|
::v-deep .el-tree { |
|
background: transparent; |
|
.el-tree-node__content { |
|
height: 40px; |
|
} |
|
.el-tree-node:focus > .el-tree-node__content { |
|
background-color: #e5f1fb; |
|
} |
|
.el-tree-node__content:hover { |
|
background-color: #f0f7ff; |
|
} |
|
} |
|
} |
|
// 中间列表 |
|
.addrbook-center { |
|
width: 280px; |
|
border-right: 1px solid #ebeef5; |
|
display: flex; |
|
flex-direction: column; |
|
.search-box, |
|
.center-header { |
|
padding: 12px 16px; |
|
border-bottom: 1px solid #ebeef5; |
|
height: 61px; |
|
} |
|
.center-header { |
|
display: flex; |
|
align-items: center; |
|
justify-content: space-between; |
|
.structure-name { |
|
font-size: 14px; |
|
font-weight: 500; |
|
color: #303133; |
|
} |
|
.refresh-btn { |
|
font-size: 16px; |
|
color: #909399; |
|
cursor: pointer; |
|
&:hover { |
|
color: #409eff; |
|
} |
|
} |
|
} |
|
.member-list { |
|
flex: 1; |
|
overflow-y: auto; |
|
.member-item { |
|
display: flex; |
|
align-items: center; |
|
gap: 10px; |
|
padding: 8px 16px; |
|
cursor: pointer; |
|
transition: background 0.2s; |
|
&:hover { |
|
background: #f5f7fa; |
|
} |
|
&.active { |
|
background: #e5f1fb; |
|
} |
|
&.offline .member-name { |
|
color: #909399; |
|
} |
|
.member-name { |
|
flex: 1; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
white-space: nowrap; |
|
font-size: 14px; |
|
color: #303133; |
|
} |
|
.online-dot { |
|
width: 8px; |
|
height: 8px; |
|
border-radius: 50%; |
|
background: #67c23a; |
|
flex-shrink: 0; |
|
} |
|
} |
|
.load-more-tip { |
|
padding: 12px; |
|
text-align: center; |
|
font-size: 12px; |
|
color: #909399; |
|
} |
|
} |
|
} |
|
// 右侧详情 |
|
.addrbook-right { |
|
flex: 1; |
|
display: flex; |
|
flex-direction: column; |
|
align-items: center; |
|
justify-content: center; |
|
.detail-panel { |
|
width: 100%; |
|
max-width: 400px; |
|
padding: 40px 24px; |
|
.avatar-section { |
|
display: flex; |
|
justify-content: center; |
|
margin-bottom: 32px; |
|
} |
|
.info-section { |
|
.info-row { |
|
display: flex; |
|
align-items: center; |
|
justify-content: space-between; |
|
gap: 8px; |
|
padding: 12px 0; |
|
border-bottom: 1px solid #f0f0f0; |
|
i { |
|
font-size: 16px; |
|
color: #009393; |
|
width: 20px; |
|
text-align: center; |
|
} |
|
.label { |
|
width: 70px; |
|
font-size: 14px; |
|
color: #909399; |
|
flex-shrink: 0; |
|
} |
|
.value { |
|
flex: 1; |
|
font-size: 14px; |
|
color: #303133; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
white-space: nowrap; |
|
text-align: right; |
|
&.online { |
|
color: #67c23a; |
|
} |
|
&.offline { |
|
color: #909399; |
|
} |
|
} |
|
} |
|
} |
|
.action-section { |
|
display: flex; |
|
gap: 16px; |
|
justify-content: center; |
|
margin-top: 32px; |
|
.el-button { |
|
min-width: 120px; |
|
} |
|
} |
|
} |
|
.empty-detail { |
|
width: 100%; |
|
height: 100%; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
} |
|
} |
|
</style> |