新增IM聊天

This commit is contained in:
geht
2026-05-28 14:37:05 +08:00
parent 99e574f600
commit 3539eab924
35 changed files with 2864 additions and 36 deletions

View File

@@ -0,0 +1,42 @@
<template>
<Tooltip :title="tooltipTitle" placement="bottom" :mouseEnterDelay="0.5">
<span :class="`${prefixCls}-action__item refresh-cache-item`" class="refresh-cache-btn" @click="clearCache">
<SyncOutlined :spin="loading" />
</span>
</Tooltip>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { Tooltip } from 'ant-design-vue';
import { SyncOutlined } from '@ant-design/icons-vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { useRefreshCache } from './useRefreshCache';
export default defineComponent({
name: 'RefreshCache',
components: { Tooltip, SyncOutlined },
setup() {
const { prefixCls } = useDesign('layout-header');
const { loading, clearCache, tooltipTitle } = useRefreshCache();
return {
prefixCls,
loading,
clearCache,
tooltipTitle,
};
},
});
</script>
<style lang="less" scoped>
.refresh-cache-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 12px;
font-size: 16px;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,94 @@
<template>
<div :class="prefixCls">
<Badge :count="totalUnread" :overflowCount="99" :offset="[-4, 18]" :numberStyle="numberStyle" @click="openChat">
<MessageOutlined />
</Badge>
<ImChatModal @register="registerModal" />
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted } from 'vue';
import { Badge } from 'ant-design-vue';
import { MessageOutlined } from '@ant-design/icons-vue';
import { useModal } from '/@/components/Modal';
import { useDesign } from '/@/hooks/web/useDesign';
import { ensureWebSocketConnected, onWebSocket, offWebSocket } from '/@/hooks/web/useWebSocket';
import ImChatModal from '/@/views/system/im/ImChatModal.vue';
import { prefetchImChatData, handleImChatSocket } from '/@/views/system/im/imCache';
import { useImUnread } from '/@/views/system/im/useImUnread';
export default defineComponent({
name: 'HeaderImChat',
components: {
Badge,
MessageOutlined,
ImChatModal,
},
setup() {
const { prefixCls } = useDesign('header-im-chat');
const { totalUnread } = useImUnread();
const [registerModal, { openModal }] = useModal();
const numberStyle = {
fontSize: '12px',
height: '16px',
minWidth: '16px',
lineHeight: '16px',
padding: '0 4px',
};
function openChat() {
openModal(true, {});
}
function onImSocket(data: Record<string, any>) {
handleImChatSocket(data);
}
onMounted(() => {
ensureWebSocketConnected();
prefetchImChatData();
onWebSocket(onImSocket);
});
onUnmounted(() => {
offWebSocket(onImSocket);
});
return {
prefixCls,
totalUnread,
numberStyle,
openChat,
registerModal,
};
},
});
</script>
<style lang="less">
@prefix-cls: ~'@{namespace}-header-im-chat';
.@{prefix-cls} {
cursor: pointer;
padding: 0 10px;
.ant-badge {
font-size: 18px;
.ant-badge-count {
@badget-size: 16px;
width: @badget-size;
height: @badget-size;
min-width: @badget-size;
line-height: @badget-size;
padding: 0;
}
svg {
width: 0.9em;
}
}
}
</style>

View File

@@ -14,3 +14,7 @@ export const ErrorAction = createAsyncComponent(() => import('./ErrorAction.vue'
export const LockScreen = createAsyncComponent(() => import('./LockScreen.vue'));
export { FullScreen };
export { default as RefreshCache } from './RefreshCache.vue';
export const ImChat = createAsyncComponent(() => import('./im-chat/index.vue'));

View File

@@ -25,10 +25,8 @@
import { useDesign } from '/@/hooks/web/useDesign';
import { useGlobSetting } from '/@/hooks/setting';
import { useUserStore } from '/@/store/modules/user';
import { connectWebSocket, onWebSocket } from '/@/hooks/web/useWebSocket';
import { connectWebSocket, onWebSocket, buildSystemWebSocketUrl } from '/@/hooks/web/useWebSocket';
import { readAllMsg } from '/@/views/monitor/mynews/mynews.api';
import { getToken } from '/@/utils/auth';
import md5 from 'crypto-js/md5';
import { useRouter } from 'vue-router';
import SysMessageModal from '/@/views/system/message/components/SysMessageModal.vue'
@@ -148,12 +146,10 @@
// 初始化 WebSocket
function initWebSocket() {
let token = getToken();
//将登录token生成一个短的标识
let wsClientId = md5(token);
let userId = unref(userStore.getUserInfo).id + "_" + wsClientId;
// WebSocket与普通的请求所用协议有所不同ws等同于httpwss等同于https
let url = glob.domainUrl?.replace('https://', 'wss://').replace('http://', 'ws://') + '/websocket/' + userId;
const url = buildSystemWebSocketUrl();
if (!url) {
return;
}
connectWebSocket(url);
onWebSocket(onWebSocketMessage);
}

View File

@@ -0,0 +1,46 @@
import { ref } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useMessage } from '/@/hooks/web/useMessage';
import { useUserStore } from '/@/store/modules/user';
import { refreshCache, queryAllDictItems } from '/@/views/system/dict/dict.api';
import { refreshDragCache } from '/@/api/common/api';
import { DB_DICT_DATA_KEY } from '/@/enums/cacheEnum';
import { removeAuthCache, setAuthCache } from '/@/utils/auth';
/** 顶部/用户菜单共用的刷新缓存逻辑 */
export function useRefreshCache() {
const { t } = useI18n();
const { createMessage } = useMessage();
const userStore = useUserStore();
const loading = ref(false);
async function clearCache() {
if (loading.value) {
return;
}
loading.value = true;
try {
const result = await refreshCache();
await refreshDragCache();
if (result.success) {
const res = await queryAllDictItems();
removeAuthCache(DB_DICT_DATA_KEY);
setAuthCache(DB_DICT_DATA_KEY, res.result);
createMessage.success(t('layout.header.refreshCacheComplete'));
userStore.setAllDictItems(res.result);
} else {
createMessage.error(t('layout.header.refreshCacheFailure'));
}
} catch {
createMessage.error(t('layout.header.refreshCacheFailure'));
} finally {
loading.value = false;
}
}
return {
loading,
clearCache,
tooltipTitle: t('layout.header.dropdownItemRefreshCache'),
};
}

View File

@@ -44,7 +44,6 @@
import { useI18n } from '/@/hooks/web/useI18n';
import { useDesign } from '/@/hooks/web/useDesign';
import { useModal } from '/@/components/Modal';
import { useMessage } from '/src/hooks/web/useMessage';
import { useGo } from '/@/hooks/web/usePage';
import headerImg from '/@/assets/images/header.jpg';
import { propTypes } from '/@/utils/propTypes';
@@ -52,15 +51,11 @@
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { refreshCache, queryAllDictItems } from '/@/views/system/dict/dict.api';
import { DB_DICT_DATA_KEY } from '/src/enums/cacheEnum';
import { removeAuthCache, setAuthCache } from '/src/utils/auth';
import { useRefreshCache } from '../useRefreshCache';
import { getFileAccessHttpUrl } from '/@/utils/common/compUtils';
import { getRefPromise } from '/@/utils/index';
import { refreshDragCache } from "@/api/common/api";
type MenuEvent = 'logout' | 'doc' | 'lock' | 'cache' | 'depart' | 'defaultHomePage' | 'password' | 'account';
const { createMessage } = useMessage();
export default defineComponent({
name: 'UserDropdown',
components: {
@@ -84,6 +79,7 @@
const passwordVisible = ref(false);
const lockActionVisible = ref(false);
const lockActionRef = ref(null);
const { clearCache } = useRefreshCache();
const getUserInfo = computed(() => {
const { realname = '', avatar, desc } = userStore.getUserInfo || {};
@@ -119,22 +115,6 @@
openWindow(SITE_URL);
}
// 清除缓存
async function clearCache() {
const result = await refreshCache();
const dragRes = await refreshDragCache();
console.log('dragRes', dragRes);
if (result.success) {
const res = await queryAllDictItems();
removeAuthCache(DB_DICT_DATA_KEY);
setAuthCache(DB_DICT_DATA_KEY, res.result);
createMessage.success(t('layout.header.refreshCacheComplete'));
// 代码逻辑说明: 【issues/7433】vue3 数据字典优化建议
userStore.setAllDictItems(res.result);
} else {
createMessage.error(t('layout.header.refreshCacheFailure'));
}
}
// 切换部门
function updateCurrentDepart() {
loginSelectRef.value.show();

View File

@@ -29,10 +29,14 @@
<Notify v-if="getShowNotice" :class="`${prefixCls}-action__item notify-item`" />
<ImChat :class="`${prefixCls}-action__item im-chat-item`" />
<FullScreen v-if="getShowFullScreen" :class="`${prefixCls}-action__item fullscreen-item`" />
<LockScreen v-if="getUseLockPage" />
<RefreshCache />
<AppLocalePicker v-if="getShowLocalePicker" :reload="true" :showText="false" :class="`${prefixCls}-action__item`" />
<UserDropDown :theme="getHeaderTheme" />
@@ -64,7 +68,7 @@
import { SettingButtonPositionEnum } from '/@/enums/appEnum';
import { AppLocalePicker } from '/@/components/Application';
import { UserDropDown, LayoutBreadcrumb, FullScreen, Notify, ErrorAction, LockScreen } from './components';
import { UserDropDown, LayoutBreadcrumb, FullScreen, Notify, ImChat, ErrorAction, LockScreen, RefreshCache } from './components';
import { useAppInject } from '/@/hooks/web/useAppInject';
import { useDesign } from '/@/hooks/web/useDesign';
@@ -86,9 +90,11 @@
LayoutBreadcrumb,
LayoutMenu,
UserDropDown,
RefreshCache,
AppLocalePicker,
FullScreen,
Notify,
ImChat,
AppSearch,
ErrorAction,
LockScreen,