message.vue 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. <template>
  2. <view class="container pb-tabbar">
  3. <!-- 筛选工具栏 -->
  4. <view class="filter-toolbar">
  5. <view class="filter-tabs">
  6. <view
  7. v-for="tab in filterTabs"
  8. :key="tab.key"
  9. :class="['tab-item', activeTab === tab.key ? 'active' : '']"
  10. @click="onTabChange(tab.key)"
  11. >
  12. <text class="tab-text">{{ tab.name }}</text>
  13. <text v-if="getCount(tab.key) > 0" class="count">({{ getCount(tab.key) }})</text>
  14. </view>
  15. </view>
  16. <view v-if="messageStore.unreadCount > 0" class="read-all" @click="markAllAsRead">
  17. <text>全部已读</text>
  18. </view>
  19. </view>
  20. <!-- 消息列表 -->
  21. <view class="msg-list">
  22. <view
  23. v-for="(msg, index) in filteredMessages"
  24. :key="index"
  25. class="msg-card card-anim"
  26. @click="goToDetail(msg)"
  27. >
  28. <view class="msg-header">
  29. <text class="msg-title">{{ msg.title }}</text>
  30. <view v-if="msg.unread" class="unread-dot"></view>
  31. <text v-else class="time-text">{{ msg.time }}</text>
  32. </view>
  33. <view class="msg-body">
  34. <view class="info-row">
  35. <text class="label">岗位:</text>
  36. <text class="value">{{ msg.position }}</text>
  37. </view>
  38. <view class="info-row flex-between">
  39. <view class="left-box">
  40. <text class="label">公司:</text>
  41. <text class="value">{{ msg.company }}</text>
  42. </view>
  43. <view class="right-box">
  44. <text v-if="msg.unread" class="time-text-inline">{{ msg.time }}</text>
  45. <image src="/static/icons/chevron-right.svg" class="chevron"></image>
  46. </view>
  47. </view>
  48. </view>
  49. </view>
  50. <!-- 空状态 -->
  51. <view v-if="filteredMessages.length === 0" class="empty-box">
  52. <text class="empty-text">暂无相关消息</text>
  53. </view>
  54. </view>
  55. <view class="no-more">—— 已到底啦~ ——</view>
  56. <view class="tabbar-placeholder"></view>
  57. <!-- Custom Tabbar -->
  58. <custom-tabbar :activeIndex="2"></custom-tabbar>
  59. </view>
  60. </template>
  61. <script setup>
  62. import { ref, computed, onMounted } from 'vue';
  63. import { onShow, onPullDownRefresh } from '@dcloudio/uni-app';
  64. import CustomTabbar from '../../components/custom-tabbar/custom-tabbar.vue';
  65. import { messageStore } from '../../store/message.js';
  66. import { getMessageList, readAllMessages, readMessage } from '../../api/message.js';
  67. const activeTab = ref('all');
  68. const filterTabs = [
  69. { name: '全部', key: 'all' },
  70. { name: '未读', key: 'unread' },
  71. { name: '已读', key: 'read' }
  72. ];
  73. const messages = ref([]);
  74. const loading = ref(false);
  75. // 获取消息列表
  76. const fetchMessages = async () => {
  77. loading.value = true;
  78. try {
  79. // 始终获取全部消息,由前端过滤,保证 Tab 上的计数稳定
  80. const res = await getMessageList();
  81. if (res.code === 200) {
  82. // 兼容多种返回结构:res.data (数组), res.rows (分页), res.data.list (某些自定义结构)
  83. let list = [];
  84. if (Array.isArray(res.data)) {
  85. list = res.data;
  86. } else if (res.rows) {
  87. list = res.rows;
  88. } else if (res.data && res.data.list) {
  89. list = res.data.list;
  90. }
  91. messages.value = list.map(item => ({
  92. id: item.id,
  93. title: item.title || '系统通知',
  94. position: item.positionName || '系统消息',
  95. company: item.companyName || '审计之家',
  96. status: item.status,
  97. desc: item.content,
  98. time: item.createTime ? item.createTime.substring(5, 16) : '',
  99. unread: item.isRead === 0
  100. }));
  101. updateGlobalUnread();
  102. }
  103. } catch (e) {
  104. console.error('获取消息列表失败', e);
  105. } finally {
  106. loading.value = false;
  107. }
  108. };
  109. // 监听并更新全局未读数
  110. const updateGlobalUnread = () => {
  111. const count = messages.value.filter(m => m.unread).length;
  112. messageStore.setUnreadCount(count);
  113. };
  114. onMounted(() => {
  115. fetchMessages();
  116. });
  117. onShow(() => {
  118. fetchMessages();
  119. });
  120. onPullDownRefresh(async () => {
  121. await fetchMessages();
  122. uni.stopPullDownRefresh();
  123. });
  124. // 监听标签切换
  125. const onTabChange = (key) => {
  126. activeTab.value = key;
  127. // 不再重新请求后端,直接切换本地过滤
  128. };
  129. const getCount = (key) => {
  130. if (key === 'unread') return messageStore.unreadCount;
  131. // 全部和已读的计数暂时不显示或显示列表长度,主要关注未读数
  132. if (key === 'all') return messages.value.length;
  133. if (key === 'read') return messages.value.filter(m => !m.unread).length;
  134. return 0;
  135. };
  136. const filteredMessages = computed(() => {
  137. if (activeTab.value === 'unread') return messages.value.filter(m => m.unread);
  138. if (activeTab.value === 'read') return messages.value.filter(m => !m.unread);
  139. return messages.value;
  140. });
  141. const markAllAsRead = async () => {
  142. try {
  143. const res = await readAllMessages();
  144. if (res.code === 200) {
  145. messages.value.forEach(m => m.unread = false);
  146. updateGlobalUnread();
  147. uni.showToast({ title: '已全部设为已读', icon: 'none' });
  148. }
  149. } catch (e) {
  150. console.error('标记已读失败', e);
  151. }
  152. };
  153. const goToDetail = async (msg) => {
  154. if (msg.unread) {
  155. try {
  156. await readMessage(msg.id);
  157. msg.unread = false;
  158. // 成功进入详情并标记已读后,全局未读数减1
  159. messageStore.setUnreadCount(Math.max(0, messageStore.unreadCount - 1));
  160. } catch (e) {
  161. console.error('标记单条已读失败', e);
  162. }
  163. }
  164. uni.navigateTo({
  165. url: `/pages/message/msgdetail?data=${encodeURIComponent(JSON.stringify(msg))}`
  166. });
  167. };
  168. </script>
  169. <style lang="scss" scoped>
  170. /* 样式保持不变... */
  171. .container {
  172. min-height: 100vh;
  173. background-color: #F8F9FB;
  174. }
  175. .filter-toolbar {
  176. position: sticky;
  177. top: 0;
  178. z-index: 100;
  179. background-color: #F8F9FB;
  180. padding: 20rpx 30rpx;
  181. display: flex;
  182. justify-content: space-between;
  183. align-items: center;
  184. .filter-tabs {
  185. display: flex;
  186. gap: 30rpx;
  187. .tab-item {
  188. display: flex;
  189. align-items: center;
  190. position: relative;
  191. padding: 10rpx 0;
  192. .tab-text {
  193. font-size: 28rpx;
  194. color: #666666;
  195. }
  196. .count {
  197. font-size: 22rpx;
  198. color: #999999;
  199. margin-left: 4rpx;
  200. }
  201. &.active {
  202. .tab-text {
  203. color: #1F6CFF;
  204. font-weight: bold;
  205. }
  206. &::after {
  207. content: '';
  208. position: absolute;
  209. bottom: 0;
  210. left: 50%;
  211. transform: translateX(-50%);
  212. width: 20rpx;
  213. height: 4rpx;
  214. background-color: #1F6CFF;
  215. border-radius: 2rpx;
  216. }
  217. }
  218. }
  219. }
  220. .read-all {
  221. font-size: 26rpx;
  222. color: #1F6CFF;
  223. &:active { opacity: 0.7; }
  224. }
  225. }
  226. .msg-list {
  227. padding: 0 30rpx;
  228. display: flex;
  229. flex-direction: column;
  230. gap: 24rpx;
  231. }
  232. .msg-card {
  233. background: #FFFFFF;
  234. border-radius: 24rpx;
  235. padding: 32rpx;
  236. box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.02);
  237. .msg-header {
  238. display: flex;
  239. justify-content: space-between;
  240. align-items: center;
  241. margin-bottom: 24rpx;
  242. .msg-title {
  243. font-size: 34rpx;
  244. font-weight: bold;
  245. color: #1A1A1A;
  246. }
  247. .unread-dot {
  248. width: 14rpx;
  249. height: 14rpx;
  250. background-color: #FF4D4F;
  251. border-radius: 50%;
  252. }
  253. .time-text {
  254. font-size: 24rpx;
  255. color: #CCCCCC;
  256. }
  257. }
  258. .msg-body {
  259. .info-row {
  260. display: flex;
  261. align-items: center;
  262. margin-bottom: 8rpx;
  263. &:last-child { margin-bottom: 0; }
  264. .label {
  265. font-size: 28rpx;
  266. color: #999999;
  267. }
  268. .value {
  269. font-size: 28rpx;
  270. color: #666666;
  271. }
  272. }
  273. .flex-between {
  274. justify-content: space-between;
  275. .left-box { display: flex; align-items: center; }
  276. .right-box {
  277. display: flex;
  278. align-items: center;
  279. .time-text-inline {
  280. font-size: 24rpx;
  281. color: #CCCCCC;
  282. margin-right: 12rpx;
  283. }
  284. }
  285. .chevron {
  286. width: 32rpx;
  287. height: 32rpx;
  288. opacity: 0.2;
  289. }
  290. }
  291. }
  292. }
  293. .empty-box {
  294. padding: 100rpx 0;
  295. display: flex;
  296. flex-direction: center;
  297. justify-content: center;
  298. .empty-text {
  299. font-size: 28rpx;
  300. color: #999999;
  301. }
  302. }
  303. .no-more {
  304. text-align: center;
  305. font-size: 24rpx;
  306. color: #CCCCCC;
  307. padding: 60rpx 0;
  308. }
  309. .tabbar-placeholder {
  310. height: 120rpx;
  311. padding-bottom: env(safe-area-inset-bottom);
  312. }
  313. .card-anim {
  314. animation: slideUp 0.5s ease-out backwards;
  315. @for $i from 1 through 10 {
  316. &:nth-child(#{$i}) { animation-delay: #{$i * 0.05s}; }
  317. }
  318. }
  319. @keyframes slideUp {
  320. from { transform: translateY(20rpx); opacity: 0; }
  321. to { transform: translateY(0); opacity: 1; }
  322. }
  323. </style>