This commit is contained in:
oneao committed 2025-12-29 17:31:10 +08:00
1 parent 00b21b6b44
commit 6fd3bf5138
41 files changed
+1183 -98

No files matched your search

+2 -7
View File
@@ -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,