u
This commit is contained in:
1 parent
00b21b6b44
commit
6fd3bf5138
41 files changed
+1183
-98
No files matched your search
@@ -4,25 +4,20 @@ import { onBeforeUnmount, onMounted } from 'vue'
|
||||
import { initBaseApi } from '@/request/api/base'
|
||||
import { useCommonStore } from '@/store/common'
|
||||
import { getPostTagApi } from './request/api/post'
|
||||
import { useUserStore } from './store/user'
|
||||
import { useWebSocket } from './store/websocket'
|
||||
|
||||
const websocket = useWebSocket()
|
||||
const commonOptions = useCommonStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
onMounted(() => {
|
||||
websocket.connect()
|
||||
|
||||
if (userStore.isLogined) {
|
||||
// websocket.setAuthToken(userStore.token)
|
||||
websocket.setAuthToken("123")
|
||||
}
|
||||
|
||||
handleInitData()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
websocket.logout()
|
||||
|
||||
websocket.close()
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { CreateFriendParams, ImFriend, QueryFriendParams } from '@/types/im'
|
||||
import { httpPostJson } from '@/request/utils'
|
||||
|
||||
export function createFriendApi(params: CreateFriendParams) {
|
||||
return httpPostJson<void>(`/im/friend/create`, params)
|
||||
}
|
||||
|
||||
export function queryFriendApi(params: QueryFriendParams) {
|
||||
return httpPostJson<BasePageResult<ImFriend>>('/im/friend/query', params)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { MessageRequest, MessageResponse } from '@/types/im'
|
||||
import { readonly, ref } from 'vue'
|
||||
import { MessageType } from '@/types/im'
|
||||
import { useUserStore } from './user'
|
||||
|
||||
interface UseWebSocketOptions {
|
||||
url: string
|
||||
@@ -14,6 +15,7 @@ interface UseWebSocketOptions {
|
||||
let singleton: ReturnType<typeof createWebSocket> | null = null
|
||||
|
||||
function createWebSocket(options?: Partial<UseWebSocketOptions>) {
|
||||
const userStore = useUserStore()
|
||||
/* ================== 配置 ================== */
|
||||
const config: Required<UseWebSocketOptions> = {
|
||||
url:
|
||||
@@ -33,7 +35,7 @@ function createWebSocket(options?: Partial<UseWebSocketOptions>) {
|
||||
const connected = ref(false)
|
||||
const authenticated = ref(false)
|
||||
|
||||
let cachedToken: string | null = null
|
||||
let cachedToken: string | null = userStore.token
|
||||
|
||||
let heartbeatTimer: number | null = null
|
||||
let pongTimeoutTimer: number | null = null
|
||||
@@ -75,6 +77,10 @@ function createWebSocket(options?: Partial<UseWebSocketOptions>) {
|
||||
|
||||
ws.onmessage = (evt) => {
|
||||
const msg: MessageResponse = JSON.parse(evt.data)
|
||||
|
||||
console.log('===收到消息===')
|
||||
console.log(msg)
|
||||
|
||||
handleMessage(msg)
|
||||
}
|
||||
|
||||
@@ -178,7 +184,7 @@ function createWebSocket(options?: Partial<UseWebSocketOptions>) {
|
||||
|
||||
const msg: MessageRequest = {
|
||||
type,
|
||||
from: 0,
|
||||
from: userStore.user.id,
|
||||
to: to ?? null,
|
||||
timestamp: Date.now(),
|
||||
data: data ?? null,
|
||||
@@ -196,15 +202,9 @@ function createWebSocket(options?: Partial<UseWebSocketOptions>) {
|
||||
}
|
||||
|
||||
/* ================== 登录相关 ================== */
|
||||
|
||||
function setAuthToken(token: string | null) {
|
||||
cachedToken = token
|
||||
if (token && connected.value) {
|
||||
login(token)
|
||||
}
|
||||
}
|
||||
|
||||
function login(token: string) {
|
||||
console.log('触发login:', authenticated.value)
|
||||
|
||||
if (!connected.value || authenticated.value)
|
||||
return
|
||||
cachedToken = token
|
||||
@@ -214,7 +214,6 @@ function createWebSocket(options?: Partial<UseWebSocketOptions>) {
|
||||
function logout() {
|
||||
cachedToken = null
|
||||
authenticated.value = false
|
||||
sendRaw(MessageType.LOGOUT)
|
||||
}
|
||||
|
||||
/* ================== 消息处理(核心修改点) ================== */
|
||||
@@ -264,7 +263,6 @@ function createWebSocket(options?: Partial<UseWebSocketOptions>) {
|
||||
return {
|
||||
connect,
|
||||
close,
|
||||
setAuthToken,
|
||||
|
||||
login,
|
||||
logout,
|
||||
|
||||
@@ -33,6 +33,16 @@ export interface Company {
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface CompanyDetailUser {
|
||||
id: number
|
||||
companyId: number
|
||||
phone: string
|
||||
email: string
|
||||
role: number
|
||||
position: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface CompanyDetail {
|
||||
id: number
|
||||
name: string
|
||||
@@ -44,15 +54,7 @@ export interface CompanyDetail {
|
||||
email: string
|
||||
description: string
|
||||
establishmentDate: string
|
||||
users: {
|
||||
id: number
|
||||
companyId: number
|
||||
phone: string
|
||||
email: string
|
||||
role: number
|
||||
position: string
|
||||
name: string
|
||||
}[]
|
||||
users: CompanyDetailUser[]
|
||||
}
|
||||
|
||||
export interface UpdateCompanyParams {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// WebSocket 相关
|
||||
export enum MessageType {
|
||||
PING = 0,
|
||||
PONG = 1,
|
||||
@@ -6,10 +7,12 @@ export enum MessageType {
|
||||
LOGOUT = 3,
|
||||
|
||||
ONLINE = 4,
|
||||
OFFLINE = 5,
|
||||
GROUP_ONLINE = 5,
|
||||
OFFLINE = 6,
|
||||
GROUP_OFFLINE = 7,
|
||||
|
||||
PRIVATE = 6,
|
||||
GROUP = 7,
|
||||
PRIVATE = 8,
|
||||
GROUP = 9,
|
||||
}
|
||||
|
||||
export interface MessageRequest {
|
||||
@@ -29,3 +32,33 @@ export interface MessageResponse {
|
||||
code: number
|
||||
message: string
|
||||
}
|
||||
|
||||
// http 相关
|
||||
export interface CreateFriendParams {
|
||||
friendId: number
|
||||
}
|
||||
|
||||
export interface QueryFriendParams {
|
||||
currentPage: number
|
||||
pageSize: number
|
||||
|
||||
name: string
|
||||
id: number | null
|
||||
}
|
||||
|
||||
export interface ImFriend {
|
||||
id: number
|
||||
|
||||
friendId: number
|
||||
friendName: string
|
||||
friendAvatar: string
|
||||
|
||||
unreadCount: number
|
||||
|
||||
lastMessageId: number
|
||||
lastMessageTime: number
|
||||
lastMessageType: number
|
||||
lastMessageContent: string
|
||||
|
||||
isOnline: boolean
|
||||
}
|
||||
@@ -113,3 +113,54 @@ export function formatDate(
|
||||
|
||||
return date.format(format)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化聊天时间
|
||||
* @param timestamp - 毫秒级时间戳
|
||||
* @param mode - 显示模式:'list' 为列表,'chat' 为聊天记录
|
||||
* @returns 格式化后的时间字符串
|
||||
*/
|
||||
export function formatImTime(timestamp: number | null | undefined, mode: 'list' | 'chat' = 'list'): string {
|
||||
if (!timestamp)
|
||||
return ''
|
||||
|
||||
const now = new Date()
|
||||
const date = new Date(timestamp)
|
||||
|
||||
const hour = String(date.getHours()).padStart(2, '0')
|
||||
const minute = String(date.getMinutes()).padStart(2, '0')
|
||||
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
|
||||
// 去除时分秒的日期
|
||||
const nowMidnight = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const dateMidnight = new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
||||
const diffDays = Math.floor((nowMidnight.getTime() - dateMidnight.getTime()) / (1000 * 3600 * 24))
|
||||
|
||||
// 1️⃣ 当天
|
||||
if (diffDays === 0)
|
||||
return `${hour}:${minute}`
|
||||
|
||||
// 2️⃣ 昨天
|
||||
if (diffDays === 1)
|
||||
return `昨天 ${hour}:${minute}`
|
||||
|
||||
// 3️⃣ 一周内(周一到周日)
|
||||
const weekStart = new Date(nowMidnight.getTime() - ((now.getDay() || 7) - 1) * 24 * 3600 * 1000)
|
||||
if (dateMidnight >= weekStart) {
|
||||
return mode === 'chat'
|
||||
? `${weekDays[date.getDay()]} ${hour}:${minute}`
|
||||
: weekDays[date.getDay()]
|
||||
}
|
||||
|
||||
// 4️⃣ 本年或跨年(聊天记录模式:统一显示 YYYY/MM/DD HH:mm)
|
||||
const ymd = `${date.getFullYear()}/${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`
|
||||
|
||||
if (mode === 'chat')
|
||||
return `${ymd} ${hour}:${minute}`
|
||||
|
||||
// 列表模式区分本年/跨年
|
||||
if (date.getFullYear() === now.getFullYear())
|
||||
return `${String(date.getMonth() + 1).padStart(2, '0')}/${String(date.getDate()).padStart(2, '0')}`
|
||||
|
||||
return ymd
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" setup>
|
||||
import type { AnchorMenuItem } from '@/components/anchor-menu/type'
|
||||
import type { CompanyDetail } from '@/types/company'
|
||||
import type { CompanyDetail, CompanyDetailUser } from '@/types/company'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@@ -8,8 +8,12 @@ import AnchorMenu from '@/components/anchor-menu/index.vue'
|
||||
|
||||
import WangEditor from '@/components/wang-editor/index.vue'
|
||||
import { getCompanyDetailApi } from '@/request/api/company'
|
||||
import { createFriendApi } from '@/request/api/im'
|
||||
import { useUserStore } from '@/store/user'
|
||||
import { formatDate } from '@/utils/time'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -78,6 +82,28 @@ onMounted(() => {
|
||||
function handleUpdateEditorDirectory(menus: AnchorMenuItem[]) {
|
||||
anchorMenu.value[3].children.push(...menus)
|
||||
}
|
||||
|
||||
function handleToIm(user: CompanyDetailUser) {
|
||||
if (userStore.user.id === user.id) {
|
||||
ElMessage.warning('不能与自己发起聊天')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
createFriendApi({
|
||||
friendId: user.id,
|
||||
}).then((res) => {
|
||||
if (res.isSuccess) {
|
||||
router.push('/im')
|
||||
}
|
||||
else {
|
||||
ElMessage.error(res.message || '发起聊天失败,请稍后再试')
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
ElMessage.error('发起聊天失败,请稍后再试')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -145,8 +171,8 @@ function handleUpdateEditorDirectory(menus: AnchorMenuItem[]) {
|
||||
<el-table-column prop="phone" label="手机号" width="180" />
|
||||
<el-table-column prop="email" label="邮箱号" width="180" />
|
||||
<el-table-column fixed="right" label="操作" min-width="120">
|
||||
<template #default>
|
||||
<el-button type="primary" size="small" text bg>
|
||||
<template #default="scope">
|
||||
<el-button v-if="scope.row.id !== userStore.user.id" type="primary" size="small" text bg @click="handleToIm(scope.row)">
|
||||
发起聊天
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,108 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ImFriend } from '@/types/im'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { queryFriendApi } from '@/request/api/im'
|
||||
import { useWebSocket } from '@/store/websocket'
|
||||
import { MessageType } from '@/types/im'
|
||||
import { formatImTime } from '@/utils/time'
|
||||
|
||||
const route = useRoute()
|
||||
const websocket = useWebSocket()
|
||||
|
||||
// 当前聊天对象
|
||||
const activeChatTab = ref<'private' | 'group'>('private')
|
||||
|
||||
const initialFriendId = ref<number | null>(
|
||||
route.params.friendId ? Number(route.params.friendId) : null,
|
||||
)
|
||||
|
||||
// 好友列表
|
||||
const friends = ref<ImFriend[]>([])
|
||||
const friendsPage = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
loading: false,
|
||||
})
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadFriends()
|
||||
|
||||
websocket.onMessage((msg) => {
|
||||
const { type, from } = msg
|
||||
|
||||
const updateOnlineStatus = (isOnline: boolean) => {
|
||||
const friend = friends.value.find(item => item.friendId === from)
|
||||
if (friend)
|
||||
friend.isOnline = isOnline
|
||||
}
|
||||
|
||||
if (type === MessageType.ONLINE) {
|
||||
updateOnlineStatus(true)
|
||||
}
|
||||
else if (type === MessageType.OFFLINE) {
|
||||
updateOnlineStatus(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 加载好友列表
|
||||
async function loadFriends() {
|
||||
if (friendsPage.value.loading)
|
||||
return
|
||||
friendsPage.value.loading = true
|
||||
|
||||
try {
|
||||
const res = await queryFriendApi({
|
||||
currentPage: friendsPage.value.page,
|
||||
pageSize: friendsPage.value.pageSize,
|
||||
name: '',
|
||||
id: initialFriendId.value,
|
||||
})
|
||||
|
||||
if (res.isSuccess) {
|
||||
const { records, total } = res.data
|
||||
|
||||
if (friendsPage.value.page === 1) {
|
||||
friends.value = records
|
||||
}
|
||||
else {
|
||||
// 合并并去重
|
||||
const merged = friends.value.concat(records)
|
||||
const map = new Map<number, typeof merged[0]>()
|
||||
for (const f of merged) {
|
||||
map.set(f.id, f)
|
||||
}
|
||||
friends.value = Array.from(map.values())
|
||||
}
|
||||
|
||||
// 设置当前聊天对象
|
||||
if (initialFriendId.value) {
|
||||
const friend = friends.value.find(f => f.id === initialFriendId.value)
|
||||
if (friend) {
|
||||
// openPrivateChat(friend)
|
||||
}
|
||||
initialFriendId.value = null
|
||||
}
|
||||
|
||||
friendsPage.value.total = total
|
||||
}
|
||||
else {
|
||||
ElMessage.error(res.message || '加载好友列表失败,请稍后再试')
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.log(err)
|
||||
ElMessage.error('加载好友列表失败,请稍后再试')
|
||||
}
|
||||
finally {
|
||||
friendsPage.value.loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -40,15 +139,88 @@ const websocket = useWebSocket()
|
||||
<div class="im__list-search">
|
||||
<el-input placeholder="请根据名称搜索" :prefix-icon="Search" />
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeChatTab" class="im__list-tabs">
|
||||
<el-tab-pane label="单聊" name="private" />
|
||||
<el-tab-pane label="群聊" name="group" />
|
||||
</el-tabs>
|
||||
<div class="im__list-item">
|
||||
<div class="im__list-body">
|
||||
<div
|
||||
v-for="item in friends" :key="item.id" class="im__chat-item"
|
||||
>
|
||||
<el-badge
|
||||
is-dot
|
||||
:color="item.isOnline ? '#24e68a' : '#bfbfbf'"
|
||||
:badge-style="{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
}"
|
||||
:offset="[-4, 34]"
|
||||
>
|
||||
<SafeImage class="im__chat-item-avatar" :url="item.friendAvatar" :size="40" round />
|
||||
</el-badge>
|
||||
<div class="im__chat-item-info">
|
||||
<div class="im__chat-item-main">
|
||||
<div class="im__chat-item-name">
|
||||
{{ item.friendName }}
|
||||
</div>
|
||||
<div class="im__chat-item-last-time">
|
||||
{{ formatImTime(item.lastMessageTime, 'list') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="im__chat-item-message">
|
||||
<div class="im__chat-item-last-message">
|
||||
{{ item.lastMessageContent }}
|
||||
</div>
|
||||
<el-badge :value="item.unreadCount || 0" :show-zero="false" :max="99" class="im__chat-item-unread-count" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="im__list-item">
|
||||
<div class="im__list-header">
|
||||
<SvgIcon icon="tabler:message-circle" :size="16" />
|
||||
<span class="im__list-header-title">
|
||||
单聊
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="im__list-body">
|
||||
<div
|
||||
v-for="item in friends" :key="item.id" class="im__chat-item"
|
||||
>
|
||||
<el-badge
|
||||
is-dot
|
||||
:color="item.isOnline ? '#24e68a' : '#bfbfbf'"
|
||||
:badge-style="{
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
}"
|
||||
:offset="[-4, 34]"
|
||||
>
|
||||
<SafeImage class="im__chat-item-avatar" :url="item.friendAvatar" :size="40" round />
|
||||
</el-badge>
|
||||
<div class="im__chat-item-info">
|
||||
<div class="im__chat-item-main">
|
||||
<div class="im__chat-item-name">
|
||||
{{ item.friendName }}
|
||||
</div>
|
||||
<div class="im__chat-item-last-time">
|
||||
{{ formatImTime(item.lastMessageTime, 'list') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="im__chat-item-message">
|
||||
<div class="im__chat-item-last-message">
|
||||
{{ item.lastMessageContent }}
|
||||
</div>
|
||||
<el-badge :value="item.unreadCount || 0" :show-zero="false" :max="99" class="im__chat-item-unread-count" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,6 +234,7 @@ const websocket = useWebSocket()
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background-color: #f5f6fa;
|
||||
margin-top: 60px;
|
||||
|
||||
& *::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
@@ -186,6 +359,18 @@ const websocket = useWebSocket()
|
||||
border-bottom: 1px solid #e6e9ef;
|
||||
}
|
||||
|
||||
.im__list-tabs {
|
||||
:deep(.el-tabs__header) {
|
||||
margin: 0;
|
||||
.el-tabs__nav {
|
||||
width: 100%;
|
||||
.el-tabs__item {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.im__list-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -244,7 +429,7 @@ const websocket = useWebSocket()
|
||||
}
|
||||
|
||||
.im__chat-item-name {
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.im__chat-item-last-time,
|
||||
|
||||
Reference in new issue
Block a user