diff --git a/.env.development b/.env.development index 589516e..561e73f 100644 --- a/.env.development +++ b/.env.development @@ -3,4 +3,4 @@ NODE_ENV = 'development' VITE_APP_BASE_API = 'https://mini-chat.1024tool.vip/api' VITE_SERVE = "https://mini-chat.1024tool.vip/api" - +VITE_APP_BASE_API_img = "https://mini-chat.1024tool.vip/" diff --git a/.env.production b/.env.production index 092c243..d914d14 100644 --- a/.env.production +++ b/.env.production @@ -2,4 +2,4 @@ NODE_ENV = 'production' VITE_APP_BASE_API = 'https://dsjhd9s.tbmw.cn/api/' VITE_SERVE = "https://dsjhd9s.tbmw.cn/api/" - +VITE_APP_BASE_API_img = "https://dsjhd9s.tbmw.cn/" \ No newline at end of file diff --git a/components.d.ts b/components.d.ts index 369448a..c66751b 100644 --- a/components.d.ts +++ b/components.d.ts @@ -12,13 +12,10 @@ declare module 'vue' { ElButton: typeof import('element-plus/es')['ElButton'] ElCard: typeof import('element-plus/es')['ElCard'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] - ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] ElCol: typeof import('element-plus/es')['ElCol'] ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] ElContainer: typeof import('element-plus/es')['ElContainer'] - ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] ElDialog: typeof import('element-plus/es')['ElDialog'] - ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElForm: typeof import('element-plus/es')['ElForm'] ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElHeader: typeof import('element-plus/es')['ElHeader'] @@ -26,30 +23,21 @@ declare module 'vue' { ElImage: typeof import('element-plus/es')['ElImage'] ElInput: typeof import('element-plus/es')['ElInput'] ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] - ElLink: typeof import('element-plus/es')['ElLink'] ElMain: typeof import('element-plus/es')['ElMain'] ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElOption: typeof import('element-plus/es')['ElOption'] ElPagination: typeof import('element-plus/es')['ElPagination'] ElPopover: typeof import('element-plus/es')['ElPopover'] - ElProgress: typeof import('element-plus/es')['ElProgress'] - ElRadio: typeof import('element-plus/es')['ElRadio'] - ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRow: typeof import('element-plus/es')['ElRow'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelectV2: typeof import('element-plus/es')['ElSelectV2'] - ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTabPane: typeof import('element-plus/es')['ElTabPane'] ElTabs: typeof import('element-plus/es')['ElTabs'] - ElTag: typeof import('element-plus/es')['ElTag'] - ElTimePicker: typeof import('element-plus/es')['ElTimePicker'] ElTooltip: typeof import('element-plus/es')['ElTooltip'] - ElTour: typeof import('element-plus/es')['ElTour'] - ElTourStep: typeof import('element-plus/es')['ElTourStep'] IndexUser: typeof import('./src/components/TagClass/indexUser.vue')['default'] KeyWords: typeof import('./src/components/KeyWords/index.vue')['default'] MaterialPublic: typeof import('./src/components/MaterialPublic/index.vue')['default'] diff --git a/src/components/UserCard/index.vue b/src/components/UserCard/index.vue index 5783098..dc81040 100644 --- a/src/components/UserCard/index.vue +++ b/src/components/UserCard/index.vue @@ -8,7 +8,10 @@ {{ item.unread_count > 99 ? '99+' : item.unread_count }} - {{ item.sender_name }} +
+ {{ item.sender_name }} + {{ item.send_time }} +
加载中...
@@ -59,6 +59,7 @@ const messages = ref([]) const draft = ref('') const imageInput = ref(null) const chatBody = ref(null) +const isLoadingMessages = ref(false) // 发送者信息 const sendeInfo = reactive({ @@ -95,11 +96,20 @@ const onSelectUser = async (id) => { msgQuery.user_id = u.sender_id noMoreMessages.value = false + // 为避免切换用户时看到明显的滚动过程: + // - 在开始加载消息前将 chat 区域设置为不可见(但依然可滚动), + // - 加载完成后恢复显示并滚到底部(保持原有的交互顺序)。 + isLoadingMessages.value = true + // 保持原有行为:先滚动到底部再加载(不 await),以免改变用户可感知的交互顺序 - scrollToBottom() - // await scrollToBottom() - getMessages() + // 启动加载(不 await,使界面更流畅) + getMessages().finally(() => { + // 在消息加载结束后恢复可见性并尝试滚到底部 + isLoadingMessages.value = false + // 确保滚到底部以避免残留偏移 + scrollToBottom() + }) // 重启定时器 restartMessageTimer() @@ -117,6 +127,10 @@ const getMessages = async (isHistory = true, pageOverride = null) => { if (!msgQuery.user_id) return loadingMore.value = true + // 在第一次加载(page=1 且 isHistory=true)期间也将 isLoadingMessages 保持为 true + if ((pageOverride ?? msgQuery.page) === 1 && isHistory) { + isLoadingMessages.value = true + } const queryPage = pageOverride ?? msgQuery.page const query = { ...msgQuery, page: queryPage } @@ -175,6 +189,10 @@ const getMessages = async (isHistory = true, pageOverride = null) => { // 可扩展错误处理 } finally { loadingMore.value = false + // 如果这是第一页历史加载,确保 loading 标志被清理(页面外也会在调用者 finally 中清理) + if ((pageOverride ?? msgQuery.page) === 1 && isHistory) { + isLoadingMessages.value = false + } // 如果是向上翻页(非第一页)并且 prepend 后视图仍然靠近顶部,自动继续翻页 try { @@ -467,6 +485,35 @@ watch(activeUser, (newVal) => { stopMessageTimer() } }) + +// 当 messages 变化时,尝试可靠地滚动到底部: +// - 等待 DOM 更新(nextTick), +// - 立即滚动并在短延迟后再滚一次以应对图片等资源加载导致的高度变化, +// - 对未加载的图片注册 load 事件,加载完成后再滚动。 +watch(messages, async (newVal, oldVal) => { + // 只有在自动滚动模式下才触发 + if (!isAutoScroll.value) return + + await nextTick() + // 立即尝试滚动到底部 + scrollToBottom() + + // 在短延迟后再尝试一次,以容忍布局抖动或图片加载 + setTimeout(() => scrollToBottom(), 120) + + // 如果存在未完成加载的图片,注册 load 事件,加载后滚到底部 + const el = chatBody.value + if (el) { + const imgs = el.querySelectorAll('img') + imgs.forEach(img => { + if (!img.complete) { + img.addEventListener('load', () => { + if (isAutoScroll.value) scrollToBottom() + }, { once: true }) + } + }) + } +})