message.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023
  1. <template>
  2. <view class="matchmaker-message">
  3. <!-- 顶部导航栏 -->
  4. <view class="header">
  5. <view class="back-btn" @click="goBack"></view>
  6. <text class="header-title">消息</text>
  7. <view class="placeholder"></view>
  8. </view>
  9. <scroll-view scroll-y class="content">
  10. <!-- 加载中 -->
  11. <view v-if="loading" class="loading-container">
  12. <text>加载中...</text>
  13. </view>
  14. <!-- 消息内容 -->
  15. <view v-else-if="conversationList.length > 0">
  16. <!-- 系统通知卡片 -->
  17. <view class="message-item system-notification" v-if="systemNotification" @click="openSystemMessages">
  18. <text class="message-type">{{ systemNotification.title }}</text>
  19. <text class="message-time">{{ systemNotification.timeText }}</text>
  20. <text class="message-content">{{ systemNotification.content }}</text>
  21. <text class="message-footer">{{ systemNotification.footer }}</text>
  22. </view>
  23. <!-- 撮合成功通知卡片 -->
  24. <view class="message-item match-success" v-if="matchSuccessNotification" @click="openAuditRecords">
  25. <view class="message-icon heart"></view>
  26. <view class="message-body">
  27. <text class="message-type">{{ matchSuccessNotification.title }}</text>
  28. <text class="message-content">{{ matchSuccessNotification.content }}</text>
  29. </view>
  30. <view class="message-right-area">
  31. <text class="message-time">{{ matchSuccessNotification.timeText }}</text>
  32. <view v-if="matchSuccessUnread > 0" class="match-unread-badge">
  33. {{ matchSuccessUnread > 99 ? '99+' : matchSuccessUnread }}
  34. </view>
  35. </view>
  36. </view>
  37. <!-- 今天 -->
  38. <view v-if="todayConversations.length > 0">
  39. <view class="time-group">
  40. <text class="time-label">今天</text>
  41. </view>
  42. <view
  43. v-for="conversation in todayConversations"
  44. :key="conversation.conversationID"
  45. class="message-item user-message"
  46. @click="openChat(conversation)"
  47. >
  48. <image
  49. v-if="conversation.userProfile.avatar"
  50. :src="conversation.userProfile.avatar"
  51. class="message-avatar-img"
  52. />
  53. <view v-else class="message-avatar">
  54. {{ conversation.userProfile.nick || conversation.userProfile.userID.charAt(0) }}
  55. </view>
  56. <view class="message-body">
  57. <text class="message-type">{{ conversation.userProfile.nick || conversation.userProfile.userID }}</text>
  58. <text class="message-content">{{ getLastMessageText(conversation.lastMessage) }}</text>
  59. </view>
  60. <view class="message-right">
  61. <text class="message-time">{{ formatTime(conversation.lastMessage.lastTime) }}</text>
  62. <view v-if="conversation.unreadCount > 0" class="unread-badge">
  63. {{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
  64. </view>
  65. </view>
  66. </view>
  67. </view>
  68. <!-- 昨日 -->
  69. <view v-if="yesterdayConversations.length > 0">
  70. <view class="time-group">
  71. <text class="time-label">昨日</text>
  72. </view>
  73. <view
  74. v-for="conversation in yesterdayConversations"
  75. :key="conversation.conversationID"
  76. class="message-item user-message"
  77. @click="openChat(conversation)"
  78. >
  79. <image
  80. v-if="conversation.userProfile.avatar"
  81. :src="conversation.userProfile.avatar"
  82. class="message-avatar-img"
  83. />
  84. <view v-else class="message-avatar">
  85. {{ conversation.userProfile.nick || conversation.userProfile.userID.charAt(0) }}
  86. </view>
  87. <view class="message-body">
  88. <text class="message-type">{{ conversation.userProfile.nick || conversation.userProfile.userID }}</text>
  89. <text class="message-content">{{ getLastMessageText(conversation.lastMessage) }}</text>
  90. </view>
  91. <view class="message-right">
  92. <text class="message-time">{{ formatTime(conversation.lastMessage.lastTime) }}</text>
  93. <view v-if="conversation.unreadCount > 0" class="unread-badge">
  94. {{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
  95. </view>
  96. </view>
  97. </view>
  98. </view>
  99. <!-- 更早 -->
  100. <view v-if="earlierConversations.length > 0">
  101. <view class="time-group">
  102. <text class="time-label">更早</text>
  103. </view>
  104. <view
  105. v-for="conversation in earlierConversations"
  106. :key="conversation.conversationID"
  107. class="message-item user-message"
  108. @click="openChat(conversation)"
  109. >
  110. <image
  111. v-if="conversation.userProfile.avatar"
  112. :src="conversation.userProfile.avatar"
  113. class="message-avatar-img"
  114. />
  115. <view v-else class="message-avatar">
  116. {{ conversation.userProfile.nick || conversation.userProfile.userID.charAt(0) }}
  117. </view>
  118. <view class="message-body">
  119. <text class="message-type">{{ conversation.userProfile.nick || conversation.userProfile.userID }}</text>
  120. <text class="message-content">{{ getLastMessageText(conversation.lastMessage) }}</text>
  121. </view>
  122. <view class="message-right">
  123. <text class="message-time">{{ formatTime(conversation.lastMessage.lastTime) }}</text>
  124. <view v-if="conversation.unreadCount > 0" class="unread-badge">
  125. {{ conversation.unreadCount > 99 ? '99+' : conversation.unreadCount }}
  126. </view>
  127. </view>
  128. </view>
  129. </view>
  130. </view>
  131. <!-- 空状态 -->
  132. <view v-else class="empty-container">
  133. <text>暂无消息</text>
  134. </view>
  135. </scroll-view>
  136. <!-- 底部导航 -->
  137. <view class="tabbar">
  138. <view class="tabbar-item home" @click="navigateToWorkbench">
  139. <view class="tabbar-icon"></view>
  140. <text class="tabbar-text">工作台</text>
  141. </view>
  142. <view class="tabbar-item resources" @click="navigateToMyResources">
  143. <view class="tabbar-icon"></view>
  144. <text class="tabbar-text">我的资源</text>
  145. </view>
  146. <view class="tabbar-item trophy" @click="navigateToRanking">
  147. <view class="tabbar-icon"></view>
  148. <text class="tabbar-text">排行榜</text>
  149. </view>
  150. <view class="tabbar-item message active" @click="navigateToMessage">
  151. <view class="tabbar-icon">
  152. <view v-if="totalUnreadCount > 0" class="badge">
  153. {{ totalUnreadCount > 99 ? '99+' : totalUnreadCount }}
  154. </view>
  155. </view>
  156. <text class="tabbar-text">消息</text>
  157. </view>
  158. <view class="tabbar-item mine" @click="navigateToMine">
  159. <view class="tabbar-icon"></view>
  160. <text class="tabbar-text">我的</text>
  161. </view>
  162. </view>
  163. </view>
  164. </template>
  165. <script>
  166. import timManager from '@/utils/tim-manager.js'
  167. import TIM from 'tim-wx-sdk'
  168. import api from '@/utils/api.js'
  169. export default {
  170. data() {
  171. return {
  172. loading: true,
  173. conversationList: [],
  174. matchmakerInfo: null,
  175. imUserId: '',
  176. totalUnreadCount: 0,
  177. // 系统消息未读数
  178. systemUnread: 0,
  179. // 撮合成功通知未读数
  180. matchSuccessUnread: 0,
  181. // 红娘ID
  182. matchmakerId: null,
  183. // 顶部系统通知占位(后续可从后端加载)
  184. systemNotification: {
  185. title: '系统通知',
  186. content: '您的线索审核已通过,获得20积分奖励,当期积分可兑换【资源查看权限】x1',
  187. footer: '完成线索录采集,积分+20(距离黄金级还差72分)',
  188. timeText: '刚刚'
  189. },
  190. // 撮合成功通知占位
  191. matchSuccessNotification: {
  192. title: '撮合成功通知',
  193. content: '您推荐的李先生和王女士已成功匹配,获得50积分+100元现金奖励',
  194. timeText: '10分钟前'
  195. }
  196. }
  197. },
  198. computed: {
  199. // 今天的会话
  200. todayConversations() {
  201. const today = new Date()
  202. return this.conversationList.filter(conv => this.isSameDay(conv.lastMessage, today))
  203. },
  204. // 昨日会话
  205. yesterdayConversations() {
  206. const yesterday = new Date()
  207. yesterday.setDate(yesterday.getDate() - 1)
  208. return this.conversationList.filter(conv => this.isSameDay(conv.lastMessage, yesterday))
  209. },
  210. // 更早会话
  211. earlierConversations() {
  212. const today = new Date()
  213. const yesterday = new Date()
  214. yesterday.setDate(yesterday.getDate() - 1)
  215. return this.conversationList.filter(conv => {
  216. const msg = conv.lastMessage
  217. if (!msg || !msg.lastTime) return false
  218. const isToday = this.isSameDay(msg, today)
  219. const isYesterday = this.isSameDay(msg, yesterday)
  220. return !isToday && !isYesterday
  221. })
  222. }
  223. },
  224. onLoad() {
  225. // 为避免误用用户端 IM 账号,这里不再复用当前 TIM 登录状态,进入页面即按红娘流程重新初始化
  226. console.log('🔍 红娘消息页强制按红娘流程初始化 IM')
  227. this.initIM()
  228. },
  229. onShow() {
  230. // 页面显示时刷新会话列表
  231. if (this.imUserId) {
  232. this.loadConversationList()
  233. }
  234. // 同步系统消息未读
  235. this.loadSystemUnread()
  236. // 同步撮合成功通知未读
  237. this.loadMatchSuccessUnread()
  238. },
  239. onUnload() {
  240. // 页面卸载时移除监听
  241. this.removeListeners()
  242. },
  243. methods: {
  244. // 判断消息是否与给定日期同一天
  245. isSameDay(lastMessage, baseDate) {
  246. if (!lastMessage || !lastMessage.lastTime) return false
  247. const msgDate = new Date(lastMessage.lastTime * 1000)
  248. const d = baseDate instanceof Date ? baseDate : new Date(baseDate)
  249. return (
  250. msgDate.getFullYear() === d.getFullYear() &&
  251. msgDate.getMonth() === d.getMonth() &&
  252. msgDate.getDate() === d.getDate()
  253. )
  254. },
  255. // 加载系统消息未读数,并同步最新一条系统通知到顶部卡片
  256. async loadSystemUnread() {
  257. try {
  258. const userId = uni.getStorageSync('userId')
  259. if (!userId) return
  260. const count = await api.message.getSystemUnreadCount(userId)
  261. // getSystemUnreadCount 可能返回 {code,data} 或直接 number,这里统一兼容
  262. if (typeof count === 'number') {
  263. this.systemUnread = count
  264. } else if (count && typeof count.data === 'number') {
  265. this.systemUnread = count.data
  266. }
  267. // 同时拉取最新一条系统通知,用于顶部卡片展示
  268. const res = await api.message.getSystemList(userId, 1, 1)
  269. const list = (res && (res.list || res.data?.list)) || []
  270. if (list.length > 0) {
  271. const item = list[0]
  272. // 计算时间文案
  273. let timeText = ''
  274. const t = item.createdAt || item.created_at
  275. if (t) {
  276. const d2 = new Date(t)
  277. const now = new Date()
  278. const diff = now - d2
  279. if (diff < 60000) {
  280. timeText = '刚刚'
  281. } else if (diff < 3600000) {
  282. timeText = Math.floor(diff / 60000) + '分钟前'
  283. } else {
  284. const mm = String(d2.getMonth() + 1).padStart(2, '0')
  285. const dd = String(d2.getDate()).padStart(2, '0')
  286. const hh = String(d2.getHours()).padStart(2, '0')
  287. const mi = String(d2.getMinutes()).padStart(2, '0')
  288. timeText = `${mm}-${dd} ${hh}:${mi}`
  289. }
  290. }
  291. // 用最新系统通知更新卡片文案(小字显示最新内容)
  292. this.systemNotification = {
  293. ...this.systemNotification,
  294. title: '系统通知',
  295. content: item.title || this.systemNotification.content,
  296. footer: item.content || this.systemNotification.footer,
  297. timeText: timeText || this.systemNotification.timeText
  298. }
  299. }
  300. } catch (e) {
  301. console.log('红娘消息页加载系统未读失败', e)
  302. }
  303. },
  304. // 跳转系统通知列表
  305. openSystemMessages() {
  306. uni.navigateTo({
  307. url: '/pages/matchmaker-workbench/system-messages'
  308. })
  309. },
  310. // 跳转审核记录列表
  311. openAuditRecords() {
  312. uni.navigateTo({
  313. url: '/pages/matchmaker-workbench/audit-records'
  314. })
  315. },
  316. // 加载撮合成功通知未读数
  317. async loadMatchSuccessUnread() {
  318. try {
  319. if (!this.matchmakerId) return
  320. const res = await api.successCaseUpload.getUnreadCount(this.matchmakerId)
  321. if (res && res.code === 200) {
  322. this.matchSuccessUnread = res.data || 0
  323. }
  324. } catch (e) {
  325. console.log('加载撮合成功通知未读数失败', e)
  326. }
  327. },
  328. // 初始化 IM
  329. async initIM() {
  330. try {
  331. this.loading = true
  332. // 1. 获取当前登录用户ID
  333. const userId = uni.getStorageSync('userId')
  334. if (!userId) {
  335. uni.showToast({ title: '请先登录', icon: 'none' })
  336. return
  337. }
  338. // 2. 获取红娘信息
  339. const res = await uni.request({
  340. url: 'http://localhost:8081/api/matchmaker/current',
  341. method: 'GET',
  342. data: { userId }
  343. })
  344. if (res[1].data.code !== 200) {
  345. uni.showToast({ title: '获取红娘信息失败', icon: 'none' })
  346. return
  347. }
  348. this.matchmakerInfo = res[1].data.data
  349. this.imUserId = this.matchmakerInfo.imUserId // m_1
  350. this.matchmakerId = this.matchmakerInfo.matchmakerId || this.matchmakerInfo.matchmaker_id
  351. console.log('✅ 红娘信息:', this.matchmakerInfo)
  352. // 加载撮合成功通知未读数
  353. this.loadMatchSuccessUnread()
  354. // 3. 获取 UserSig
  355. const sigRes = await uni.request({
  356. url: `http://localhost:8083/api/im/getUserSig?userId=${this.imUserId}`,
  357. method: 'GET'
  358. })
  359. if (sigRes[1].data.code !== 200) {
  360. uni.showToast({ title: '获取UserSig失败', icon: 'none' })
  361. return
  362. }
  363. const userSig = sigRes[1].data.data.userSig
  364. console.log('✅ 获取到 UserSig:', userSig ? '成功' : '失败')
  365. // 4. 先登出当前账号(如果已登录)
  366. try {
  367. await timManager.logout()
  368. console.log('✅ 已登出之前的账号')
  369. } catch (e) {
  370. console.log('⚠️ 登出失败或未登录:', e.message)
  371. }
  372. // 5. 以红娘身份登录 IM
  373. await timManager.login(this.imUserId, userSig)
  374. console.log('✅ 红娘已登录 IM:', this.imUserId)
  375. // 6. 监听事件
  376. this.addListeners()
  377. // 7. 加载会话列表
  378. await this.loadConversationList()
  379. } catch (error) {
  380. console.error('❌ 初始化 IM 失败:', error)
  381. uni.showToast({ title: '初始化失败', icon: 'none' })
  382. } finally {
  383. this.loading = false
  384. }
  385. },
  386. // 加载会话列表
  387. async loadConversationList() {
  388. try {
  389. // SDK 未登录 / 未 ready 时不调用会话列表接口,避免报“接口调用时机不合理”
  390. if (!timManager.isLogin || !timManager.tim) {
  391. console.log('⚠️ TIM 未 ready,暂不加载会话列表')
  392. return
  393. }
  394. const tim = timManager.getTim()
  395. const { data } = await tim.getConversationList()
  396. this.conversationList = data.conversationList || []
  397. // 补全用户头像和昵称
  398. await this.loadUserAvatars()
  399. // 计算总未读数
  400. this.totalUnreadCount = this.conversationList.reduce((total, conv) => {
  401. return total + (conv.unreadCount || 0)
  402. }, 0)
  403. console.log('✅ 会话列表:', this.conversationList)
  404. } catch (error) {
  405. console.error('❌ 加载会话列表失败:', error)
  406. }
  407. },
  408. // 添加事件监听
  409. addListeners() {
  410. const tim = timManager.getTim()
  411. // 监听新消息
  412. tim.on(TIM.EVENT.MESSAGE_RECEIVED, this.onMessageReceived)
  413. // 监听会话列表更新
  414. tim.on(TIM.EVENT.CONVERSATION_LIST_UPDATED, this.onConversationListUpdated)
  415. },
  416. // 移除事件监听
  417. removeListeners() {
  418. const tim = timManager.getTim()
  419. tim.off(TIM.EVENT.MESSAGE_RECEIVED, this.onMessageReceived)
  420. tim.off(TIM.EVENT.CONVERSATION_LIST_UPDATED, this.onConversationListUpdated)
  421. },
  422. // 收到新消息
  423. onMessageReceived(event) {
  424. console.log('📩 收到新消息:', event.data)
  425. // 刷新会话列表
  426. this.loadConversationList()
  427. },
  428. // 会话列表更新
  429. onConversationListUpdated(event) {
  430. console.log('🔄 会话列表更新:', event.data)
  431. this.conversationList = event.data || []
  432. // 补全头像昵称后再计算未读
  433. this.loadUserAvatars().then(() => {
  434. this.totalUnreadCount = this.conversationList.reduce((total, conv) => {
  435. return total + (conv.unreadCount || 0)
  436. }, 0)
  437. })
  438. },
  439. // 批量补全用户头像和昵称
  440. async loadUserAvatars() {
  441. try {
  442. if (!this.conversationList || this.conversationList.length === 0) {
  443. return
  444. }
  445. // 收集对端用户ID(排除红娘 m_ 开头的ID)
  446. const userIds = []
  447. this.conversationList.forEach(conv => {
  448. let targetUserId = ''
  449. if (conv.userProfile && conv.userProfile.userID) {
  450. targetUserId = String(conv.userProfile.userID)
  451. } else if (conv.conversationID && conv.conversationID.indexOf('C2C') === 0) {
  452. targetUserId = conv.conversationID.replace('C2C', '')
  453. }
  454. if (!targetUserId) return
  455. if (targetUserId.startsWith('m_')) return
  456. userIds.push(targetUserId)
  457. })
  458. const uniqueIds = Array.from(new Set(userIds))
  459. if (uniqueIds.length === 0) return
  460. console.log('🔄 红娘消息页批量获取用户信息,数量:', uniqueIds.length)
  461. const res = await uni.request({
  462. url: 'http://localhost:8083/api/user/batch',
  463. method: 'GET',
  464. data: {
  465. userIds: uniqueIds.join(',')
  466. }
  467. })
  468. if (!res[1] || res[1].statusCode !== 200 || res[1].data.code !== 200) {
  469. console.warn('⚠️ 红娘消息页批量获取用户信息失败')
  470. return
  471. }
  472. const list = res[1].data.data || []
  473. const userMap = {}
  474. list.forEach(user => {
  475. if (!user || user.userId == null) return
  476. userMap[String(user.userId)] = {
  477. nickname: user.nickname,
  478. avatarUrl: user.avatarUrl
  479. }
  480. })
  481. // 回填到会话列表的 userProfile 中
  482. this.conversationList = this.conversationList.map(conv => {
  483. let targetUserId = ''
  484. if (conv.userProfile && conv.userProfile.userID) {
  485. targetUserId = String(conv.userProfile.userID)
  486. } else if (conv.conversationID && conv.conversationID.indexOf('C2C') === 0) {
  487. targetUserId = conv.conversationID.replace('C2C', '')
  488. }
  489. const info = targetUserId ? userMap[targetUserId] : null
  490. if (info) {
  491. conv.userProfile = conv.userProfile || {}
  492. conv.userProfile.nick = info.nickname || conv.userProfile.nick || `用户${targetUserId}`
  493. conv.userProfile.avatar = info.avatarUrl || conv.userProfile.avatar || ''
  494. }
  495. return conv
  496. })
  497. } catch (error) {
  498. console.error('❌ 红娘消息页加载用户头像失败:', error)
  499. }
  500. },
  501. // 打开聊天页面
  502. openChat(conversation) {
  503. const targetUserId = conversation.userProfile.userID
  504. const targetUserName = encodeURIComponent(conversation.userProfile.nick || `用户${targetUserId}`)
  505. const targetUserAvatar = encodeURIComponent(conversation.userProfile.avatar || '')
  506. uni.navigateTo({
  507. url: `/pages/message/chat?targetUserId=${targetUserId}&targetUserName=${targetUserName}&targetUserAvatar=${targetUserAvatar}&fromMatchmaker=true`
  508. })
  509. },
  510. // 获取最后一条消息的文本
  511. getLastMessageText(lastMessage) {
  512. if (!lastMessage) return ''
  513. switch (lastMessage.type) {
  514. case TIM.TYPES.MSG_TEXT:
  515. return lastMessage.payload.text
  516. case TIM.TYPES.MSG_IMAGE:
  517. return '[图片]'
  518. case TIM.TYPES.MSG_AUDIO:
  519. return '[语音]'
  520. case TIM.TYPES.MSG_VIDEO:
  521. return '[视频]'
  522. case TIM.TYPES.MSG_FILE:
  523. return '[文件]'
  524. default:
  525. return '[消息]'
  526. }
  527. },
  528. // 格式化时间
  529. formatTime(timestamp) {
  530. if (!timestamp) return ''
  531. const now = new Date()
  532. const msgTime = new Date(timestamp * 1000)
  533. const diff = now - msgTime
  534. // 一分钟内
  535. if (diff < 60000) {
  536. return '刚刚'
  537. }
  538. // 一小时内
  539. if (diff < 3600000) {
  540. return Math.floor(diff / 60000) + '分钟前'
  541. }
  542. // 今天
  543. if (msgTime.toDateString() === now.toDateString()) {
  544. return msgTime.getHours() + ':' + String(msgTime.getMinutes()).padStart(2, '0')
  545. }
  546. // 昨天
  547. const yesterday = new Date(now)
  548. yesterday.setDate(yesterday.getDate() - 1)
  549. if (msgTime.toDateString() === yesterday.toDateString()) {
  550. return '昨天'
  551. }
  552. // 更早
  553. return (msgTime.getMonth() + 1) + '-' + msgTime.getDate()
  554. },
  555. // 返回上一页
  556. goBack() {
  557. uni.navigateBack()
  558. },
  559. // 导航到工作台
  560. navigateToWorkbench() {
  561. uni.redirectTo({
  562. url: '/pages/matchmaker-workbench/index'
  563. })
  564. },
  565. // 导航到我的资源
  566. navigateToMyResources() {
  567. uni.redirectTo({
  568. url: '/pages/matchmaker-workbench/my-resources'
  569. })
  570. },
  571. // 导航到排行榜
  572. navigateToRanking() {
  573. uni.redirectTo({
  574. url: '/pages/matchmaker-workbench/ranking'
  575. })
  576. },
  577. // 导航到消息
  578. navigateToMessage() {
  579. // 已在消息页面,无需跳转
  580. },
  581. // 导航到我的
  582. navigateToMine() {
  583. uni.redirectTo({
  584. url: '/pages/matchmaker-workbench/mine'
  585. })
  586. }
  587. }
  588. }
  589. </script>
  590. <style lang="scss" scoped>
  591. .matchmaker-message {
  592. min-height: 100vh;
  593. /* 顶部淡粉,中部淡紫,越到底部越接近白色 */
  594. background: linear-gradient(180deg, #fff1f7 0%, #f6ebff 45%, #fbf7ff 75%, #ffffff 100%);
  595. display: flex;
  596. flex-direction: column;
  597. }
  598. /* 顶部导航栏 */
  599. .header {
  600. display: flex;
  601. align-items: center;
  602. justify-content: space-between;
  603. padding: 25rpx 30rpx;
  604. padding-top: calc(25rpx + env(safe-area-inset-top));
  605. background: transparent;
  606. border-bottom-width: 0;
  607. .back-btn {
  608. width: 70rpx;
  609. height: 70rpx;
  610. display: flex;
  611. align-items: center;
  612. justify-content: center;
  613. background: rgba(240, 240, 240, 0.5);
  614. border-radius: 50%;
  615. background-size: 40rpx 40rpx;
  616. background-repeat: no-repeat;
  617. background-position: center;
  618. }
  619. .header-title {
  620. font-size: 38rpx;
  621. font-weight: bold;
  622. color: #333;
  623. }
  624. .placeholder {
  625. width: 70rpx;
  626. }
  627. }
  628. .content {
  629. flex: 1;
  630. padding: 20rpx 24rpx 140rpx;
  631. box-sizing: border-box;
  632. }
  633. /* 消息项 */
  634. .message-item {
  635. background: #ffffff;
  636. border-radius: 24rpx;
  637. padding: 28rpx;
  638. margin-bottom: 20rpx;
  639. box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.05);
  640. position: relative;
  641. &.system-notification {
  642. background: #ffffff;
  643. border-radius: 28rpx;
  644. border: 2rpx solid rgba(255, 128, 171, 0.85);
  645. box-shadow: 0 10rpx 26rpx rgba(255, 128, 171, 0.28);
  646. padding: 26rpx 30rpx;
  647. .message-type {
  648. display: block;
  649. font-size: 32rpx;
  650. font-weight: 600;
  651. color: #333;
  652. margin-bottom: 12rpx;
  653. }
  654. .message-time {
  655. position: absolute;
  656. top: 26rpx;
  657. right: 30rpx;
  658. font-size: 24rpx;
  659. color: #b88bb0;
  660. }
  661. .message-content {
  662. display: block;
  663. font-size: 26rpx;
  664. color: #555;
  665. line-height: 1.6;
  666. margin-bottom: 12rpx;
  667. }
  668. .message-footer {
  669. display: block;
  670. font-size: 24rpx;
  671. color: #b27aa0;
  672. line-height: 1.4;
  673. }
  674. }
  675. &.match-success {
  676. display: flex;
  677. background: linear-gradient(135deg, #f8f3ff 0%, #f4e6ff 45%, #ffeaf7 100%);
  678. border: 0;
  679. box-shadow: 0 8rpx 20rpx rgba(186, 104, 200, 0.25);
  680. .message-icon {
  681. width: 60rpx;
  682. height: 60rpx;
  683. border-radius: 50%;
  684. margin-right: 20rpx;
  685. background-size: 40rpx 40rpx;
  686. background-repeat: no-repeat;
  687. background-position: center;
  688. }
  689. .message-icon.heart {
  690. background-color: #f8d9ef;
  691. background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23e573ab"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>');
  692. }
  693. .message-body {
  694. flex: 1;
  695. }
  696. .message-type {
  697. display: block;
  698. font-size: 30rpx;
  699. font-weight: 600;
  700. color: #333;
  701. margin-bottom: 10rpx;
  702. }
  703. .message-content {
  704. display: block;
  705. font-size: 26rpx;
  706. color: #666;
  707. line-height: 1.4;
  708. }
  709. .message-right-area {
  710. display: flex;
  711. flex-direction: column;
  712. align-items: flex-end;
  713. margin-left: 20rpx;
  714. }
  715. .message-time {
  716. font-size: 24rpx;
  717. color: #a691c0;
  718. margin-bottom: 10rpx;
  719. }
  720. .match-unread-badge {
  721. background: #ff5c9a;
  722. color: #FFFFFF;
  723. font-size: 20rpx;
  724. padding: 4rpx 12rpx;
  725. border-radius: 20rpx;
  726. min-width: 36rpx;
  727. text-align: center;
  728. }
  729. }
  730. &.user-message {
  731. display: flex;
  732. background: #ffffff;
  733. border: 0;
  734. box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.04);
  735. align-items: center;
  736. .message-avatar {
  737. width: 60rpx;
  738. height: 60rpx;
  739. border-radius: 50%;
  740. background: linear-gradient(135deg, #b39ddb 0%, #ce93d8 100%);
  741. color: #FFFFFF;
  742. font-size: 28rpx;
  743. font-weight: bold;
  744. display: flex;
  745. align-items: center;
  746. justify-content: center;
  747. margin-right: 20rpx;
  748. flex-shrink: 0;
  749. }
  750. .message-avatar-img {
  751. width: 60rpx;
  752. height: 60rpx;
  753. border-radius: 50%;
  754. margin-right: 20rpx;
  755. flex-shrink: 0;
  756. }
  757. .message-body {
  758. flex: 1;
  759. min-width: 0;
  760. }
  761. .message-type {
  762. display: block;
  763. font-size: 30rpx;
  764. font-weight: 500;
  765. color: #333;
  766. margin-bottom: 6rpx;
  767. }
  768. .message-content {
  769. display: block;
  770. font-size: 26rpx;
  771. color: #777;
  772. line-height: 1.4;
  773. overflow: hidden;
  774. text-overflow: ellipsis;
  775. white-space: nowrap;
  776. }
  777. .message-right {
  778. display: flex;
  779. flex-direction: column;
  780. align-items: flex-end;
  781. margin-left: 20rpx;
  782. }
  783. .message-time {
  784. font-size: 24rpx;
  785. color: #b0a6c5;
  786. margin-bottom: 10rpx;
  787. }
  788. .unread-badge {
  789. background: #ff5c9a;
  790. color: #FFFFFF;
  791. font-size: 20rpx;
  792. padding: 4rpx 10rpx;
  793. border-radius: 20rpx;
  794. min-width: 36rpx;
  795. text-align: center;
  796. }
  797. }
  798. }
  799. /* 加载和空状态 */
  800. .loading-container,
  801. .empty-container {
  802. display: flex;
  803. justify-content: center;
  804. align-items: center;
  805. padding: 100rpx 0;
  806. color: #999;
  807. font-size: 28rpx;
  808. }
  809. /* 时间分组 */
  810. .time-group {
  811. display: flex;
  812. justify-content: center;
  813. margin: 20rpx 0;
  814. .time-label {
  815. display: inline-block;
  816. background: #ffe6f0;
  817. color: #e573ab;
  818. font-size: 24rpx;
  819. padding: 8rpx 20rpx;
  820. border-radius: 20rpx;
  821. font-weight: bold;
  822. }
  823. }
  824. /* 底部导航 */
  825. .tabbar {
  826. position: fixed;
  827. bottom: 0;
  828. left: 0;
  829. right: 0;
  830. height: 100rpx;
  831. background: #FFFFFF;
  832. border-top: 1rpx solid #F0F0F0;
  833. display: flex;
  834. justify-content: space-around;
  835. align-items: center;
  836. padding-bottom: env(safe-area-inset-bottom);
  837. .tabbar-item {
  838. display: flex;
  839. flex-direction: column;
  840. align-items: center;
  841. gap: 8rpx;
  842. padding: 10rpx 0;
  843. .tabbar-icon {
  844. width: 44rpx;
  845. height: 44rpx;
  846. background-size: contain;
  847. background-repeat: no-repeat;
  848. background-position: center;
  849. position: relative;
  850. .badge {
  851. position: absolute;
  852. top: -8rpx;
  853. right: -8rpx;
  854. background: #FF4444;
  855. color: #FFFFFF;
  856. font-size: 20rpx;
  857. font-weight: bold;
  858. width: 32rpx;
  859. height: 32rpx;
  860. display: flex;
  861. align-items: center;
  862. justify-content: center;
  863. border-radius: 16rpx;
  864. }
  865. }
  866. .tabbar-text {
  867. font-size: 20rpx;
  868. color: #999;
  869. }
  870. &.active .tabbar-text {
  871. color: #9C27B0;
  872. font-weight: bold;
  873. }
  874. &.home .tabbar-icon {
  875. background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23999"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>');
  876. }
  877. &.home.active .tabbar-icon {
  878. background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%239C27B0"><path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/></svg>');
  879. }
  880. &.resources .tabbar-icon {
  881. background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23999"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>');
  882. }
  883. &.resources.active .tabbar-icon {
  884. background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%239C27B0"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>');
  885. }
  886. &.trophy .tabbar-icon {
  887. background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23999"><path d="M19 5h-2V3H7v2H5c-1.1 0-2 .9-2 2v1c0 2.55 1.92 4.63 4.39 4.94.63 1.5 1.98 2.63 3.61 2.96V19H7v2h10v-2h-4v-3.1c1.63-.33 2.98-1.46 3.61-2.96C19.08 12.63 21 10.55 21 8V7c0-1.1-.9-2-2-2zM5 8V7h2v3.82C5.84 10.4 5 9.3 5 8zm14 0c0 1.3-.84 2.4-2 2.82V7h2v1z"/></svg>');
  888. }
  889. &.trophy.active .tabbar-icon {
  890. background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%239C27B0"><path d="M19 5h-2V3H7v2H5c-1.1 0-2 .9-2 2v1c0 2.55 1.92 4.63 4.39 4.94.63 1.5 1.98 2.63 3.61 2.96V19H7v2h10v-2h-4v-3.1c1.63-.33 2.98-1.46 3.61-2.96C19.08 12.63 21 10.55 21 8V7c0-1.1-.9-2-2-2zM5 8V7h2v3.82C5.84 10.4 5 9.3 5 8zm14 0c0 1.3-.84 2.4-2 2.82V7h2v1z"/></svg>');
  891. }
  892. &.message .tabbar-icon {
  893. background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23999"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>');
  894. }
  895. &.message.active .tabbar-icon {
  896. background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%239C27B0"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>');
  897. }
  898. &.mine .tabbar-icon {
  899. background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%23999"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>');
  900. }
  901. &.mine.active .tabbar-icon {
  902. background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="%239C27B0"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>');
  903. }
  904. }
  905. }
  906. </style>