Explorar o código

红娘工作台系统通知

mazhenhang hai 1 mes
pai
achega
743fac406d

+ 13 - 0
LiangZhiYUMao/App.vue

@@ -3,6 +3,12 @@ import timPresenceManager from '@/utils/tim-presence-manager.js';
 import timManager from '@/utils/tim-manager.js';
 
 export default {
+	data() {
+		return {
+			_timInitializing: false
+		}
+	},
+
 	onLaunch: function () {
 		console.log('=== App启动 ===');
 		
@@ -42,6 +48,11 @@ export default {
 		 * 初始化全局TIM
 		 */
 		async initGlobalTIM() {
+			// 避免在登录过程中被重复触发
+			if (this._timInitializing) {
+				return
+			}
+			this._timInitializing = true
 			try {
 				// 获取当前登录用户ID
 				const userInfo = uni.getStorageSync('userInfo');
@@ -98,6 +109,8 @@ export default {
 			}
 			} catch (error) {
 				console.error('❌ 初始化全局TIM失败:', error);
+			} finally {
+				this._timInitializing = false
 			}
 		},
 		

+ 7 - 0
LiangZhiYUMao/pages.json

@@ -70,6 +70,13 @@
 				"navigationStyle": "custom"
 			}
 		},
+		{
+			"path": "pages/matchmaker-workbench/system-messages",
+			"style": {
+				"navigationBarTitleText": "系统通知",
+				"navigationStyle": "custom"
+			}
+		},
 		{
 			"path": "pages/matchmaker-workbench/mine",
 			"style": {

+ 85 - 26
LiangZhiYUMao/pages/index/index.vue

@@ -222,7 +222,7 @@
 				<text class="tabbar-text">我的</text>
 			</view>
 		</view>
-		
+
 		<!-- 红娘提示弹框 -->
 		<uni-popup ref="matchmakerPopup" type="dialog">
 			<uni-popup-dialog
@@ -240,42 +240,39 @@
 
 <script>
 	import api, { request } from '@/utils/api.js'
-    import { formatTime, formatCountdown, isLoggedIn, goToLogin } from '@/utils/util.js'
+	import { formatTime, formatCountdown, isLoggedIn, goToLogin } from '@/utils/util.js'
 	import { DEFAULT_IMAGES, ACTIVITY_TYPES } from '@/config/index.js'
 	
+	import timManager from '@/utils/tim-manager.js'
+
 	export default {
-		components: {},
 		data() {
 			return {
 				// 用户信息
-			userInfo: {
-				nickname: '小张',
-				userId: null,
-				isMatchmaker: '1' // 测试用,实际从后端获取
-			},
+				userInfo: {
+					nickname: '小张',
+					userId: null,
+					isMatchmaker: '1' // 测试用,实际从后端获取
+				},
 				matchCount: 3,
-			
 
 				// 轮播图数据
-				bannerList: [
-
-				],
+				bannerList: [],
 
 				// 公告数据
-				noticeList: [
+				noticeList: [],
+
+				// 功能入口
+				functionList: [
+					{ id: 1, name: '星命测算', icon: '💖', path: '/pages/astrology/index', bgColor: '#FF6B9D', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FF6B9D 0%, #FF8EAB 100%)' },
+					{ id: 2, name: '红娘列表', icon: '👤', path: '/pages/matchmakers/list', bgColor: '#6BC5F8', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #6BC5F8 0%, #87CEEB 100%)' },
+					// { id: 3, name: '加入红娘', icon: '💕', path: '/pages/part-time-matchmaker/index', bgColor: '#9B7EDE', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #9B7EDE 0%, #B19CD9 100%)' },
+					// { id: 3, name: '加入红娘', icon: '💕', path: '/pages/part-time-matchmaker/index', bgColor: '#9B7EDE', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #9B7EDE 0%, #B19CD9 100%)' },
+					{ id: 4, name: '精品课程', icon: '📚', path: '/pages/courses/list', bgColor: '#FF8C42', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FF8C42 0%, #FFA366 100%)' },
+					{ id: 5, name: '今日缘分', icon: '💝', path: '/pages/recommend/index', bgColor: '#FF69B4', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FF69B4 0%, #FF8CC8 100%)', needLogin: true },
+					// { id: 6, name: '专属定制', icon: '🎁', path: '/pages/customize/index', bgColor: '#FFA500', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FFA500 0%, #FFB84D 100%)' }
 				],
 
-			// 功能入口
-			functionList: [
-				{ id: 1, name: '星命测算', icon: '💖', path: '/pages/astrology/index', bgColor: '#FF6B9D', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FF6B9D 0%, #FF8EAB 100%)' },
-				{ id: 2, name: '红娘列表', icon: '👤', path: '/pages/matchmakers/list', bgColor: '#6BC5F8', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #6BC5F8 0%, #87CEEB 100%)' },
-				// { id: 3, name: '加入红娘', icon: '💕', path: '/pages/part-time-matchmaker/index', bgColor: '#9B7EDE', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #9B7EDE 0%, #B19CD9 100%)' },
-				// { id: 3, name: '加入红娘', icon: '💕', path: '/pages/part-time-matchmaker/index', bgColor: '#9B7EDE', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #9B7EDE 0%, #B19CD9 100%)' },
-				{ id: 4, name: '精品课程', icon: '📚', path: '/pages/courses/list', bgColor: '#FF8C42', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FF8C42 0%, #FFA366 100%)' },
-				{ id: 5, name: '今日缘分', icon: '💝', path: '/pages/recommend/index', bgColor: '#FF69B4', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FF69B4 0%, #FF8CC8 100%)', needLogin: true },
-				// { id: 6, name: '专属定制', icon: '🎁', path: '/pages/customize/index', bgColor: '#FFA500', iconColor: '#FFFFFF', gradient: 'linear-gradient(135deg, #FFA500 0%, #FFB84D 100%)' }
-			],
-
 				// 热门活动
 				hotActivities: [],
 
@@ -356,11 +353,73 @@
 			},
 			
 			// 前往红娘工作台
-			goToMatchmakerWorkspace() {
+			async goToMatchmakerWorkspace() {
 				console.log('🚀 前往红娘工作台')
 				this.$refs.matchmakerPopup.close()
 				
-				// 跳转到红娘工作台
+				try {
+					const userId = uni.getStorageSync('userId')
+					if (!userId) {
+						uni.showToast({ title: '请先登录', icon: 'none' })
+						return
+					}
+					
+					if (!timManager.tim) {
+						try {
+							timManager.init(1600109674)
+							console.log('✅ TIM SDK 初始化完成(首页)')
+						} catch (e) {
+							console.error('❌ TIM 初始化失败(首页):', e)
+						}
+					}
+					
+					const res = await uni.request({
+						url: 'http://localhost:8081/api/matchmaker/current',
+						method: 'GET',
+						data: { userId }
+					})
+					
+					if (!res[1] || res[1].data.code !== 200) {
+						uni.showToast({ title: '获取红娘信息失败', icon: 'none' })
+						return
+					}
+					
+					const matchmakerInfo = res[1].data.data || {}
+					const imUserId = matchmakerInfo.imUserId
+					if (!imUserId) {
+						uni.showToast({ title: '红娘IM账号缺失', icon: 'none' })
+						return
+					}
+					
+					const sigRes = await uni.request({
+						url: `http://localhost:8083/api/im/getUserSig?userId=${imUserId}`,
+						method: 'GET'
+					})
+					
+					if (!sigRes[1] || sigRes[1].data.code !== 200) {
+						uni.showToast({ title: '获取UserSig失败', icon: 'none' })
+						return
+					}
+					
+					const userSig = sigRes[1].data.data.userSig
+					console.log('✅ 首页获取 UserSig:', userSig ? '成功' : '失败')
+					
+					try {
+						await timManager.logout()
+						console.log('✅ 已登出之前的IM账号(首页)')
+					} catch (e) {
+						console.log('⚠️ 登出失败或未登录(首页):', e.message)
+					}
+					
+					await timManager.login(imUserId, userSig)
+					console.log('✅ 红娘已在首页登录 IM:', imUserId)
+					
+					uni.$emit('matchmakerIMReady', { imUserId })
+				} catch (error) {
+					console.error('❌ 首页初始化红娘IM失败:', error)
+					uni.showToast({ title: '红娘IM初始化失败', icon: 'none' })
+				}
+				
 				uni.navigateTo({
 					url: '/pages/matchmaker-workbench/index'
 				})

+ 320 - 32
LiangZhiYUMao/pages/matchmaker-workbench/message.vue

@@ -13,30 +13,118 @@
 				<text>加载中...</text>
 			</view>
 			
-			<!-- 会话列表 -->
+			<!-- 消息内容 -->
 			<view v-else-if="conversationList.length > 0">
-				<view 
-					v-for="conversation in conversationList" 
-					:key="conversation.conversationID"
-					class="message-item user-message"
-					@click="openChat(conversation)"
-				>
-					<image 
-						v-if="conversation.userProfile.avatar" 
-						:src="conversation.userProfile.avatar" 
-						class="message-avatar-img"
-					/>
-					<view v-else class="message-avatar">
-						{{ conversation.userProfile.nick || conversation.userProfile.userID.charAt(0) }}
-					</view>
+				<!-- 系统通知卡片 -->
+				<view class="message-item system-notification" v-if="systemNotification" @click="openSystemMessages">
+					<text class="message-type">{{ systemNotification.title }}</text>
+					<text class="message-time">{{ systemNotification.timeText }}</text>
+					<text class="message-content">{{ systemNotification.content }}</text>
+					<text class="message-footer">{{ systemNotification.footer }}</text>
+				</view>
+				
+				<!-- 撮合成功通知卡片 -->
+				<view class="message-item match-success" v-if="matchSuccessNotification">
+					<view class="message-icon heart"></view>
 					<view class="message-body">
-						<text class="message-type">{{ conversation.userProfile.nick || conversation.userProfile.userID }}</text>
-						<text class="message-content">{{ getLastMessageText(conversation.lastMessage) }}</text>
+						<text class="message-type">{{ matchSuccessNotification.title }}</text>
+						<text class="message-content">{{ matchSuccessNotification.content }}</text>
+					</view>
+					<text class="message-time">{{ matchSuccessNotification.timeText }}</text>
+				</view>
+				
+				<!-- 今天 -->
+				<view v-if="todayConversations.length > 0">
+					<view class="time-group">
+						<text class="time-label">今天</text>
 					</view>
-					<view class="message-right">
-						<text class="message-time">{{ formatTime(conversation.lastMessage.lastTime) }}</text>
-						<view v-if="conversation.unreadCount > 0" class="unread-badge">
-							{{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
+					<view
+						v-for="conversation in todayConversations"
+						:key="conversation.conversationID"
+						class="message-item user-message"
+						@click="openChat(conversation)"
+					>
+						<image
+							v-if="conversation.userProfile.avatar"
+							:src="conversation.userProfile.avatar"
+							class="message-avatar-img"
+						/>
+						<view v-else class="message-avatar">
+							{{ conversation.userProfile.nick || conversation.userProfile.userID.charAt(0) }}
+						</view>
+						<view class="message-body">
+							<text class="message-type">{{ conversation.userProfile.nick || conversation.userProfile.userID }}</text>
+							<text class="message-content">{{ getLastMessageText(conversation.lastMessage) }}</text>
+						</view>
+						<view class="message-right">
+							<text class="message-time">{{ formatTime(conversation.lastMessage.lastTime) }}</text>
+							<view v-if="conversation.unreadCount > 0" class="unread-badge">
+								{{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
+							</view>
+						</view>
+					</view>
+				</view>
+				
+				<!-- 昨日 -->
+				<view v-if="yesterdayConversations.length > 0">
+					<view class="time-group">
+						<text class="time-label">昨日</text>
+					</view>
+					<view
+						v-for="conversation in yesterdayConversations"
+						:key="conversation.conversationID"
+						class="message-item user-message"
+						@click="openChat(conversation)"
+					>
+						<image
+							v-if="conversation.userProfile.avatar"
+							:src="conversation.userProfile.avatar"
+							class="message-avatar-img"
+						/>
+						<view v-else class="message-avatar">
+							{{ conversation.userProfile.nick || conversation.userProfile.userID.charAt(0) }}
+						</view>
+						<view class="message-body">
+							<text class="message-type">{{ conversation.userProfile.nick || conversation.userProfile.userID }}</text>
+							<text class="message-content">{{ getLastMessageText(conversation.lastMessage) }}</text>
+						</view>
+						<view class="message-right">
+							<text class="message-time">{{ formatTime(conversation.lastMessage.lastTime) }}</text>
+							<view v-if="conversation.unreadCount > 0" class="unread-badge">
+								{{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
+							</view>
+						</view>
+					</view>
+				</view>
+				
+				<!-- 更早 -->
+				<view v-if="earlierConversations.length > 0">
+					<view class="time-group">
+						<text class="time-label">更早</text>
+					</view>
+					<view
+						v-for="conversation in earlierConversations"
+						:key="conversation.conversationID"
+						class="message-item user-message"
+						@click="openChat(conversation)"
+					>
+						<image
+							v-if="conversation.userProfile.avatar"
+							:src="conversation.userProfile.avatar"
+							class="message-avatar-img"
+						/>
+						<view v-else class="message-avatar">
+							{{ conversation.userProfile.nick || conversation.userProfile.userID.charAt(0) }}
+						</view>
+						<view class="message-body">
+							<text class="message-type">{{ conversation.userProfile.nick || conversation.userProfile.userID }}</text>
+							<text class="message-content">{{ getLastMessageText(conversation.lastMessage) }}</text>
+						</view>
+						<view class="message-right">
+							<text class="message-time">{{ formatTime(conversation.lastMessage.lastTime) }}</text>
+							<view v-if="conversation.unreadCount > 0" class="unread-badge">
+								{{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
+							</view>
 						</view>
 					</view>
 				</view>
@@ -64,7 +152,9 @@
 			</view>
 			<view class="tabbar-item message active" @click="navigateToMessage">
 				<view class="tabbar-icon">
-					<view class="badge">3</view>
+					<view v-if="totalUnreadCount > 0" class="badge">
+						{{ totalUnreadCount > 99 ? '99+' : totalUnreadCount }}
+					</view>
 				</view>
 				<text class="tabbar-text">消息</text>
 			</view>
@@ -79,6 +169,7 @@
 <script>
 	import timManager from '@/utils/tim-manager.js'
 	import TIM from 'tim-wx-sdk'
+	import api from '@/utils/api.js'
 	
 	export default {
 		data() {
@@ -87,12 +178,56 @@
 				conversationList: [],
 				matchmakerInfo: null,
 				imUserId: '',
-				totalUnreadCount: 0
+				totalUnreadCount: 0,
+				// 系统消息未读数
+				systemUnread: 0,
+				
+				// 顶部系统通知占位(后续可从后端加载)
+				systemNotification: {
+					title: '系统通知',
+					content: '您的线索审核已通过,获得20积分奖励,当期积分可兑换【资源查看权限】x1',
+					footer: '完成线索录采集,积分+20(距离黄金级还差72分)',
+					timeText: '刚刚'
+				},
+				// 撮合成功通知占位
+				matchSuccessNotification: {
+					title: '撮合成功通知',
+					content: '您推荐的李先生和王女士已成功匹配,获得50积分+100元现金奖励',
+					timeText: '10分钟前'
+				}
+			}
+		},
+		computed: {
+			// 今天的会话
+			todayConversations() {
+				const today = new Date()
+				return this.conversationList.filter(conv => this.isSameDay(conv.lastMessage, today))
+			},
+			// 昨日会话
+			yesterdayConversations() {
+				const yesterday = new Date()
+				yesterday.setDate(yesterday.getDate() - 1)
+				return this.conversationList.filter(conv => this.isSameDay(conv.lastMessage, yesterday))
+			},
+			// 更早会话
+			earlierConversations() {
+				const today = new Date()
+				const yesterday = new Date()
+				yesterday.setDate(yesterday.getDate() - 1)
+				return this.conversationList.filter(conv => {
+					const msg = conv.lastMessage
+					if (!msg || !msg.lastTime) return false
+					const isToday = this.isSameDay(msg, today)
+					const isYesterday = this.isSameDay(msg, yesterday)
+					return !isToday && !isYesterday
+				})
 			}
 		},
 		
-		async onLoad() {
-			await this.initIM()
+		onLoad() {
+			// 为避免误用用户端 IM 账号,这里不再复用当前 TIM 登录状态,进入页面即按红娘流程重新初始化
+			console.log('🔍 红娘消息页强制按红娘流程初始化 IM')
+			this.initIM()
 		},
 		
 		onShow() {
@@ -100,6 +235,8 @@
 			if (this.imUserId) {
 				this.loadConversationList()
 			}
+			// 同步系统消息未读
+			this.loadSystemUnread()
 		},
 		
 		onUnload() {
@@ -108,6 +245,77 @@
 		},
 		
 		methods: {
+			// 判断消息是否与给定日期同一天
+			isSameDay(lastMessage, baseDate) {
+				if (!lastMessage || !lastMessage.lastTime) return false
+				const msgDate = new Date(lastMessage.lastTime * 1000)
+				const d = baseDate instanceof Date ? baseDate : new Date(baseDate)
+				return (
+					msgDate.getFullYear() === d.getFullYear() &&
+					msgDate.getMonth() === d.getMonth() &&
+					msgDate.getDate() === d.getDate()
+				)
+			},
+
+			// 加载系统消息未读数,并同步最新一条系统通知到顶部卡片
+			async loadSystemUnread() {
+				try {
+					const userId = uni.getStorageSync('userId')
+					if (!userId) return
+					const count = await api.message.getSystemUnreadCount(userId)
+					// getSystemUnreadCount 可能返回 {code,data} 或直接 number,这里统一兼容
+					if (typeof count === 'number') {
+						this.systemUnread = count
+					} else if (count && typeof count.data === 'number') {
+						this.systemUnread = count.data
+					}
+					
+					// 同时拉取最新一条系统通知,用于顶部卡片展示
+					const res = await api.message.getSystemList(userId, 1, 1)
+					const list = (res && (res.list || res.data?.list)) || []
+					if (list.length > 0) {
+						const item = list[0]
+						// 计算时间文案
+						let timeText = ''
+						const t = item.createdAt || item.created_at
+						if (t) {
+							const d2 = new Date(t)
+							const now = new Date()
+							const diff = now - d2
+							if (diff < 60000) {
+								timeText = '刚刚'
+							} else if (diff < 3600000) {
+								timeText = Math.floor(diff / 60000) + '分钟前'
+							} else {
+								const mm = String(d2.getMonth() + 1).padStart(2, '0')
+								const dd = String(d2.getDate()).padStart(2, '0')
+								const hh = String(d2.getHours()).padStart(2, '0')
+								const mi = String(d2.getMinutes()).padStart(2, '0')
+								timeText = `${mm}-${dd} ${hh}:${mi}`
+							}
+						}
+						
+						// 用最新系统通知更新卡片文案(小字显示最新内容)
+						this.systemNotification = {
+							...this.systemNotification,
+							title: '系统通知',
+							content: item.title || this.systemNotification.content,
+							footer: item.content || this.systemNotification.footer,
+							timeText: timeText || this.systemNotification.timeText
+						}
+					}
+				} catch (e) {
+					console.log('红娘消息页加载系统未读失败', e)
+				}
+			},
+
+			// 跳转系统通知列表
+			openSystemMessages() {
+				uni.navigateTo({
+					url: '/pages/matchmaker-workbench/system-messages'
+				})
+			},
+
 			// 初始化 IM
 			async initIM() {
 				try {
@@ -181,13 +389,21 @@
 			// 加载会话列表
 			async loadConversationList() {
 				try {
+					// SDK 未登录 / 未 ready 时不调用会话列表接口,避免报“接口调用时机不合理”
+					if (!timManager.isLogin || !timManager.tim) {
+						console.log('⚠️ TIM 未 ready,暂不加载会话列表')
+						return
+					}
 					const tim = timManager.getTim()
 					const { data } = await tim.getConversationList()
-					this.conversationList = data.conversationList
+					this.conversationList = data.conversationList || []
+					
+					// 补全用户头像和昵称
+					await this.loadUserAvatars()
 					
 					// 计算总未读数
 					this.totalUnreadCount = this.conversationList.reduce((total, conv) => {
-						return total + conv.unreadCount
+						return total + (conv.unreadCount || 0)
 					}, 0)
 					
 					console.log('✅ 会话列表:', this.conversationList)
@@ -224,12 +440,84 @@
 			// 会话列表更新
 			onConversationListUpdated(event) {
 				console.log('🔄 会话列表更新:', event.data)
-				this.conversationList = event.data
+				this.conversationList = event.data || []
 				
-				// 更新总未读数
-				this.totalUnreadCount = this.conversationList.reduce((total, conv) => {
-					return total + conv.unreadCount
-				}, 0)
+				// 补全头像昵称后再计算未读
+				this.loadUserAvatars().then(() => {
+					this.totalUnreadCount = this.conversationList.reduce((total, conv) => {
+						return total + (conv.unreadCount || 0)
+					}, 0)
+				})
+			},
+			
+			// 批量补全用户头像和昵称
+			async loadUserAvatars() {
+				try {
+					if (!this.conversationList || this.conversationList.length === 0) {
+						return
+					}
+					
+					// 收集对端用户ID(排除红娘 m_ 开头的ID)
+					const userIds = []
+					this.conversationList.forEach(conv => {
+						let targetUserId = ''
+						if (conv.userProfile && conv.userProfile.userID) {
+							targetUserId = String(conv.userProfile.userID)
+						} else if (conv.conversationID && conv.conversationID.indexOf('C2C') === 0) {
+							targetUserId = conv.conversationID.replace('C2C', '')
+						}
+						if (!targetUserId) return
+						if (targetUserId.startsWith('m_')) return
+						userIds.push(targetUserId)
+					})
+					
+					const uniqueIds = Array.from(new Set(userIds))
+					if (uniqueIds.length === 0) return
+					
+					console.log('🔄 红娘消息页批量获取用户信息,数量:', uniqueIds.length)
+					
+					const res = await uni.request({
+						url: 'http://localhost:8083/api/user/batch',
+						method: 'GET',
+						data: {
+							userIds: uniqueIds.join(',')
+						}
+					})
+					
+					if (!res[1] || res[1].statusCode !== 200 || res[1].data.code !== 200) {
+						console.warn('⚠️ 红娘消息页批量获取用户信息失败')
+						return
+					}
+					
+					const list = res[1].data.data || []
+					const userMap = {}
+					list.forEach(user => {
+						if (!user || user.userId == null) return
+						userMap[String(user.userId)] = {
+							nickname: user.nickname,
+							avatarUrl: user.avatarUrl
+						}
+					})
+					
+					// 回填到会话列表的 userProfile 中
+					this.conversationList = this.conversationList.map(conv => {
+						let targetUserId = ''
+						if (conv.userProfile && conv.userProfile.userID) {
+							targetUserId = String(conv.userProfile.userID)
+						} else if (conv.conversationID && conv.conversationID.indexOf('C2C') === 0) {
+							targetUserId = conv.conversationID.replace('C2C', '')
+						}
+						const info = targetUserId ? userMap[targetUserId] : null
+						if (info) {
+							conv.userProfile = conv.userProfile || {}
+							conv.userProfile.nick = info.nickname || conv.userProfile.nick || `用户${targetUserId}`
+							conv.userProfile.avatar = info.avatarUrl || conv.userProfile.avatar || ''
+						}
+						return conv
+					})
+				} catch (error) {
+					console.error('❌ 红娘消息页加载用户头像失败:', error)
+				}
 			},
 			
 			// 打开聊天页面

+ 289 - 0
LiangZhiYUMao/pages/matchmaker-workbench/system-messages.vue

@@ -0,0 +1,289 @@
+<template>
+  <view class="matchmaker-system-page">
+    <!-- 顶部导航栏 -->
+    <view class="header">
+      <view class="back-btn" @click="goBack"></view>
+      <text class="header-title">系统通知</text>
+      <view class="placeholder">
+        <button class="read-all-btn" @click.stop="markAllRead" size="mini">一键已读</button>
+      </view>
+    </view>
+
+    <!-- 列表区域 -->
+    <scroll-view scroll-y class="content" @scrolltolower="loadMore">
+      <view v-if="loading && list.length === 0" class="loading-container">
+        <text>加载中...</text>
+      </view>
+
+      <view v-else-if="list.length === 0" class="empty-container">
+        <text>暂无系统通知</text>
+      </view>
+
+      <view v-else>
+        <view
+          v-for="item in list"
+          :key="item.id"
+          class="system-item"
+          @click="openDetail(item)"
+        >
+          <view class="item-left">
+            <view class="icon-wrapper">
+              <text class="icon">📢</text>
+              <view v-if="item.isRead === 0" class="dot"></view>
+            </view>
+          </view>
+          <view class="item-body">
+            <view class="item-title-row">
+              <text class="item-title">{{ item.title || '系统通知' }}</text>
+              <text class="item-time">{{ formatTime(item.createdAt || item.created_at) }}</text>
+            </view>
+            <text class="item-content">{{ item.content }}</text>
+          </view>
+        </view>
+
+        <view class="load-more" v-if="hasMore">
+          {{ loading ? '加载中...' : '上拉加载更多' }}
+        </view>
+        <view class="load-more" v-else>没有更多了</view>
+      </view>
+    </scroll-view>
+  </view>
+</template>
+
+<script>
+import api from '@/utils/api.js'
+
+export default {
+  data() {
+    return {
+      userId: null,
+      list: [],
+      pageNum: 1,
+      pageSize: 20,
+      hasMore: true,
+      loading: false
+    }
+  },
+  onLoad() {
+    this.userId = uni.getStorageSync('userId')
+    this.loadList()
+  },
+  methods: {
+    async loadList() {
+      if (this.loading || !this.hasMore) return
+      this.loading = true
+      try {
+        const res = await api.message.getSystemList(this.userId, this.pageNum, this.pageSize)
+        const data = (res && (res.list || res.data?.list)) || []
+        this.list = this.list.concat(data)
+        const total = (typeof res.total === 'number') ? res.total : (res.data?.total || this.list.length)
+        this.hasMore = this.list.length < total
+        this.pageNum++
+      } catch (e) {
+        console.log('红娘系统通知加载失败', e)
+      } finally {
+        this.loading = false
+      }
+    },
+    loadMore() {
+      this.loadList()
+    },
+    async markAllRead() {
+      if (!this.userId) return
+      try {
+        await api.message.markAllSystemRead(this.userId)
+        // 本地列表全部置为已读
+        this.list.forEach(item => {
+          item.isRead = 1
+        })
+        uni.showToast({ title: '已全部标记为已读', icon: 'success' })
+      } catch (e) {
+        console.log('一键已读失败', e)
+        uni.showToast({ title: '操作失败,请稍后重试', icon: 'none' })
+      }
+    },
+    async openDetail(item) {
+      try {
+        const data = await api.message.getSystemDetail(item.id)
+        if (item.isRead === 0) {
+          try {
+            await api.message.markSystemRead(item.id)
+            item.isRead = 1
+          } catch (e) {}
+        }
+        uni.showModal({
+          title: data.title || '系统通知',
+          content: (data.content || '').replace(/\n/g, '\n'),
+          showCancel: false
+        })
+      } catch (e) {
+        uni.showToast({ title: '获取详情失败', icon: 'none' })
+      }
+    },
+    formatTime(t) {
+      if (!t) return ''
+      const d = new Date(t)
+      const month = String(d.getMonth() + 1).padStart(2, '0')
+      const day = String(d.getDate()).padStart(2, '0')
+      const hour = String(d.getHours()).padStart(2, '0')
+      const minute = String(d.getMinutes()).padStart(2, '0')
+      return `${month}-${day} ${hour}:${minute}`
+    },
+    goBack() {
+      uni.navigateBack()
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.matchmaker-system-page {
+  min-height: 100vh;
+  background: #FFF9F9;
+  display: flex;
+  flex-direction: column;
+}
+
+.header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 25rpx 30rpx;
+  padding-top: calc(25rpx + env(safe-area-inset-top));
+  background: #FFF9F9;
+  border-bottom: 1rpx solid #F0F0F0;
+
+  .back-btn {
+    width: 70rpx;
+    height: 70rpx;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background: rgba(240, 240, 240, 0.5);
+    border-radius: 50%;
+    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23333"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>');
+    background-size: 40rpx 40rpx;
+    background-repeat: no-repeat;
+    background-position: center;
+  }
+
+  .header-title {
+    font-size: 36rpx;
+    font-weight: bold;
+    color: #333;
+  }
+
+  .placeholder {
+    min-width: 120rpx;
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+
+    .read-all-btn {
+      min-width: 96rpx;
+      height: 48rpx;
+      line-height: 48rpx;
+      padding: 0 12rpx;
+      font-size: 22rpx;
+      color: #666;
+      background: #ffffff;
+      border-radius: 999rpx;
+      border: 1rpx solid #e5e5e5;
+      box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.04);
+      text-align: center;
+    }
+  }
+}
+
+.content {
+  flex: 1;
+  padding: 20rpx 20rpx 40rpx;
+}
+
+.system-item {
+  display: flex;
+  padding: 24rpx 20rpx;
+  margin-bottom: 20rpx;
+  border-radius: 20rpx;
+  background: #FFFFFF;
+  box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
+}
+
+.item-left {
+  margin-right: 20rpx;
+
+  .icon-wrapper {
+    width: 64rpx;
+    height: 64rpx;
+    border-radius: 50%;
+    background: linear-gradient(135deg, #FCE4EC 0%, #F8BBD0 100%);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+
+    .icon {
+      font-size: 32rpx;
+    }
+
+    .dot {
+      position: absolute;
+      top: -6rpx;
+      right: -6rpx;
+      width: 18rpx;
+      height: 18rpx;
+      border-radius: 50%;
+      background: #FF4444;
+      border: 2rpx solid #FFFFFF;
+    }
+  }
+}
+
+.item-body {
+  flex: 1;
+  min-width: 0;
+
+  .item-title-row {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 8rpx;
+
+    .item-title {
+      font-size: 30rpx;
+      font-weight: bold;
+      color: #333;
+    }
+
+    .item-time {
+      font-size: 24rpx;
+      color: #999;
+      margin-left: 20rpx;
+      flex-shrink: 0;
+    }
+  }
+
+  .item-content {
+    font-size: 26rpx;
+    color: #666;
+    line-height: 1.5;
+  }
+}
+
+.loading-container,
+.empty-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 80rpx 0;
+  color: #999;
+  font-size: 28rpx;
+}
+
+.load-more {
+  text-align: center;
+  padding: 20rpx 0 40rpx;
+  color: #999;
+  font-size: 24rpx;
+}
+</style>

+ 33 - 1
LiangZhiYUMao/pages/message/chat.vue

@@ -398,8 +398,12 @@ export default {
       this.userId = String(rawUserId);
     }
     
-    // 头像:默认先用用户信息中的头像(红娘入口会被 loadMatchmakerAvatarByImId 覆盖)
+    // 头像:默认先用用户信息中的头像(红娘入口会被 loadMatchmakerAvatar 覆盖)
     this.userAvatar = userInfo.avatar || userInfo.avatarUrl || '/static/default-avatar.svg';
+    // 如果是红娘入口,再从红娘后台加载一次头像覆盖默认值
+    if (this.fromMatchmaker) {
+      await this.loadMatchmakerAvatar();
+    }
     
     // 获取对方用户信息(确保是字符串格式)
     this.targetUserId = String(options.targetUserId);
@@ -460,6 +464,34 @@ export default {
   },
 
   methods: {
+    /**
+     * 红娘入口:根据当前登录用户ID获取红娘资料并设置头像
+     */
+    async loadMatchmakerAvatar() {
+      try {
+        const userId = uni.getStorageSync('userId');
+        if (!userId) {
+          console.warn('⚠️ 红娘聊天页:本地无 userId,无法加载红娘头像');
+          return;
+        }
+        const res = await uni.request({
+          url: 'http://localhost:8081/api/matchmaker/current',
+          method: 'GET',
+          data: { userId }
+        });
+        if (!res[1] || res[1].statusCode !== 200 || res[1].data.code !== 200) {
+          console.warn('⚠️ 红娘聊天页:获取红娘信息失败');
+          return;
+        }
+        const info = res[1].data.data || {};
+        if (info.avatarUrl) {
+          this.userAvatar = info.avatarUrl;
+        }
+      } catch (e) {
+        console.error('❌ 红娘聊天页加载头像失败:', e);
+      }
+    },
+
     /**
      * 初始化 TIM
      */

+ 33 - 22
LiangZhiYUMao/utils/tim-presence-manager.js

@@ -6,6 +6,9 @@
 import timManager from './tim-manager.js';
 import TIM from 'tim-wx-sdk';
 
+// WebSocket/TIM 调试日志开关
+const DEBUG_WS = false;
+
 class TIMPresenceManager {
   constructor() {
     this.ws = null;
@@ -40,12 +43,14 @@ class TIMPresenceManager {
   async init(userId) {
     this.userId = userId;
     
-    console.log('🚀 ========== 初始化 tim-presence-manager ==========');
-    console.log('🚀 用户ID:', userId);
-    console.log('🚀 WebSocket URL:', this.wsUrl);
+    if (DEBUG_WS) {
+      console.log('🚀 ========== 初始化 tim-presence-manager ==========');
+      console.log('🚀 用户ID:', userId);
+      console.log('🚀 WebSocket URL:', this.wsUrl);
+    }
     
     // 连接 WebSocket(接收服务端推送的状态变更)
-    console.log('🚀 开始连接 WebSocket...');
+    if (DEBUG_WS) console.log('🚀 开始连接 WebSocket...');
     this.connectWebSocket();
     
     console.log('✅ tim-presence-manager 初始化完成(WebSocket连接中...)');
@@ -58,9 +63,11 @@ class TIMPresenceManager {
   connectWebSocket() {
     try {
       const wsUrl = `${this.wsUrl}?userId=${this.userId}`;
-      console.log('🔌 准备连接 WebSocket:', wsUrl);
-      console.log('   当前用户ID:', this.userId);
-      console.log('   WebSocket URL:', wsUrl);
+      if (DEBUG_WS) {
+        console.log('🔌 准备连接 WebSocket:', wsUrl);
+        console.log('   当前用户ID:', this.userId);
+        console.log('   WebSocket URL:', wsUrl);
+      }
       
       // 先关闭旧连接
       if (this.ws) {
@@ -74,7 +81,7 @@ class TIMPresenceManager {
       this.ws = uni.connectSocket({
         url: wsUrl,
         success: () => {
-          console.log('✅ WebSocket连接请求已发送成功');
+          if (DEBUG_WS) console.log('✅ WebSocket连接请求已发送成功');
         },
         fail: (err) => {
           console.error('❌ WebSocket连接请求发送失败:', err);
@@ -85,11 +92,13 @@ class TIMPresenceManager {
       
       // 使用 SocketTask 对象的监听器(推荐方式)
       this.ws.onOpen((res) => {
-        console.log('🎉 ========== WebSocket 连接成功 ==========');
-        console.log('   响应数据:', res);
-        console.log('   用户ID:', this.userId);
-        console.log('   连接URL:', wsUrl);
-        console.log('==========================================');
+        if (DEBUG_WS) {
+          console.log('🎉 ========== WebSocket 连接成功 ==========');
+          console.log('   响应数据:', res);
+          console.log('   用户ID:', this.userId);
+          console.log('   连接URL:', wsUrl);
+          console.log('==========================================');
+        }
         
         this.isConnected = true;
         this.reconnectCount = 0; // 重置重连计数
@@ -111,7 +120,7 @@ class TIMPresenceManager {
       
       // 监听消息
       this.ws.onMessage((res) => {
-        console.log('📨 收到WebSocket消息:', res.data);
+        if (DEBUG_WS) console.log('📨 收到WebSocket消息:', res.data);
         this.handleMessage(res.data);
       });
       
@@ -136,12 +145,14 @@ class TIMPresenceManager {
       
       // 监听关闭
       this.ws.onClose((res) => {
-        console.log('🔌 ========== WebSocket 连接关闭 ==========');
-        console.log('   关闭信息:', res);
-        console.log('   关闭码:', res?.code);
-        console.log('   关闭原因:', res?.reason);
-        console.log('   用户ID:', this.userId);
-        console.log('=========================================');
+        if (DEBUG_WS) {
+          console.log('🔌 ========== WebSocket 连接关闭 ==========');
+          console.log('   关闭信息:', res);
+          console.log('   关闭码:', res?.code);
+          console.log('   关闭原因:', res?.reason);
+          console.log('   用户ID:', this.userId);
+          console.log('=========================================');
+        }
         
         this.isConnected = false;
         this.stopHeartbeat();
@@ -173,7 +184,7 @@ class TIMPresenceManager {
     
     // 监听 IM 连接状态变化
     this.timConnectListener = (event) => {
-      console.log('📡 TIM 连接状态变更:', event.data.state);
+      if (DEBUG_WS) console.log('📡 TIM 连接状态变更:', event.data.state);
       
       let imStatus = 'offline';
       
@@ -193,7 +204,7 @@ class TIMPresenceManager {
     };
     
     timManager.tim.on(TIM.EVENT.NET_STATE_CHANGE, this.timConnectListener);
-    console.log('✅ 已监听 TIM 连接状态变更');
+    if (DEBUG_WS) console.log('✅ 已监听 TIM 连接状态变更');
   }
   
   /**

+ 17 - 18
LiangZhiYUMao/utils/websocket.js

@@ -3,13 +3,10 @@
  * 实现了心跳机制、断线重连、消息队列等功能
  */
 
+const DEBUG_WS = false;
+
 class WebSocketManager {
   constructor() {
-    this.socket = null;
-    this.isConnected = false;
-    this.userId = null;
-    this.url = '';
-    
     // 心跳相关
     this.heartbeatTimer = null;
     this.heartbeatInterval = 30000; // 30秒
@@ -38,19 +35,19 @@ class WebSocketManager {
    */
   connect(userId, baseUrl = 'ws://localhost:8083') {
     if (this.isConnected) {
-      console.log('WebSocket已连接');
+      if (DEBUG_WS) console.log('WebSocket已连接');
       return;
     }
 
     this.userId = userId;
     this.url = `${baseUrl}/ws/chat?userId=${userId}`;
     
-    console.log('正在连接WebSocket...', this.url);
+    if (DEBUG_WS) console.log('正在连接WebSocket...', this.url);
 
     this.socket = uni.connectSocket({
       url: this.url,
       success: () => {
-        console.log('WebSocket连接请求已发送');
+        if (DEBUG_WS) console.log('WebSocket连接请求已发送');
       },
       fail: (err) => {
         console.error('WebSocket连接失败', err);
@@ -60,7 +57,7 @@ class WebSocketManager {
 
     // 监听连接打开
     uni.onSocketOpen((res) => {
-      console.log('WebSocket连接成功');
+      if (DEBUG_WS) console.log('WebSocket连接成功');
       this.isConnected = true;
       this.reconnectCount = 0;
       
@@ -79,9 +76,9 @@ class WebSocketManager {
     // 监听消息接收
     uni.onSocketMessage((res) => {
       try {
-        console.log('🔵 收到原始WebSocket消息:', res.data);
+        if (DEBUG_WS) console.log('🔵 收到原始WebSocket消息:', res.data);
         const message = JSON.parse(res.data);
-        console.log('🔵 解析后的消息对象:', JSON.stringify(message, null, 2));
+        if (DEBUG_WS) console.log('🔵 解析后的消息对象:', JSON.stringify(message, null, 2));
         this.handleMessage(message);
       } catch (e) {
         console.error('❌ 解析消息失败', e);
@@ -91,7 +88,7 @@ class WebSocketManager {
 
     // 监听连接关闭
     uni.onSocketClose((res) => {
-      console.log('WebSocket连接关闭', res);
+      if (DEBUG_WS) console.log('WebSocket连接关闭', res);
       this.isConnected = false;
       this.stopHeartbeat();
       
@@ -116,7 +113,7 @@ class WebSocketManager {
    * 处理接收到的消息
    */
   handleMessage(message) {
-    console.log('收到消息', message);
+    if (DEBUG_WS) console.log('收到消息', message);
 
     switch (message.type) {
       case 'pong':
@@ -135,9 +132,11 @@ class WebSocketManager {
         
       case 'ack':
         // 消息确认
-        console.log('✅ 收到ACK消息!');
-        console.log('   messageId:', message.messageId);
-        console.log('   完整ACK消息:', JSON.stringify(message));
+        if (DEBUG_WS) {
+          console.log('✅ 收到ACK消息!');
+          console.log('   messageId:', message.messageId);
+          console.log('   完整ACK消息:', JSON.stringify(message));
+        }
         if (this.onMessageCallback) {
           console.log('   正在调用onMessageCallback...');
           this.onMessageCallback(message);
@@ -169,7 +168,7 @@ class WebSocketManager {
         
       case 'online':
         // 上线通知
-        console.log('用户上线', message.fromUserId);
+        if (DEBUG_WS) console.log('用户上线', message.fromUserId);
         break;
         
       case 'error':
@@ -182,7 +181,7 @@ class WebSocketManager {
         break;
         
       default:
-        console.log('未知消息类型', message.type);
+        if (DEBUG_WS) console.log('未知消息类型', message.type);
     }
   }