This commit is contained in:
oneao committed 2025-12-31 17:21:39 +08:00
1 parent 52b16ed2b0
commit c26d1b3c81
15 files changed
+315 -42

No files matched your search

@@ -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;
}
@@ -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));
+2 -2
View File
@@ -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: {