message-history.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. <template>
  2. <view class="container">
  3. <!-- 顶部导航栏 -->
  4. <view class="nav-bar">
  5. <view class="nav-back" @click="goBack">
  6. <text class="nav-back-icon">&#x2190;</text>
  7. </view>
  8. <text class="nav-title">消息历史</text>
  9. <view class="nav-placeholder"></view>
  10. </view>
  11. <!-- 筛选标签 -->
  12. <view class="filter-tabs">
  13. <view
  14. class="filter-tab"
  15. :class="{ active: activeFilter === 'all' }"
  16. @click="switchFilter('all')">
  17. <text>全部</text>
  18. </view>
  19. <view
  20. class="filter-tab"
  21. :class="{ active: activeFilter === 'energy' }"
  22. @click="switchFilter('energy')">
  23. <text>能量</text>
  24. </view>
  25. <view
  26. class="filter-tab"
  27. :class="{ active: activeFilter === 'points' }"
  28. @click="switchFilter('points')">
  29. <text>积分</text>
  30. </view>
  31. <view
  32. class="filter-tab"
  33. :class="{ active: activeFilter === 'cf' }"
  34. @click="switchFilter('cf')">
  35. <text>CF值</text>
  36. </view>
  37. <view
  38. class="filter-tab"
  39. :class="{ active: activeFilter === 'system' }"
  40. @click="switchFilter('system')">
  41. <text>系统</text>
  42. </view>
  43. </view>
  44. <!-- 消息列表 -->
  45. <scroll-view
  46. class="message-list"
  47. scroll-y
  48. @scrolltolower="loadMore">
  49. <view v-if="loading && messages.length === 0" class="loading-state">
  50. <text>加载中...</text>
  51. </view>
  52. <view v-else-if="messages.length === 0" class="empty-state">
  53. <text class="empty-icon">📭</text>
  54. <text class="empty-text">暂无消息</text>
  55. </view>
  56. <template v-else>
  57. <!-- 能量变更消息 -->
  58. <view
  59. v-for="(msg, index) in energyMessages"
  60. :key="msg.id"
  61. class="message-item"
  62. :class="{ unread: !msg.isRead }"
  63. @click="markAsRead(msg)">
  64. <view class="message-icon" :style="{ background: 'rgba(249,115,22,0.15)' }">
  65. <text>⚡</text>
  66. </view>
  67. <view class="message-content">
  68. <text class="message-title">{{ msg.fullText || msg.description }}</text>
  69. <text v-if="msg.amount !== 0" class="message-amount" :style="{ color: msg.amount >= 0 ? '#10B981' : '#EF4444' }">
  70. {{ msg.amount >= 0 ? '+' : '' }}{{ msg.amount }}
  71. </text>
  72. </view>
  73. <view class="message-meta">
  74. <text class="message-time">{{ msg.time }}</text>
  75. </view>
  76. </view>
  77. <!-- 积分变更消息 -->
  78. <view
  79. v-for="(msg, index) in pointsMessages"
  80. :key="msg.id"
  81. class="message-item"
  82. :class="{ unread: !msg.isRead }"
  83. @click="markAsRead(msg)">
  84. <view class="message-icon" :style="{ background: 'rgba(16,185,129,0.15)' }">
  85. <text>🪙</text>
  86. </view>
  87. <view class="message-content">
  88. <text class="message-title">{{ msg.fullText || msg.description }}</text>
  89. <text v-if="msg.amount !== 0" class="message-amount" :style="{ color: msg.amount >= 0 ? '#10B981' : '#EF4444' }">
  90. {{ msg.amount >= 0 ? '+' : '' }}{{ msg.amount }}
  91. </text>
  92. </view>
  93. <view class="message-meta">
  94. <text class="message-time">{{ msg.time }}</text>
  95. </view>
  96. </view>
  97. <!-- CF值变更消息 -->
  98. <view
  99. v-for="(msg, index) in cfMessages"
  100. :key="msg.id"
  101. class="message-item"
  102. :class="{ unread: !msg.isRead }"
  103. @click="markAsRead(msg)">
  104. <view class="message-icon" :style="{ background: 'rgba(99,102,241,0.15)' }">
  105. <text>💰</text>
  106. </view>
  107. <view class="message-content">
  108. <text class="message-title">{{ msg.fullText || msg.description }}</text>
  109. <text v-if="msg.amount !== 0" class="message-amount" :style="{ color: msg.amount >= 0 ? '#10B981' : '#EF4444' }">
  110. {{ msg.amount >= 0 ? '+' : '' }}{{ msg.amount }}
  111. </text>
  112. </view>
  113. <view class="message-meta">
  114. <text class="message-time">{{ msg.time }}</text>
  115. </view>
  116. </view>
  117. <!-- 系统通知 -->
  118. <view
  119. v-for="(msg, index) in systemMessages"
  120. :key="msg.id"
  121. class="message-item"
  122. :class="{ unread: !msg.isRead }"
  123. @click="markAsRead(msg)">
  124. <view class="message-icon" :style="{ background: 'rgba(100,116,139,0.15)' }">
  125. <text>📢</text>
  126. </view>
  127. <view class="message-content">
  128. <text class="message-title">{{ msg.title }}</text>
  129. <text class="message-desc">{{ msg.description }}</text>
  130. </view>
  131. <view class="message-meta">
  132. <text class="message-time">{{ msg.time }}</text>
  133. </view>
  134. </view>
  135. <!-- 加载更多 -->
  136. <view v-if="loadingMore" class="loading-more">
  137. <text>加载中...</text>
  138. </view>
  139. <view v-else-if="!hasMore && messages.length > 0" class="no-more">
  140. <text>没有更多了</text>
  141. </view>
  142. </template>
  143. </scroll-view>
  144. <!-- 底部操作 -->
  145. <view class="bottom-actions">
  146. <button class="btn-clear-all" @click="clearAllMessages">
  147. 清除所有消息
  148. </button>
  149. <button class="btn-mark-read" @click="markAllAsRead">
  150. 全部已读
  151. </button>
  152. </view>
  153. </view>
  154. </template>
  155. <script>
  156. import {
  157. getEnergyLogs,
  158. getPointsLogs,
  159. getCfList,
  160. getNotificationList,
  161. readNotification
  162. } from '../../utils/api.js'
  163. import { parseDate } from '../../utils/format.js'
  164. export default {
  165. name: 'MessageHistory',
  166. data() {
  167. return {
  168. activeFilter: 'all',
  169. loading: true,
  170. loadingMore: false,
  171. hasMore: true,
  172. currentPage: 1,
  173. pageSize: 20,
  174. // 各类消息
  175. energyMessages: [],
  176. pointsMessages: [],
  177. cfMessages: [],
  178. systemMessages: [],
  179. // 用于分页
  180. energyPage: 1,
  181. pointsPage: 1,
  182. cfPage: 1,
  183. notificationPage: 1,
  184. }
  185. },
  186. computed: {
  187. messages() {
  188. // 根据当前筛选返回对应消息
  189. switch(this.activeFilter) {
  190. case 'energy': return this.energyMessages
  191. case 'points': return this.pointsMessages
  192. case 'cf': return this.cfMessages
  193. case 'system': return this.systemMessages
  194. default:
  195. // 合并所有消息并按时间排序
  196. var all = [
  197. ...this.energyMessages.map(m => ({...m, type: 'energy'})),
  198. ...this.pointsMessages.map(m => ({...m, type: 'points'})),
  199. ...this.cfMessages.map(m => ({...m, type: 'cf'})),
  200. ...this.systemMessages.map(m => ({...m, type: 'system'}))
  201. ]
  202. return all.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
  203. }
  204. }
  205. },
  206. onLoad() {
  207. this.loadAllMessages()
  208. },
  209. onShow() {
  210. // 每次显示时刷新未读数
  211. this.refreshUnreadCounts()
  212. },
  213. methods: {
  214. goBack() {
  215. uni.navigateBack()
  216. },
  217. switchFilter(filter) {
  218. this.activeFilter = filter
  219. // 如果还没加载过该类型消息,则加载
  220. if (filter === 'energy' && this.energyMessages.length === 0) {
  221. this.loadEnergyMessages()
  222. } else if (filter === 'points' && this.pointsMessages.length === 0) {
  223. this.loadPointsMessages()
  224. } else if (filter === 'cf' && this.cfMessages.length === 0) {
  225. this.loadCfMessages()
  226. } else if (filter === 'system' && this.systemMessages.length === 0) {
  227. this.loadSystemMessages()
  228. }
  229. },
  230. async loadAllMessages() {
  231. this.loading = true
  232. try {
  233. await Promise.all([
  234. this.loadEnergyMessages(),
  235. this.loadPointsMessages(),
  236. this.loadCfMessages(),
  237. this.loadSystemMessages()
  238. ])
  239. } finally {
  240. this.loading = false
  241. }
  242. },
  243. async loadEnergyMessages() {
  244. var memberId = uni.getStorageSync('currentMemberId') || this.currentChildId
  245. if (!memberId) return
  246. var res = await getEnergyLogs(memberId, null, this.energyPage, this.pageSize)
  247. if (res.code === 200 && res.data) {
  248. var records = res.data.records || []
  249. this.energyMessages = records.map(record => this.formatEnergyMessage(record))
  250. this.energyPage++
  251. this.hasMore = records.length === this.pageSize
  252. }
  253. },
  254. async loadPointsMessages() {
  255. var memberId = uni.getStorageSync('currentMemberId') || this.currentChildId
  256. if (!memberId) return
  257. var res = await getPointsLogs(memberId, this.pointsPage, this.pageSize)
  258. if (res.code === 200 && res.data) {
  259. var records = res.data.records || []
  260. this.pointsMessages = records.map(record => this.formatPointsMessage(record))
  261. this.pointsPage++
  262. this.hasMore = records.length === this.pageSize
  263. }
  264. },
  265. async loadCfMessages() {
  266. var res = await getCfList(this.cfPage, this.pageSize)
  267. if (res.code === 200 && res.data) {
  268. var records = res.data.records || []
  269. this.cfMessages = records.map(record => this.formatCfMessage(record))
  270. this.cfPage++
  271. this.hasMore = records.length === this.pageSize
  272. }
  273. },
  274. async loadSystemMessages() {
  275. var res = await getNotificationList()
  276. if (res.code === 200 && res.data) {
  277. var records = res.data || []
  278. this.systemMessages = records.map(record => this.formatSystemMessage(record))
  279. this.notificationPage++
  280. }
  281. },
  282. formatEnergyMessage(record) {
  283. var desc = record.description || ''
  284. var sourceType = record.sourceType || ''
  285. var contentText = ''
  286. switch(sourceType) {
  287. case 'task': contentText = '完成了' + (desc || '任务'); break
  288. case 'product': contentText = '购买了' + (desc || '商品'); break
  289. case 'medical_report': contentText = '上传了' + (desc || '体检报告'); break
  290. case 'daily_record': contentText = '完成了' + (desc || '日常记录'); break
  291. default: contentText = desc || '能量变动'
  292. }
  293. return {
  294. id: 'energy-' + record.id,
  295. type: 'energy',
  296. title: '能量变动',
  297. fullText: '⚡ ' + contentText,
  298. description: contentText,
  299. amount: record.amount,
  300. isRead: true,
  301. createdAt: record.createdAt,
  302. time: this.formatTime(record.createdAt)
  303. }
  304. },
  305. formatPointsMessage(record) {
  306. var desc = record.description || ''
  307. var type = record.type || ''
  308. var contentText = ''
  309. switch(type) {
  310. case 'task':
  311. contentText = '完成了' + (desc || '任务') + '获得' + (record.amount >= 0 ? '+' : '') + record.amount + '积分'; break
  312. case 'reward':
  313. contentText = '完成了' + (desc || '心愿兑换') + '获得' + (record.amount >= 0 ? '+' : '') + record.amount + '积分'; break
  314. case 'adjust':
  315. contentText = '管理员调整' + (record.amount >= 0 ? '+' : '') + record.amount + '积分'; break
  316. case 'expire':
  317. contentText = '积分过期扣除' + Math.abs(record.amount) + '积分'; break
  318. default:
  319. contentText = (desc || '积分变动') + (record.amount >= 0 ? '获得' : '扣除') + Math.abs(record.amount) + '积分'
  320. }
  321. return {
  322. id: 'points-' + record.id,
  323. type: 'points',
  324. fullText: '🪙 ' + contentText,
  325. description: contentText,
  326. amount: record.amount,
  327. isRead: true,
  328. createdAt: record.createdAt,
  329. time: this.formatTime(record.createdAt)
  330. }
  331. },
  332. formatCfMessage(record) {
  333. var desc = record.description || ''
  334. var action = record.action || ''
  335. var contentText = ''
  336. switch(action) {
  337. case 'transfer_send': contentText = 'CF值转出' + Math.abs(record.amount) + 'CF'; break
  338. case 'transfer_receive': contentText = 'CF值转入' + record.amount + 'CF'; break
  339. case 'commission': contentText = '获得' + record.amount + 'CF分佣'; break
  340. default: contentText = (desc || 'CF值变动') + (record.amount >= 0 ? '+' : '') + record.amount + 'CF'
  341. }
  342. return {
  343. id: 'cf-' + record.id,
  344. type: 'cf',
  345. fullText: '💰 ' + contentText,
  346. description: contentText,
  347. amount: record.amount,
  348. isRead: true,
  349. createdAt: record.createdAt,
  350. time: this.formatTime(record.createdAt)
  351. }
  352. },
  353. formatSystemMessage(record) {
  354. return {
  355. id: 'system-' + record.id,
  356. type: 'system',
  357. title: record.title || '系统通知',
  358. description: record.content || '',
  359. amount: 0,
  360. isRead: record.isRead === 1,
  361. createdAt: record.createdAt,
  362. time: this.formatTime(record.createdAt),
  363. record: record
  364. }
  365. },
  366. formatTime(dateStr) {
  367. if (!dateStr) return ''
  368. var d = parseDate(dateStr)
  369. if (!d) return ''
  370. var now = new Date()
  371. var diff = now - d
  372. var minutes = Math.floor(diff / 60000)
  373. if (minutes < 1) return '刚刚'
  374. if (minutes < 60) return minutes + '分钟前'
  375. var hours = Math.floor(minutes / 60)
  376. if (hours < 24) return hours + '小时前'
  377. var days = Math.floor(hours / 24)
  378. if (days === 1) return '昨天'
  379. if (days < 7) return days + '天前'
  380. return (d.getMonth() + 1) + '月' + d.getDate() + '日'
  381. },
  382. markAsRead(msg) {
  383. msg.isRead = true
  384. // 如果是系统通知,调用API标记为已读
  385. if (msg.type === 'system' && msg.record && msg.record.id) {
  386. readNotification({ id: msg.record.id }).catch(function() {})
  387. }
  388. },
  389. markAllAsRead() {
  390. // 标记所有消息为已读
  391. this.energyMessages.forEach(m => m.isRead = true)
  392. this.pointsMessages.forEach(m => m.isRead = true)
  393. this.cfMessages.forEach(m => m.isRead = true)
  394. this.systemMessages.forEach(m => m.isRead = true)
  395. // 批量标记系统通知为已读
  396. var unreadNotifications = this.systemMessages.filter(m => !m.isRead)
  397. if (unreadNotifications.length > 0) {
  398. readNotification({ ids: unreadNotifications.map(m => m.record.id) }).catch(function() {})
  399. }
  400. uni.showToast({ title: '已全部标为已读', icon: 'success' })
  401. },
  402. clearAllMessages() {
  403. uni.showModal({
  404. title: '清除消息',
  405. content: '确定要清除所有历史消息吗?',
  406. success: (res) => {
  407. if (res.confirm) {
  408. this.energyMessages = []
  409. this.pointsMessages = []
  410. this.cfMessages = []
  411. this.systemMessages = []
  412. this.currentPage = 1
  413. this.energyPage = 1
  414. this.pointsPage = 1
  415. this.cfPage = 1
  416. this.notificationPage = 1
  417. uni.showToast({ title: '已清除', icon: 'success' })
  418. }
  419. }
  420. })
  421. },
  422. refreshUnreadCounts() {
  423. // 刷新未读数(可选:从后端获取)
  424. },
  425. loadMore() {
  426. if (!this.hasMore || this.loadingMore) return
  427. this.loadingMore = true
  428. var self = this
  429. Promise.all([
  430. this.energyPage < 10 ? this.loadEnergyMessages() : Promise.resolve(),
  431. this.pointsPage < 10 ? this.loadPointsMessages() : Promise.resolve(),
  432. this.cfPage < 10 ? this.loadCfMessages() : Promise.resolve(),
  433. this.notificationPage < 10 ? this.loadSystemMessages() : Promise.resolve()
  434. ]).finally(() => {
  435. this.loadingMore = false
  436. })
  437. }
  438. }
  439. }
  440. </script>
  441. <style scoped>
  442. .container {
  443. min-height: 100vh;
  444. background: #f5f7fa;
  445. display: flex;
  446. flex-direction: column;
  447. }
  448. .nav-bar {
  449. display: flex;
  450. align-items: center;
  451. justify-content: space-between;
  452. padding: 20rpx 30rpx;
  453. background: #fff;
  454. border-bottom: 1rpx solid #eee;
  455. position: sticky;
  456. top: 0;
  457. z-index: 100;
  458. }
  459. .nav-back {
  460. width: 60rpx;
  461. height: 60rpx;
  462. display: flex;
  463. align-items: center;
  464. justify-content: center;
  465. }
  466. .nav-back-icon {
  467. font-size: 40rpx;
  468. color: #333;
  469. }
  470. .nav-title {
  471. font-size: 32rpx;
  472. font-weight: bold;
  473. color: #333;
  474. }
  475. .nav-placeholder {
  476. width: 60rpx;
  477. }
  478. .filter-tabs {
  479. display: flex;
  480. padding: 20rpx 30rpx;
  481. background: #fff;
  482. border-bottom: 1rpx solid #eee;
  483. gap: 16rpx;
  484. }
  485. .filter-tab {
  486. padding: 12rpx 24rpx;
  487. background: #f5f5f5;
  488. border-radius: 20rpx;
  489. font-size: 24rpx;
  490. color: #666;
  491. }
  492. .filter-tab.active {
  493. background: linear-gradient(135deg, #F97316, #FB923C);
  494. color: #fff;
  495. }
  496. .message-list {
  497. flex: 1;
  498. padding: 20rpx 30rpx;
  499. overflow-y: auto;
  500. }
  501. .message-item {
  502. display: flex;
  503. align-items: flex-start;
  504. padding: 24rpx;
  505. background: #fff;
  506. border-radius: 16rpx;
  507. margin-bottom: 16rpx;
  508. box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
  509. }
  510. .message-item.unread {
  511. border: 2rpx solid #F97316;
  512. }
  513. .message-icon {
  514. width: 64rpx;
  515. height: 64rpx;
  516. border-radius: 12rpx;
  517. display: flex;
  518. align-items: center;
  519. justify-content: center;
  520. margin-right: 20rpx;
  521. flex-shrink: 0;
  522. }
  523. .message-icon text {
  524. font-size: 32rpx;
  525. }
  526. .message-content {
  527. flex: 1;
  528. min-width: 0;
  529. }
  530. .message-title {
  531. font-size: 28rpx;
  532. font-weight: 500;
  533. color: #333;
  534. display: block;
  535. overflow: hidden;
  536. text-overflow: ellipsis;
  537. -webkit-line-clamp: 1;
  538. -webkit-box-orient: vertical;
  539. }
  540. .message-desc {
  541. font-size: 24rpx;
  542. color: #999;
  543. margin-top: 6rpx;
  544. display: block;
  545. overflow: hidden;
  546. text-overflow: ellipsis;
  547. -webkit-line-clamp: 1;
  548. -webkit-box-orient: vertical;
  549. }
  550. .message-amount {
  551. font-size: 28rpx;
  552. font-weight: bold;
  553. margin-top: 4rpx;
  554. }
  555. .message-meta {
  556. display: flex;
  557. flex-direction: column;
  558. align-items: flex-end;
  559. gap: 8rpx;
  560. }
  561. .message-time {
  562. font-size: 20rpx;
  563. color: #bbb;
  564. }
  565. .loading-state,
  566. .empty-state {
  567. display: flex;
  568. flex-direction: column;
  569. align-items: center;
  570. justify-content: center;
  571. padding: 100rpx 0;
  572. }
  573. .empty-icon {
  574. font-size: 80rpx;
  575. margin-bottom: 20rpx;
  576. }
  577. .empty-text {
  578. font-size: 26rpx;
  579. color: #999;
  580. }
  581. .loading-more,
  582. .no-more {
  583. text-align: center;
  584. padding: 30rpx 0;
  585. font-size: 24rpx;
  586. color: #999;
  587. }
  588. .bottom-actions {
  589. display: flex;
  590. gap: 20rpx;
  591. padding: 30rpx;
  592. background: #fff;
  593. border-top: 1rpx solid #eee;
  594. position: sticky;
  595. bottom: 0;
  596. }
  597. .btn-clear-all,
  598. .btn-mark-read {
  599. flex: 1;
  600. height: 80rpx;
  601. line-height: 80rpx;
  602. border-radius: 40rpx;
  603. font-size: 28rpx;
  604. font-weight: 500;
  605. border: none;
  606. }
  607. .btn-clear-all {
  608. background: #f5f5f5;
  609. color: #666;
  610. }
  611. .btn-mark-read {
  612. background: linear-gradient(135deg, #F97316, #FB923C);
  613. color: #fff;
  614. }
  615. </style>