index.uvue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. <template>
  2. <uni-navbar-lite :showLeft=false :show-right=false title="我的消息"></uni-navbar-lite>
  3. <view class="page-container">
  4. <!-- <view>
  5. <text class="page-header"> 我的消息 </text>
  6. </view> -->
  7. <view class="list-page">
  8. <!-- 搜索栏 -->
  9. <view class="search-bar">
  10. <view class="search-box">
  11. <image class="search-icon" src="/static/images/workbench/list/1.png" mode="aspectFit"></image>
  12. <input class="search-input" type="text" placeholder="请输入关键字查询" v-model="keyword" @input="handleInput" @confirm="handleSearch" />
  13. <view v-if="keyword.length > 0" class="clear-btn" @tap="handleClear">
  14. <text class="clear-btn-text">×</text>
  15. </view>
  16. <view class="search-btn" @tap="handleSearch">
  17. <text class="search-btn-text">搜索</text>
  18. </view>
  19. </view>
  20. </view>
  21. <!-- 状态标签 -->
  22. <view class="status-tabs">
  23. <view
  24. class="status-tab"
  25. :class="{ 'active': currentStatus === '' }"
  26. @tap="handleStatusChange('')"
  27. >
  28. <text class="status-tab-text" :class="{ 'active-text': currentStatus === '' }">全部</text>
  29. </view>
  30. <view
  31. class="status-tab"
  32. :class="{ 'active': currentStatus === 'UNREAD' }"
  33. @tap="handleStatusChange('UNREAD')"
  34. >
  35. <text class="status-tab-text" :class="{ 'active-text': currentStatus === 'UNREAD' }">未读</text>
  36. </view>
  37. <view
  38. class="status-tab"
  39. :class="{ 'active': currentStatus === 'READ' }"
  40. @tap="handleStatusChange('READ')"
  41. >
  42. <text class="status-tab-text" :class="{ 'active-text': currentStatus === 'READ' }">已读</text>
  43. </view>
  44. </view>
  45. <!-- 列表内容 -->
  46. <common-list
  47. :dataList="dataList"
  48. :loading="loading"
  49. :refreshing="refreshing"
  50. :hasMore="hasMore"
  51. @refresh="handleRefresh"
  52. @loadMore="loadMore"
  53. @itemClick="handleItemClick"
  54. >
  55. <template #default="{ item, index }">
  56. <view class="list-item" :class="{ 'unread': getStatus(item) === 'UNREAD' }">
  57. <view class="item-container">
  58. <view class="item-header">
  59. <text class="item-title">{{ getMessageTitle(item) }}</text>
  60. <text class="item-status" :class="'status-' + getStatus(item)">{{ getStatusText(item) }}</text>
  61. </view>
  62. <view class="item-content">
  63. <text class="content-text">{{ getMessageContent(item) }}</text>
  64. </view>
  65. <view class="item-footer">
  66. <text class="create-time">{{ getCreateTime(item) }}</text>
  67. </view>
  68. </view>
  69. </view>
  70. </template>
  71. </common-list>
  72. <custom-tabbar :current="2" />
  73. </view>
  74. </view>
  75. </template>
  76. <script setup lang="uts">
  77. import { ref, onBeforeUnmount, onMounted } from 'vue'
  78. import { getMessageList, markMessageAsRead } from '../../api/message/index'
  79. // 列表数据
  80. const dataList = ref<any[]>([])
  81. const keyword = ref<string>("")
  82. const currentStatus = ref<string>("")
  83. const page = ref<number>(1)
  84. const pageSize: number = 10
  85. const hasMore = ref<boolean>(true)
  86. const loading = ref<boolean>(false)
  87. const refreshing = ref<boolean>(false)
  88. const total = ref<number>(0)
  89. // 获取消息标题
  90. const getMessageTitle = (item: any | null): string => {
  91. if (item == null) return ''
  92. const jsonItem = item as UTSJSONObject
  93. const val = jsonItem['messageTitle']
  94. return val != null ? val.toString() : ''
  95. }
  96. // 获取消息内容
  97. const getMessageContent = (item: any | null): string => {
  98. if (item == null) return ''
  99. const jsonItem = item as UTSJSONObject
  100. const val = jsonItem['messageContent']
  101. return val != null ? val.toString() : ''
  102. }
  103. // 获取消息状态
  104. const getStatus = (item: any | null): string => {
  105. if (item == null) return ''
  106. const jsonItem = item as UTSJSONObject
  107. const val = jsonItem['status']
  108. return val != null ? val.toString() : ''
  109. }
  110. // 获取状态文本
  111. const getStatusText = (item: any | null): string => {
  112. const status = getStatus(item)
  113. switch (status) {
  114. case 'UNREAD': return '未读'
  115. case 'READ': return '已读'
  116. default: return status
  117. }
  118. }
  119. // 获取创建时间
  120. const getCreateTime = (item: any | null): string => {
  121. if (item == null) return ''
  122. const jsonItem = item as UTSJSONObject
  123. const val = jsonItem['createTime']
  124. return val != null ? val.toString() : ''
  125. }
  126. // 获取消息ID
  127. const getMessageId = (item: any | null): string => {
  128. if (item == null) return ''
  129. const jsonItem = item as UTSJSONObject
  130. const val = jsonItem['messageId']
  131. return val != null ? val.toString() : ''
  132. }
  133. // 加载列表数据
  134. const loadData = async (isRefresh: boolean): Promise<void> => {
  135. if (loading.value) {
  136. refreshing.value = false
  137. return
  138. }
  139. try {
  140. loading.value = true
  141. if (isRefresh) {
  142. page.value = 1
  143. }
  144. // 调用 API
  145. const result = await getMessageList(page.value, pageSize, keyword.value, currentStatus.value)
  146. // 提取响应数据
  147. const resultObj = result as UTSJSONObject
  148. const rows = resultObj['rows']
  149. const responseTotal = resultObj['total'] as number
  150. if (rows != null) {
  151. const newData = rows as any[]
  152. if (isRefresh) {
  153. dataList.value = newData
  154. } else {
  155. dataList.value = [...dataList.value, ...newData]
  156. }
  157. total.value = responseTotal
  158. hasMore.value = dataList.value.length < responseTotal
  159. } else {
  160. if (isRefresh) {
  161. dataList.value = []
  162. }
  163. hasMore.value = false
  164. }
  165. } catch (e: any) {
  166. uni.showToast({
  167. title: e.message ?? '加载失败',
  168. icon: 'none'
  169. })
  170. } finally {
  171. loading.value = false
  172. setTimeout(() => {
  173. refreshing.value = false
  174. }, 100)
  175. }
  176. }
  177. // 下拉刷新
  178. const handleRefresh = async (): Promise<void> => {
  179. refreshing.value = true
  180. await loadData(true)
  181. }
  182. // 加载更多
  183. const loadMore = (): void => {
  184. if (!hasMore.value || loading.value) {
  185. return
  186. }
  187. page.value++
  188. loadData(false)
  189. }
  190. // 搜索
  191. const handleSearch = (): void => {
  192. page.value = 1
  193. loadData(true)
  194. }
  195. // 输入时搜索
  196. const handleInput = (): void => {
  197. page.value = 1
  198. loadData(true)
  199. }
  200. // 清除搜索
  201. const handleClear = (): void => {
  202. keyword.value = ''
  203. page.value = 1
  204. loadData(true)
  205. }
  206. // 切换状态标签
  207. const handleStatusChange = (status: string): void => {
  208. currentStatus.value = status
  209. page.value = 1
  210. loadData(true)
  211. }
  212. // 点击列表项
  213. const handleItemClick = (item: any | null, index: number): void => {
  214. if (item == null) return
  215. const messageId = getMessageId(item)
  216. if (messageId.length == 0) return
  217. // 标记为已读
  218. if (getStatus(item) == 'UNREAD') {
  219. markMessageAsRead(messageId).then(() => {
  220. // 重新加载数据
  221. loadData(true)
  222. // 通知tabbar更新未读消息数量
  223. uni.$emit('updateUnreadCount')
  224. }).catch((e) => {
  225. console.error('标记已读失败:', e)
  226. })
  227. }
  228. // 可以在这里添加跳转到消息详情页的逻辑
  229. }
  230. // 组件卸载前清理
  231. onBeforeUnmount(() => {
  232. refreshing.value = false
  233. loading.value = false
  234. })
  235. // 初始化
  236. onMounted(() => {
  237. loadData(true)
  238. })
  239. </script>
  240. <style lang="scss">
  241. .page-container {
  242. flex: 1;
  243. background-color: #f5f8fe;
  244. padding-top: env(safe-area-inset-top);
  245. background-color: #e8f0f9;
  246. padding: 20rpx 10rpx 20rpx 10rpx;
  247. }
  248. .page-header{
  249. font-size: 32rpx;
  250. color: #333333;
  251. font-weight: bold;
  252. padding-top:20px;
  253. padding-bottom: 10px;
  254. }
  255. .list-page {
  256. flex: 1;
  257. background-color: #e8f0f9;
  258. }
  259. .search-bar {
  260. padding: 20rpx 30rpx;
  261. background-color: #d7eafe;
  262. }
  263. .search-box {
  264. flex-direction: row;
  265. align-items: center;
  266. height: 72rpx;
  267. padding: 0 24rpx;
  268. background-color: #f5f5f5;
  269. border-radius: 36rpx;
  270. .search-icon {
  271. width: 32rpx;
  272. height: 32rpx;
  273. margin-right: 12rpx;
  274. }
  275. .search-input {
  276. flex: 1;
  277. font-size: 28rpx;
  278. color: #333333;
  279. }
  280. .search-btn {
  281. padding: 10rpx 20rpx;
  282. background-color: #007aff;
  283. border-radius: 8rpx;
  284. margin-left: 10rpx;
  285. }
  286. .search-btn-text {
  287. color: #ffffff;
  288. font-size: 24rpx;
  289. }
  290. .clear-btn {
  291. width: 36rpx;
  292. height: 36rpx;
  293. border-radius: 18rpx;
  294. background-color: #cccccc;
  295. align-items: center;
  296. justify-content: center;
  297. margin-left: 10rpx;
  298. }
  299. .clear-btn-text {
  300. color: #ffffff;
  301. font-size: 24rpx;
  302. font-weight: bold;
  303. }
  304. }
  305. .status-tabs {
  306. display: flex;
  307. flex-direction: row;
  308. padding: 0rpx 30rpx 20rpx;
  309. background-color: #d7eafe;
  310. }
  311. .status-tab {
  312. flex: 1;
  313. padding: 16rpx 0;
  314. text-align: center;
  315. margin-right: 16rpx;
  316. border-radius: 8rpx;
  317. background-color: #f5f5f5;
  318. justify-content: center;
  319. align-items: center;
  320. &:last-child {
  321. margin-right: 0;
  322. }
  323. &.active {
  324. background-color: #007aff;
  325. }
  326. .status-tab-text {
  327. font-size: 26rpx;
  328. color: #666666;
  329. &.active-text {
  330. color: #ffffff;
  331. font-weight: bold;
  332. }
  333. }
  334. }
  335. .list-item {
  336. margin: 16rpx 30rpx;
  337. background-color: #ffffff;
  338. border-radius: 12rpx;
  339. box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
  340. &.unread {
  341. border-left: 4rpx solid #007aff;
  342. }
  343. }
  344. .item-container {
  345. padding: 20rpx;
  346. }
  347. .item-header {
  348. flex-direction: row;
  349. align-items: center;
  350. justify-content: space-between;
  351. margin-bottom: 12rpx;
  352. .item-title {
  353. flex: 1;
  354. font-size: 28rpx;
  355. color: #333333;
  356. font-weight: bold;
  357. }
  358. .item-status {
  359. font-size: 20rpx;
  360. padding: 6rpx 12rpx;
  361. border-radius: 6rpx;
  362. &.status-UNREAD {
  363. background-color: #e6f7ff;
  364. color: #007aff;
  365. }
  366. &.status-READ {
  367. background-color: #f6ffed;
  368. color: #52c41a;
  369. }
  370. }
  371. }
  372. .item-content {
  373. margin-bottom: 12rpx;
  374. .content-text {
  375. font-size: 24rpx;
  376. color: #666666;
  377. line-height: 36rpx;
  378. }
  379. }
  380. .item-footer {
  381. display: flex;
  382. justify-content: flex-end;
  383. .create-time {
  384. font-size: 20rpx;
  385. color: #999999;
  386. }
  387. }
  388. </style>