message.vue 29 KB

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