IM聊天功能优化

This commit is contained in:
geht
2026-05-28 17:08:34 +08:00
parent 3539eab924
commit a63cd6ad1a
29 changed files with 3565 additions and 203 deletions

View File

@@ -1,4 +1,5 @@
import { reactive, ref, Ref, unref } from 'vue';
import { reactive, ref, Ref, unref, onUnmounted, watch, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import { merge } from 'lodash-es';
import { DynamicProps } from '/#/utils';
import { BasicTableProps, TableActionType, useTable } from '/@/components/Table';
@@ -9,6 +10,16 @@ import { useMethods } from '/@/hooks/system/useMethods';
import { useDesign } from '/@/hooks/web/useDesign';
import { filterObj } from '/@/utils/common/compUtils';
import { isFunction } from '@/utils/is';
import { registerImPageListProvider } from '/@/views/system/im/imPageListRegistry';
import { buildImPageListSnapshot } from '/@/views/system/im/imPageListUtil';
import { IM_RECORD_QUERY_KEY } from '/@/views/system/im/imBizRecordMessage';
import {
IM_RECORD_LOCATE_CLEAR_EVENT,
IM_RECORD_LOCATE_EVENT,
removeImRecordQueryFromRoute,
resolveImLocateRecordId,
scrollToImRecordRowWithRetry,
} from '/@/views/system/im/imRecordLocate';
const { handleExportXls, handleImportXls } = useMethods();
// 定义 useListPage 方法所需参数
@@ -59,7 +70,168 @@ export function useListPage(options: ListPageOptions) {
const tableContext = useListTable(options.tableProps);
const [, { getForm, reload, setLoading, getColumns }, { selectedRowKeys }] = tableContext;
const route = useRoute();
const [, tableMethods, { selectedRowKeys }] = tableContext;
const { getForm, reload, setLoading, getColumns } = tableMethods;
const imHighlightRecordId = ref('');
let clearHighlightTimer: ReturnType<typeof setTimeout> | null = null;
let locatingRecordId = '';
onUnmounted(() => {
if (clearHighlightTimer) {
clearTimeout(clearHighlightTimer);
clearHighlightTimer = null;
}
});
//update-begin---author:xsl ---date:20260528 for【IM聊天-OA】列表页注册 IM 明细快照提供器-----------
onUnmounted(
registerImPageListProvider(() => {
const sourceColumns = tableMethods.getColumns?.() || options.tableProps?.columns || [];
return buildImPageListSnapshot({
title: (options.tableProps?.title as string) || '',
pagePath: route.fullPath,
rowKey: (options.tableProps?.rowKey as string) || 'id',
sourceColumns,
records: tableMethods.getDataSource?.() || [],
});
}),
);
//update-end---author:xsl ---date:20260528 for【IM聊天-OA】列表页注册 IM 明细快照提供器-----------
//update-begin---author:xsl ---date:20260528 for【IM聊天-OA】IM 消息链接跳转后定位列表行-----------
function isTableReady() {
try {
tableMethods.getDataSource?.();
return true;
} catch {
return false;
}
}
function applyImRecordRowClassName() {
if (!isTableReady()) {
return;
}
const rowKey = (options.tableProps?.rowKey as string) || 'id';
tableMethods.setProps?.({
rowClassName: (record: Recordable) => {
if (imHighlightRecordId.value && String(record[rowKey]) === imHighlightRecordId.value) {
return 'im-record-locate-row';
}
return '';
},
});
}
/** 等待列表首屏数据加载(兼容 immediate:false + 左侧树页面) */
async function waitForLocateContext(maxWaitMs = 3500) {
const start = Date.now();
while (Date.now() - start < maxWaitMs) {
if (!isTableReady()) {
await new Promise((resolve) => setTimeout(resolve, 50));
continue;
}
const data = tableMethods.getDataSource?.() || [];
if (data.length > 0) {
return true;
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
return isTableReady();
}
function findRecordInTable(recordId: string) {
const rowKey = (options.tableProps?.rowKey as string) || 'id';
const data = tableMethods.getDataSource?.() || [];
return data.some((item) => String(item[rowKey]) === recordId);
}
function clearImRecordHighlight() {
imHighlightRecordId.value = '';
applyImRecordRowClassName();
}
function scheduleClearImRecordHighlight(delayMs = 3500) {
if (clearHighlightTimer) {
clearTimeout(clearHighlightTimer);
}
clearHighlightTimer = setTimeout(() => {
clearImRecordHighlight();
clearHighlightTimer = null;
}, delayMs);
}
async function applyImRecordHighlight(recordId: string) {
imHighlightRecordId.value = recordId;
applyImRecordRowClassName();
await nextTick();
await new Promise((resolve) => requestAnimationFrame(() => resolve(undefined)));
applyImRecordRowClassName();
await scrollToImRecordRowWithRetry(recordId);
}
async function locateImRecordRow(recordId: string) {
if (locatingRecordId === recordId) {
return;
}
locatingRecordId = recordId;
try {
if (!(await waitForLocateContext())) {
return;
}
if (!findRecordInTable(recordId)) {
$message.createMessage.warning('当前列表中未找到对应数据');
removeImRecordQueryFromRoute();
return;
}
await applyImRecordHighlight(recordId);
scheduleClearImRecordHighlight();
// 直链 URL 场景:仅改地址栏,避免 router.replace 导致 fullPath 变化 remount
removeImRecordQueryFromRoute();
} finally {
locatingRecordId = '';
}
}
watch(
() => [route.path, route.query[IM_RECORD_QUERY_KEY]] as const,
([path, queryRecordId]) => {
const recordId = resolveImLocateRecordId(path, queryRecordId);
if (!recordId) {
return;
}
nextTick(() => locateImRecordRow(recordId));
},
{ immediate: true },
);
function handleImRecordLocateEvent(e: Event) {
const detail = (e as CustomEvent<{ path: string; recordId: string }>).detail;
if (!detail?.path || detail.path !== route.path || !detail.recordId) {
return;
}
nextTick(() => locateImRecordRow(detail.recordId));
}
function handleImRecordLocateClearEvent() {
if (clearHighlightTimer) {
clearTimeout(clearHighlightTimer);
clearHighlightTimer = null;
}
locatingRecordId = '';
clearImRecordHighlight();
}
onUnmounted(() => {
window.removeEventListener(IM_RECORD_LOCATE_EVENT, handleImRecordLocateEvent);
window.removeEventListener(IM_RECORD_LOCATE_CLEAR_EVENT, handleImRecordLocateClearEvent);
});
window.addEventListener(IM_RECORD_LOCATE_EVENT, handleImRecordLocateEvent);
window.addEventListener(IM_RECORD_LOCATE_CLEAR_EVENT, handleImRecordLocateClearEvent);
//update-end---author:xsl ---date:20260528 for【IM聊天-OA】IM 消息链接跳转后定位列表行-----------
// 导出 excel
async function onExportXls() {

View File

@@ -8,6 +8,7 @@ import { useRouter } from 'vue-router';
import { REDIRECT_NAME } from '/@/router/constant';
import { useUserStore } from '/@/store/modules/user';
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
import { clearImRecordLocateState, stripImRecordQuery } from '/@/views/system/im/imRecordLocate';
export type RouteLocationRawEx = Omit<RouteLocationRaw, 'path'> & { path: PageEnum };
@@ -43,10 +44,17 @@ export function useGo(_router?: Router) {
* @description: redo current page
*/
export const useRedo = (_router?: Router, otherQuery?: Recordable) => {
const { push, currentRoute } = _router || useRouter();
const { query, params = {}, name, fullPath } = unref(currentRoute.value);
const router = _router || useRouter();
const { push, currentRoute, resolve: resolveRoute } = router;
function redo(): Promise<boolean> {
return new Promise((resolve) => {
//update-begin---author:xsl ---date:20260528 for【IM聊天-OA】标签页刷新时取消 IM 定位-----------
clearImRecordLocateState();
const rawRoute = unref(currentRoute.value);
let { query, params = {}, name, fullPath } = rawRoute;
query = stripImRecordQuery(query as Recordable);
fullPath = resolveRoute({ path: rawRoute.path, query, hash: rawRoute.hash }).fullPath;
//update-end---author:xsl ---date:20260528 for【IM聊天-OA】标签页刷新时取消 IM 定位-----------
if (name === REDIRECT_NAME) {
resolve(false);
return;