u
This commit is contained in:
1 parent
52b16ed2b0
commit
c26d1b3c81
15 files changed
+315
-42
No files matched your search
+8
@@ -6,8 +6,10 @@ import cn.link56.company.model.dto.AddCompanyUserDTO;
|
||||
import cn.link56.company.model.dto.DeleteCompanyUserDTO;
|
||||
import cn.link56.company.model.dto.QueryCompanyUserDTO;
|
||||
import cn.link56.company.model.dto.UpdateCompanyUserDTO;
|
||||
import cn.link56.company.model.vo.ChatUserVO;
|
||||
import cn.link56.company.model.vo.QueryCompanyUserVO;
|
||||
import cn.link56.company.service.CompanyUserService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -43,4 +45,10 @@ public class CompanyUserController {
|
||||
companyUserService.deleteCompanyUser(deleteCompanyUserDTO);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@GetMapping("/chat/{companyId}")
|
||||
public Result<List<ChatUserVO>> listCompanyChatUsers(@PathVariable("companyId") Long companyId){
|
||||
List<ChatUserVO> data = companyUserService.listCompanyChatUsers(companyId);
|
||||
return Result.success(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cn.link56.company.model.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ChatUserVO {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String position;
|
||||
private String avatar;
|
||||
private Boolean isOnline;
|
||||
}
|
||||
+23
@@ -12,7 +12,9 @@ import cn.link56.company.model.dto.QueryCompanyUserDTO;
|
||||
import cn.link56.company.model.dto.UpdateCompanyUserDTO;
|
||||
import cn.link56.company.model.po.Company;
|
||||
import cn.link56.company.model.po.CompanyUser;
|
||||
import cn.link56.company.model.vo.ChatUserVO;
|
||||
import cn.link56.company.model.vo.QueryCompanyUserVO;
|
||||
import cn.link56.im.storage.SessionManager;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
@@ -24,6 +26,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -304,4 +307,24 @@ public class CompanyUserService extends ServiceImpl<CompanyUserMapper, CompanyUs
|
||||
}
|
||||
|
||||
|
||||
public List<ChatUserVO> listCompanyChatUsers(Long companyId) {
|
||||
LambdaQueryWrapper<CompanyUser> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(CompanyUser::getCompanyId, companyId)
|
||||
.eq(CompanyUser::getCanuse, "1");
|
||||
|
||||
List<CompanyUser> companyUsers = companyUserMapper.selectList(queryWrapper);
|
||||
|
||||
List<ChatUserVO> result = new ArrayList<>(companyUsers.size());
|
||||
|
||||
companyUsers.forEach(item -> {
|
||||
ChatUserVO chatUserVO = new ChatUserVO();
|
||||
BeanUtils.copyProperties(item, chatUserVO);
|
||||
chatUserVO.setIsOnline(SessionManager.isOnline(item.getId()));
|
||||
|
||||
result.add(chatUserVO);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -216,9 +216,6 @@ public class MessageHandler {
|
||||
groupMessageVO.setSenderName(senderName);
|
||||
groupMessageVO.setSenderAvatar(senderAvatar);
|
||||
|
||||
// 通知建立群组关系
|
||||
|
||||
|
||||
// 查询该群在线的用户
|
||||
List<Long> onlineGroupUsers = imGroupUserService.getGroupOnlineUserIds(groupId);
|
||||
MessageWriter.broadcastToUsers(onlineGroupUsers,MessageResponse.success(MessageTypeEnums.GROUP,groupId).setData(groupMessageVO));
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
VITE_SERVER_BASEURL = 'http://127.0.0.1:8084'
|
||||
VITE_WEBSOCKET_BASEURL = 'ws://127.0.0.1:8090/ws'
|
||||
VITE_SERVER_BASEURL = 'http://192.168.1.137:8084'
|
||||
VITE_WEBSOCKET_BASEURL = 'ws://192.168.1.137:8090/ws'
|
||||
@@ -0,0 +1,204 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CompanyChatUser } from '@/types/company/user'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { listCompanyChatUsersApi } from '@/request/api/company/user'
|
||||
import { createFriendApi } from '@/request/api/im'
|
||||
import { useUserStore } from '@/store/user'
|
||||
|
||||
const props = defineProps({
|
||||
companyId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const loading = ref(true)
|
||||
const users = ref<CompanyChatUser[]>([])
|
||||
|
||||
function onPopoverShow() {
|
||||
loading.value = true
|
||||
|
||||
listCompanyChatUsersApi(props.companyId).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
users.value = res.data
|
||||
}
|
||||
else {
|
||||
ElMessage.error(res.message || '获取聊天用户失败,请稍后再试')
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
ElMessage.error('获取聊天用户失败,请稍后再试')
|
||||
}).finally(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onPopoverHide() {
|
||||
loading.value = true
|
||||
users.value = []
|
||||
}
|
||||
|
||||
function selectUser(user: CompanyChatUser) {
|
||||
console.log('选择用户:', user)
|
||||
if (userStore.user.id === user.id) {
|
||||
ElMessage.warning('无法与自己发起聊天')
|
||||
return
|
||||
}
|
||||
|
||||
createFriendApi({
|
||||
friendId: user.id,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
router.push(`/im/private/${user.id}`)
|
||||
}
|
||||
else {
|
||||
ElMessage.error(res.message || '发起聊天失败,请稍后再试')
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
ElMessage.error('发起聊天失败,请稍后再试')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-popover
|
||||
placement="bottom-start"
|
||||
trigger="click"
|
||||
width="280"
|
||||
:popper-style="{
|
||||
padding: '0px',
|
||||
}"
|
||||
|
||||
@after-enter="onPopoverShow"
|
||||
@after-leave="onPopoverHide"
|
||||
>
|
||||
<div v-if="loading" class="loading-wrapper">
|
||||
<SvgIcon icon="svg-spinners:ring-resize" :size="20" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="users.length === 0" class="empty-wrapper">
|
||||
<SvgIcon icon="streamline:interface-content-file-empty-common-file" :size="20" />
|
||||
<span class="empty-tip">
|
||||
暂无聊天用户
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="user-list">
|
||||
<div
|
||||
v-for="user in users"
|
||||
:key="user.id"
|
||||
class="user-item"
|
||||
@click="selectUser(user)"
|
||||
>
|
||||
<div class="avatar-wrapper">
|
||||
<SafeImage class="avatar" :url="user.avatar" :size="40" round />
|
||||
<span
|
||||
class="status"
|
||||
:class="{ online: user.isOnline }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<div class="name">
|
||||
{{ user.name }}
|
||||
</div>
|
||||
<div v-if="user.position" class="title">
|
||||
{{ user.position }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #reference>
|
||||
<slot />
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.loading-wrapper,
|
||||
.empty-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.empty-wrapper {
|
||||
flex-direction: column;
|
||||
.empty-tip {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-search {
|
||||
border-bottom: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-fill-color-light);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 在线状态点 */
|
||||
.status {
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
bottom: -2px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background-color: #c0c4cc; // 离线
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
|
||||
.status.online {
|
||||
background-color: #67c23a; // 在线(Element Plus 绿色)
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -60,7 +60,7 @@ function isActive(path: string) {
|
||||
<div class="action">
|
||||
<i-tdesign:search class="action-item" />
|
||||
|
||||
<i-tdesign:notification v-if="userStore.isLogined" class="action-item" @click="navigateTo('/im')" />
|
||||
<SvgIcon icon="solar:chat-round-line-linear" class="action-item" @click="navigateTo('/im')" />
|
||||
|
||||
<template v-if="userStore.isLogined">
|
||||
<el-divider direction="vertical" />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AddCompanyUserParams, CompanyUser, DeleteCompanyUserParams, QueryCompanyUserParams, UpdateCompanyUserParams } from '@/types/company/user'
|
||||
import type { AddCompanyUserParams, CompanyChatUser, CompanyUser, DeleteCompanyUserParams, QueryCompanyUserParams, UpdateCompanyUserParams } from '@/types/company/user'
|
||||
import { httpGet, httpPostJson } from '@/request/utils'
|
||||
|
||||
export function queryCompanyUserApi(params: QueryCompanyUserParams) {
|
||||
@@ -13,3 +13,7 @@ export function updateCompanyUserApi(params: UpdateCompanyUserParams) {
|
||||
export function deleteCompanyUserApi(params: DeleteCompanyUserParams) {
|
||||
return httpPostJson<void>('/company/user/delete', params)
|
||||
}
|
||||
|
||||
export function listCompanyChatUsersApi(companyId: number) {
|
||||
return httpGet<CompanyChatUser[]>(`/company/user/chat/${companyId}`)
|
||||
}
|
||||
@@ -13,6 +13,8 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AnchorMenu: typeof import('./../components/anchor-menu/index.vue')['default']
|
||||
AreaSelect: typeof import('./../components/AreaSelect.vue')['default']
|
||||
ChatCompanyUserSelect: typeof import('../components/ChatUserSelect.vue')['default']
|
||||
ChatUserSelect: typeof import('./../components/ChatUserSelect.vue')['default']
|
||||
CustomEmpty: typeof import('./../components/CustomEmpty.vue')['default']
|
||||
CustomLoading: typeof import('./../components/CustomLoading.vue')['default']
|
||||
Directory: typeof import('./../components/wang-editor/modules/directory.vue')['default']
|
||||
|
||||
@@ -57,3 +57,11 @@ export interface DeleteCompanyUserParams {
|
||||
companyId: number
|
||||
ids: number[]
|
||||
}
|
||||
|
||||
export interface CompanyChatUser {
|
||||
id: number
|
||||
name: string
|
||||
position: string
|
||||
avatar: string
|
||||
isOnline: boolean
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type { Company } from '@/types/company'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ChatUserSelect from '@/components/ChatUserSelect.vue'
|
||||
import QueryCompany from '@/components/QueryCompany.vue'
|
||||
import { queryCompanyApi } from '@/request/api/company'
|
||||
import { useCommonStore } from '@/store/common'
|
||||
@@ -85,7 +86,7 @@ function toDetail(data: Company) {
|
||||
</div>
|
||||
|
||||
<div class="company-tags">
|
||||
<el-tag v-for="tag in item.industries" :key="tag" type="primary" effect="dark">
|
||||
<el-tag v-for="tag in item.industries" :key="tag" type="primary">
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</div>
|
||||
@@ -97,29 +98,29 @@ function toDetail(data: Company) {
|
||||
<div class="company-footer">
|
||||
<div class="company-details">
|
||||
<div class="detail-item">
|
||||
<i-material-symbols:location-on />
|
||||
<SvgIcon icon="material-symbols:location-on" />
|
||||
<span>{{ item.areas.join('/') || '___' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i-material-symbols:mail />
|
||||
<SvgIcon icon="material-symbols:mail" />
|
||||
<span>{{ item.email || '___' }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<i-material-symbols:call />
|
||||
<SvgIcon icon="material-symbols:call" />
|
||||
<span>{{ item.phone || '___' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="company-actions">
|
||||
<el-button
|
||||
type="primary"
|
||||
text
|
||||
bg
|
||||
>
|
||||
<i-ri:message-3-fill />
|
||||
<span class="button-text">
|
||||
发起聊天
|
||||
</span>
|
||||
</el-button>
|
||||
<ChatUserSelect :company-id="item.id">
|
||||
<el-button
|
||||
type="primary"
|
||||
>
|
||||
<i-ri:message-3-fill />
|
||||
<span class="button-text">
|
||||
发起聊天
|
||||
</span>
|
||||
</el-button>
|
||||
</ChatUserSelect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -341,7 +341,7 @@ async function sendMessage() {
|
||||
/>
|
||||
</div>
|
||||
<div class="im__chat-sendarea">
|
||||
<el-button type="primary" size="small" class="im__chat-send">
|
||||
<el-button type="primary" size="small" class="im__chat-send" @click="sendMessage">
|
||||
发送(回车)
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
@@ -327,7 +327,7 @@ async function sendMessage() {
|
||||
/>
|
||||
</div>
|
||||
<div class="im__chat-sendarea">
|
||||
<el-button type="primary" size="small" class="im__chat-send">
|
||||
<el-button type="primary" size="small" class="im__chat-send" @click="sendMessage">
|
||||
发送(回车)
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ImFriend, ImGroup } from '@/types/im'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { getFriendProfileApi, getGroupProfileApi, queryFriendApi, queryGroupApi, updateGroupUnreadApi, updatePrivateUnreadApi } from '@/request/api/im'
|
||||
import { useUserStore } from '@/store/user'
|
||||
@@ -75,8 +75,10 @@ onMounted(() => {
|
||||
selectedChat.value = { type, id }
|
||||
}
|
||||
|
||||
loadGroups()
|
||||
loadFriends()
|
||||
nextTick(() => {
|
||||
loadGroups()
|
||||
loadFriends()
|
||||
})
|
||||
|
||||
websocket.onMessage(async (msg) => {
|
||||
const { type, from, data } = msg
|
||||
@@ -182,12 +184,12 @@ onMounted(() => {
|
||||
|
||||
const currentGroupId = currentGroupChat.value?.groupId
|
||||
|
||||
// 1️⃣ 查找本地群
|
||||
// 1 查找本地群
|
||||
const group = groupState.value.list.find(
|
||||
item => item.groupId === groupId,
|
||||
)
|
||||
|
||||
// 2️⃣ 本地没有 → 懒加载群 Profile
|
||||
// 2 本地没有 → 懒加载群 Profile
|
||||
if (!group) {
|
||||
try {
|
||||
const res = await getGroupProfileApi(groupId)
|
||||
@@ -216,18 +218,18 @@ onMounted(() => {
|
||||
if (!group) {
|
||||
return
|
||||
}
|
||||
// 3️⃣ 更新最近消息
|
||||
// 3 更新最近消息
|
||||
group.lastMessageTime = sendTime
|
||||
group.lastMessageContent = content
|
||||
group.lastMessageType = contentType
|
||||
group.lastMessageSenderName = senderName
|
||||
|
||||
// 4️⃣ 未读处理(不是自己 & 当前群未打开)
|
||||
// 4 未读处理(不是自己 & 当前群未打开)
|
||||
if (senderId !== userId && groupId !== currentGroupId) {
|
||||
group.unreadCount = (group.unreadCount || 0) + 1
|
||||
}
|
||||
|
||||
// 5️⃣ 当前群窗口处理
|
||||
// 5 当前群窗口处理
|
||||
if (currentGroupChat.value && currentGroupId === groupId) {
|
||||
currentGroupChat.value.lastMessageTime = sendTime
|
||||
currentGroupChat.value.lastMessageContent = content
|
||||
@@ -244,7 +246,7 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
// 6️⃣ 永远置顶
|
||||
// 6 永远置顶
|
||||
const index = groupState.value.list.findIndex(
|
||||
item => item.groupId === groupId,
|
||||
)
|
||||
@@ -285,7 +287,20 @@ async function loadFriends() {
|
||||
state.hasMore = state.list.length < total
|
||||
state.page++
|
||||
|
||||
selectedChat.value.id = null
|
||||
if (isPrivateChat.value && selectedChat.value.id) {
|
||||
const index = state.list.findIndex(
|
||||
item => item.friendId === selectedChat.value.id,
|
||||
)
|
||||
|
||||
if (index !== -1) {
|
||||
const [target] = state.list.splice(index, 1)
|
||||
|
||||
// 移动到第一位
|
||||
state.list.unshift(target)
|
||||
|
||||
currentFriendChat.value = target
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
@@ -323,8 +338,6 @@ async function loadGroups() {
|
||||
state.total = total
|
||||
state.hasMore = state.list.length < total
|
||||
state.page++
|
||||
|
||||
selectedChat.value.id = null
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
@@ -395,9 +408,6 @@ function onGroupChat(group: ImGroup) {
|
||||
userId,
|
||||
})
|
||||
}
|
||||
|
||||
console.log(isGroupChat.value)
|
||||
console.log(currentGroupChat.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -534,7 +544,7 @@ function onGroupChat(group: ImGroup) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="im__window">
|
||||
<div v-if="selectedChat.id && selectedChat.type" class="im__window">
|
||||
<div class="im__window-header">
|
||||
<div class="im__window-peer">
|
||||
<div class="im__window-peer-info">
|
||||
@@ -603,8 +613,8 @@ function onGroupChat(group: ImGroup) {
|
||||
width: 1300px;
|
||||
min-height: 750px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 2px;
|
||||
// box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
// border-radius: 2px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -41,7 +41,7 @@ export default defineConfig((configEnv) => {
|
||||
],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 8080,
|
||||
port: 8089,
|
||||
open: true,
|
||||
// 用于配置开发服务器启动时预加载的一些客户端文件,以提高初次加载的速度。
|
||||
warmup: {
|
||||
|
||||
Reference in new issue
Block a user