From 497d0a840fabd9837d4b2dc0ffdd2cb68fd12451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Wed, 8 Jul 2026 08:18:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=80=E4=BA=9B=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + components.d.ts | 26 +- src/App.vue | 4 +- src/api/modules/contact.ts | 14 +- src/api/modules/message.ts | 9 +- src/api/modules/settings.ts | 12 + src/api/request/index.ts | 4 +- src/api/websocket/index.ts | 4 +- src/components/call/CallWindow.vue | 4 +- src/components/call/GlobalCallProvider.vue | 23 +- src/components/call/GroupCallModal.vue | 2 +- src/components/chat/EmojiPicker.vue | 162 +++---- src/components/chat/MessageBubble.vue | 52 ++- src/components/chat/MessageInput.vue | 2 +- src/components/common/AppActionSheet.vue | 58 +++ src/components/common/AppAvatar.vue | 50 +-- src/components/common/AppConfirmHost.vue | 8 + src/components/common/AppDrawer.vue | 2 +- src/components/common/AppModal.vue | 65 +++ src/components/common/ImagePreview.vue | 4 +- src/composables/useSafeArea.ts | 40 +- src/config/app.ts | 5 + src/constants/zIndex.ts | 36 ++ src/layouts/AppChatPageLayout.vue | 26 ++ src/layouts/AppTabPageLayout.vue | 104 +++++ src/pages/chat/index.vue | 2 +- src/pages/contact/add.vue | 2 +- src/pages/contact/detail.vue | 498 ++++++++++++--------- src/pages/contact/index.vue | 6 +- src/pages/group/info.vue | 231 ++++++---- src/pages/group/members.vue | 2 +- src/pages/moment/detail.vue | 2 +- src/pages/moment/index.vue | 2 +- src/pages/profile/edit.vue | 2 +- src/pages/profile/index.vue | 2 +- src/pages/search/index.vue | 2 +- src/pages/settings/index.vue | 49 +- src/pages/settings/theme.vue | 2 +- src/stores/chat.ts | 16 +- src/styles/common.scss | 6 +- src/types/api.ts | 13 + src/utils/image.ts | 20 +- src/utils/messageMerge.ts | 57 +++ 43 files changed, 1078 insertions(+), 554 deletions(-) create mode 100644 src/api/modules/settings.ts create mode 100644 src/components/common/AppActionSheet.vue create mode 100644 src/components/common/AppConfirmHost.vue create mode 100644 src/components/common/AppModal.vue create mode 100644 src/config/app.ts create mode 100644 src/constants/zIndex.ts create mode 100644 src/layouts/AppChatPageLayout.vue create mode 100644 src/layouts/AppTabPageLayout.vue create mode 100644 src/utils/messageMerge.ts diff --git a/.gitignore b/.gitignore index 69c9a5b..09babfc 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ dist *.njsproj *.sln *.sw? +/.mimocode/ +/.vscode/ diff --git a/components.d.ts b/components.d.ts index b9d0628..9fe2676 100644 --- a/components.d.ts +++ b/components.d.ts @@ -8,12 +8,15 @@ export {} declare module 'vue' { export interface GlobalComponents { ActionMenu: typeof import('./src/components/moment/ActionMenu.vue')['default'] + AppActionSheet: typeof import('./src/components/common/AppActionSheet.vue')['default'] AppAvatar: typeof import('./src/components/common/AppAvatar.vue')['default'] + AppConfirmHost: typeof import('./src/components/common/AppConfirmHost.vue')['default'] AppDrawer: typeof import('./src/components/common/AppDrawer.vue')['default'] AppEmpty: typeof import('./src/components/common/AppEmpty.vue')['default'] AppFooter: typeof import('./src/components/AppFooter.vue')['default'] AppLoading: typeof import('./src/components/common/AppLoading.vue')['default'] AppLogos: typeof import('./src/components/AppLogos.vue')['default'] + AppModal: typeof import('./src/components/common/AppModal.vue')['default'] AppNavBar: typeof import('./src/components/common/AppNavBar.vue')['default'] AppTabBar: typeof import('./src/components/common/AppTabBar.vue')['default'] CallWindow: typeof import('./src/components/call/CallWindow.vue')['default'] @@ -41,28 +44,5 @@ declare module 'vue' { UserMomentsModal: typeof import('./src/components/moment/UserMomentsModal.vue')['default'] VideoPlayer: typeof import('./src/components/common/VideoPlayer.vue')['default'] VisibilitySelector: typeof import('./src/components/moment/VisibilitySelector.vue')['default'] - WdActionSheet: typeof import('wot-design-uni/components/wd-action-sheet/wd-action-sheet.vue')['default'] - WdBadge: typeof import('wot-design-uni/components/wd-badge/wd-badge.vue')['default'] - WdButton: typeof import('wot-design-uni/components/wd-button/wd-button.vue')['default'] - WdCell: typeof import('wot-design-uni/components/wd-cell/wd-cell.vue')['default'] - WdCellGroup: typeof import('wot-design-uni/components/wd-cell-group/wd-cell-group.vue')['default'] - WdCheckbox: typeof import('wot-design-uni/components/wd-checkbox/wd-checkbox.vue')['default'] - WdConfigProvider: typeof import('wot-design-uni/components/wd-config-provider/wd-config-provider.vue')['default'] - WdDivider: typeof import('wot-design-uni/components/wd-divider/wd-divider.vue')['default'] - WdIcon: typeof import('wot-design-uni/components/wd-icon/wd-icon.vue')['default'] - WdImg: typeof import('wot-design-uni/components/wd-img/wd-img.vue')['default'] - WdInput: typeof import('wot-design-uni/components/wd-input/wd-input.vue')['default'] - WdLoading: typeof import('wot-design-uni/components/wd-loading/wd-loading.vue')['default'] - WdLoadmore: typeof import('wot-design-uni/components/wd-loadmore/wd-loadmore.vue')['default'] - WdMessageBox: typeof import('wot-design-uni/components/wd-message-box/wd-message-box.vue')['default'] - WdNavbar: typeof import('wot-design-uni/components/wd-navbar/wd-navbar.vue')['default'] - WdPopup: typeof import('wot-design-uni/components/wd-popup/wd-popup.vue')['default'] - WdSearch: typeof import('wot-design-uni/components/wd-search/wd-search.vue')['default'] - WdStatusTip: typeof import('wot-design-uni/components/wd-status-tip/wd-status-tip.vue')['default'] - WdSwipeAction: typeof import('wot-design-uni/components/wd-swipe-action/wd-swipe-action.vue')['default'] - WdSwitch: typeof import('wot-design-uni/components/wd-switch/wd-switch.vue')['default'] - WdTag: typeof import('wot-design-uni/components/wd-tag/wd-tag.vue')['default'] - WdTextarea: typeof import('wot-design-uni/components/wd-textarea/wd-textarea.vue')['default'] - WdToast: typeof import('wot-design-uni/components/wd-toast/wd-toast.vue')['default'] } } diff --git a/src/App.vue b/src/App.vue index 5b36f84..1449f58 100644 --- a/src/App.vue +++ b/src/App.vue @@ -30,9 +30,9 @@ async function initWebSocket() { // 注册全局消息处理器 wsManager.onMessage(handleGlobalMessage) // 群通话信令监听器已移至 App.ku.vue 初始化 - console.log('✅ WebSocket 全局初始化成功') + console.log('[OK] WebSocket initialized') } catch (error) { - console.error('WebSocket 初始化失败:', error) + console.error('WebSocket init failed:', error) } } diff --git a/src/api/modules/contact.ts b/src/api/modules/contact.ts index 45df62a..ef8069c 100644 --- a/src/api/modules/contact.ts +++ b/src/api/modules/contact.ts @@ -2,7 +2,7 @@ * 联系人管理相关 API */ import request from '../request' -import type { Contact, User, FriendRequest, ContactGroup } from '@/types/api' +import type { Contact, User, FriendRequest, ContactGroup, CommonGroup } from '@/types/api' // 获取联系人列表 export function getContacts() { @@ -54,16 +54,22 @@ export function deleteGroup(id: number) { return request.post(`/contacts/groups/delete/${id}`) } -// 获取好友详情 +// 获取好友详情(合并 common_groups 等扩展字段) export async function getContactDetail(id: string): Promise { - const response = await request.get<{ contact: any; user: User }>(`/contacts/${id}`) + const response = await request.get<{ + contact: any + user: User + common_groups?: CommonGroup[] + is_friend?: boolean + }>(`/contacts/${id}`) return { ...response.contact, id: response.contact.id?.toString() || response.contact.contact_id, user_id: response.contact.contact_id || response.contact.user_id, contact_user_id: response.contact.contact_id, room_id: response.contact.room_id, - user: response.user + user: response.user, + common_groups: response.common_groups ?? [], } as Contact } diff --git a/src/api/modules/message.ts b/src/api/modules/message.ts index 40c12bf..84d0ccb 100644 --- a/src/api/modules/message.ts +++ b/src/api/modules/message.ts @@ -6,7 +6,7 @@ import type { ChatMessage, SendMessageRequest, PaginatedResponse } from '@/types // 发送消息 export function sendMessage(data: SendMessageRequest) { - return request.post('/send', data) + return request.post('/send', data) } // 获取历史消息 @@ -29,5 +29,10 @@ export function syncMessages(roomId: string, page = 1, pageSize = 50) { // 撤回消息 export function recallMessage(messageId: number) { - return request.post('/messages/recall', { message_id: messageId }) + return request.post('/messages/recall', { message_id: messageId }) +} + +/** 标记消息已读 */ +export function markMessagesRead(messageIds: number[]) { + return request.post('/messages/read-receipts', { message_ids: messageIds }) } \ No newline at end of file diff --git a/src/api/modules/settings.ts b/src/api/modules/settings.ts new file mode 100644 index 0000000..a027d85 --- /dev/null +++ b/src/api/modules/settings.ts @@ -0,0 +1,12 @@ +/** + * 用户设置 API + */ +import request from '../request' + +export function getUserSettings() { + return request.get>('/user/settings') +} + +export function updateUserSettings(settings: Record) { + return request.post>('/user/settings', { settings }) +} diff --git a/src/api/request/index.ts b/src/api/request/index.ts index 6099ce0..7328285 100644 --- a/src/api/request/index.ts +++ b/src/api/request/index.ts @@ -3,10 +3,10 @@ */ import type { ApiResponse, RequestConfig } from './types' import { storage } from '@/utils/storage' +import { API_BASE_URL } from '@/config/app' // API 基础路径 -// const BASE_URL = 'http://127.0.0.1:12080/api' -const BASE_URL = 'https://g-ws.nailaoyun.cn/api' +const BASE_URL = API_BASE_URL // 默认超时时间 const TIMEOUT = 30000 diff --git a/src/api/websocket/index.ts b/src/api/websocket/index.ts index 00f6d79..79586de 100644 --- a/src/api/websocket/index.ts +++ b/src/api/websocket/index.ts @@ -39,8 +39,8 @@ class WebSocketManager { this.userId = userId // TODO: 替换为实际的 WebSocket 地址 - // const wsUrl = `ws://g-ws.nailaoyun.cn/ws?user_id=${userId}` - const wsUrl = `wss://g-ws.nailaoyun.cn/ws?user_id=${userId}` + const wsUrl = `ws://127.0.0.1:12080/ws?user_id=${userId}` + // const wsUrl = `wss://g-ws.nailaoyun.cn/ws?user_id=${userId}` this.socketTask = uni.connectSocket({ url: wsUrl, diff --git a/src/components/call/CallWindow.vue b/src/components/call/CallWindow.vue index 6200f47..2e15990 100644 --- a/src/components/call/CallWindow.vue +++ b/src/components/call/CallWindow.vue @@ -254,7 +254,7 @@ watch(() => props.remoteStream, (stream) => { /* #endif */ /* #ifndef MP-WEIXIN */ - top: calc(var(--status-bar-height, 0) + 240rpx); + top: calc(var(--status-bar-height, 20px) + 240rpx); /* #endif */ } } @@ -367,7 +367,7 @@ watch(() => props.remoteStream, (stream) => { /* #endif */ /* #ifndef MP-WEIXIN */ - top: calc(28rpx + var(--status-bar-height, 0)); + top: calc(28rpx + var(--status-bar-height, 20px)); /* #endif */ align-items: center; justify-content: center; diff --git a/src/components/call/GlobalCallProvider.vue b/src/components/call/GlobalCallProvider.vue index 05892af..74491cd 100644 --- a/src/components/call/GlobalCallProvider.vue +++ b/src/components/call/GlobalCallProvider.vue @@ -1,5 +1,5 @@ @@ -64,26 +59,20 @@ import MiniProgramCallWindow from './MiniProgramCallWindow.vue' const mpCall = useMiniProgramCall() // #endif -// 群通话 const { initListener: initGroupListener } = useGroupWebRTC() onMounted(() => { // #ifdef H5 || APP-PLUS - // 初始化单聊通话信令监听器 (H5/App) webrtc.initListener() - console.log('✅ [GlobalCallProvider] H5/App WebRTC 信令监听器已初始化') + console.log('[GlobalCallProvider] H5/App WebRTC listener initialized') // #endif // #ifdef MP-WEIXIN - // 初始化小程序通话信令监听器 - // 注意:initPusherContext 不在这里调用,因为此时 live-pusher 组件还未渲染 - // live-pusher 的 Context 初始化由 MiniProgramCallWindow.vue 在 pushUrl 设置后处理 mpCall.initListener() - console.log('✅ [GlobalCallProvider] 微信小程序通话监听器已初始化') + console.log('[GlobalCallProvider] MP call listener initialized') // #endif - // 初始化群通话信令监听器(所有平台) initGroupListener() - console.log('✅ [GlobalCallProvider] 群通话信令监听器已初始化') + console.log('[GlobalCallProvider] Group call listener initialized') }) diff --git a/src/components/call/GroupCallModal.vue b/src/components/call/GroupCallModal.vue index a965586..26e9886 100644 --- a/src/components/call/GroupCallModal.vue +++ b/src/components/call/GroupCallModal.vue @@ -179,7 +179,7 @@ function handleClose() { .modal-container { position: fixed; inset: 0; - z-index: 1000; + z-index: 2100; --bg-surface: #fff; --bg-hover: #f5f5f5; diff --git a/src/components/chat/EmojiPicker.vue b/src/components/chat/EmojiPicker.vue index 358220a..40f4f5f 100644 --- a/src/components/chat/EmojiPicker.vue +++ b/src/components/chat/EmojiPicker.vue @@ -1,6 +1,5 @@ @@ -105,6 +111,12 @@ defineEmits<{ const avatarName = computed(() => props.senderName || 'U') +/** 是否展示 IP 归属地(过滤本地/未知) */ +const showIpLocation = computed(() => { + const loc = props.msg.ip_location + return !!loc && loc !== '本地' && loc !== '未知' +}) + const mediaTypeClass = computed(() => { const type = props.msg.message_type if (type === 1) return 'media-bubble image' @@ -114,7 +126,7 @@ const mediaTypeClass = computed(() => { return '' }) -// Helpers (复用原 index.vue 逻辑) +/** 解析媒体 URL(与 chat/index.vue 保持一致) */ function getMediaUrl(msg: ChatMessage): string { let url = '' if (typeof msg.extra === 'string') { @@ -141,11 +153,8 @@ function getVideoThumb(msg: ChatMessage) { diff --git a/src/components/chat/MessageInput.vue b/src/components/chat/MessageInput.vue index 4430a98..01586bf 100644 --- a/src/components/chat/MessageInput.vue +++ b/src/components/chat/MessageInput.vue @@ -322,7 +322,7 @@ function formatRecordDuration(s: number) { return `${Math.floor(s/60).toString() .input-floater { position: fixed; bottom: 0; left: 0; right: 0; - z-index: 200; + z-index: 1; background: var(--bg-floater); border-top: 2rpx solid var(--border-color); diff --git a/src/components/common/AppActionSheet.vue b/src/components/common/AppActionSheet.vue new file mode 100644 index 0000000..435af03 --- /dev/null +++ b/src/components/common/AppActionSheet.vue @@ -0,0 +1,58 @@ + + + diff --git a/src/components/common/AppAvatar.vue b/src/components/common/AppAvatar.vue index 8da69d2..c5abb94 100644 --- a/src/components/common/AppAvatar.vue +++ b/src/components/common/AppAvatar.vue @@ -1,13 +1,14 @@