Files
akmon/components/message/MessageList.uvue
2026-01-20 08:04:15 +08:00

437 lines
8.9 KiB
Plaintext

<template>
<view class="message-list">
<!-- 加载状态 -->
<view v-if="loading && messages.length === 0" class="loading-container">
<text class="loading-text">加载中...</text>
</view>
<!-- 空状态 -->
<view v-else-if="!loading && messages.length === 0" class="empty-container">
<text class="empty-icon"></text>
<text class="empty-text">暂无消息</text>
<text class="empty-desc">您还没有收到任何消息</text>
</view>
<!-- 消息列表 -->
<scroll-view
v-else
class="message-scroll"
scroll-y="true"
@scrolltolower="handleLoadMore"
:refresher-enabled="true"
@refresherrefresh="handleRefresh"
:refresher-triggered="refreshing"
>
<!-- 批量选择模式头部 -->
<view v-if="selectionMode" class="selection-header">
<view class="selection-info">
<text class="selection-count">已选择 {{ selectedIds.length }} 条消息</text>
</view>
<view class="selection-actions">
<button class="action-btn" @click="handleBatchRead">
<text class="action-text">标为已读</text>
</button>
<button class="action-btn" @click="handleBatchStar">
<text class="action-text">收藏</text>
</button>
<button class="action-btn danger" @click="handleBatchDelete">
<text class="action-text">删除</text>
</button>
</view>
</view>
<!-- 消息项列表 -->
<view class="message-items">
<view
v-for="message in messages"
:key="message.id"
class="message-item-wrapper"
:class="{ 'selected': isSelected(message.id) }"
>
<!-- 选择框 -->
<checkbox
v-if="selectionMode"
class="selection-checkbox"
:checked="isSelected(message.id)"
@change="handleToggleSelection(message.id)"
/>
<!-- 消息内容 -->
<MessageItem
:item="message"
:show-actions="!selectionMode"
@click="handleMessageClick"
@action="handleMessageAction"
/>
</view>
</view>
<!-- 加载更多 -->
<view v-if="hasMore" class="load-more-container">
<view v-if="loadingMore" class="loading-more">
<text class="loading-text">加载更多...</text>
</view>
<button v-else class="load-more-btn" @click="handleLoadMore">
<text class="load-more-text">加载更多</text>
</button>
</view>
<!-- 已到底部 -->
<view v-else-if="messages.length > 0" class="end-container">
<text class="end-text">已显示全部消息</text>
</view>
</scroll-view>
<!-- 浮动操作按钮 -->
<view class="fab-container">
<view v-if="!selectionMode" class="fab-group">
<button class="fab-btn" @click="handleToggleSelection">
<text class="fab-icon">☑️</text>
</button>
<button class="fab-btn primary" @click="handleCompose">
<text class="fab-icon">✏️</text>
</button>
</view>
<view v-else class="fab-group">
<button class="fab-btn" @click="handleCancelSelection">
<text class="fab-icon">❌</text>
</button>
</view>
</view>
</view>
</template>
<script lang="uts">
import MessageItem from './MessageItem.uvue'
import type { Message } from '../../utils/msgTypes.uts'
export default {
name: 'MessageList',
components: {
MessageItem
},
props: {
messages: {
type: Array as PropType<Array<Message>>,
default: (): Array<Message> => []
},
loading: {
type: Boolean,
default: false
},
loadingMore: {
type: Boolean,
default: false
},
refreshing: {
type: Boolean,
default: false
},
hasMore: {
type: Boolean,
default: false
},
selectionMode: {
type: Boolean,
default: false
}
},
emits: ['click', 'action', 'refresh', 'load-more', 'compose', 'batch-action', 'toggle-selection'],
data() {
return {
selectedIds: [] as Array<string>
}
},
watch: {
selectionMode(newVal: boolean) {
if (!newVal) {
this.selectedIds = []
}
}
},
methods: {
handleMessageClick(message: Message) {
if (this.selectionMode) {
this.handleToggleSelection(message.id)
} else {
this.$emit('click', message)
}
},
handleMessageAction(event: any) {
this.$emit('action', event)
},
handleRefresh() {
this.$emit('refresh')
},
handleLoadMore() {
if (!this.loadingMore && this.hasMore) {
this.$emit('load-more')
}
},
handleCompose() {
this.$emit('compose')
},
handleToggleSelection() {
this.$emit('toggle-selection', !this.selectionMode)
},
handleCancelSelection() {
this.selectedIds = []
this.$emit('toggle-selection', false)
},
isSelected(messageId: string): boolean {
return this.selectedIds.indexOf(messageId) !== -1
},
handleToggleSelection(messageId: string) {
const index = this.selectedIds.indexOf(messageId)
if (index !== -1) {
this.selectedIds.splice(index, 1)
} else {
this.selectedIds.push(messageId)
}
},
handleBatchRead() {
if (this.selectedIds.length === 0) {
return
}
this.$emit('batch-action', {
action: 'read',
messageIds: [...this.selectedIds]
})
},
handleBatchStar() {
if (this.selectedIds.length === 0) {
return
}
this.$emit('batch-action', {
action: 'star',
messageIds: [...this.selectedIds]
})
},
handleBatchDelete() {
if (this.selectedIds.length === 0) {
return
}
uni.showModal({
title: '确认删除',
content: `确定要删除选中的 ${this.selectedIds.length} 条消息吗?`,
success: (res) => {
if (res.confirm) {
this.$emit('batch-action', {
action: 'delete',
messageIds: [...this.selectedIds]
})
}
}
})
}
}
}
</script>
<style scoped>
.message-list {
height: 100%;
position: relative;
}
.loading-container,
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
}
.loading-text {
font-size: 14px;
color: #666666;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-text {
font-size: 18px;
font-weight: 400;
color: #333333;
margin-bottom: 8px;
}
.empty-desc {
font-size: 14px;
color: #666666;
}
.message-scroll {
height: 100%;
}
.selection-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
}
.selection-count {
font-size: 14px;
color: #333333;
}
.selection-actions {
display: flex;
gap: 8px;
}
.action-btn {
padding: 6px 12px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: #ffffff;
}
.action-btn.danger {
border-color: #f44336;
}
.action-text {
font-size: 12px;
color: #333333;
}
.action-btn.danger .action-text {
color: #f44336;
}
.message-items {
padding: 16px;
}
.message-item-wrapper {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
border-radius: 8px;
transition: background-color 0.2s;
}
.message-item-wrapper.selected {
background-color: #e3f2fd;
}
.selection-checkbox {
margin-top: 16px;
}
.load-more-container,
.end-container {
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
}
.loading-more {
display: flex;
align-items: center;
gap: 8px;
}
.load-more-btn {
padding: 8px 16px;
border: 1px solid #e0e0e0;
border-radius: 4px;
background-color: #ffffff;
}
.load-more-text {
font-size: 14px;
color: #666666;
}
.end-text {
font-size: 12px;
color: #999999;
}
.fab-container {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 100;
}
.fab-group {
display: flex;
flex-direction: column;
gap: 12px;
}
.fab-btn {
width: 56px;
height: 56px;
border-radius: 50%;
background-color: #ffffff;
border: 1px solid #e0e0e0;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.fab-btn.primary {
background-color: #2196f3;
border-color: #2196f3;
}
.fab-icon {
font-size: 20px;
}
.fab-btn.primary .fab-icon {
color: #ffffff;
}
.fab-btn:active {
transform: scale(0.95);
}
/* 响应式布局 */
@media (max-width: 480px) {
.selection-header {
flex-direction: column;
gap: 8px;
align-items: stretch;
}
.selection-actions {
justify-content: space-between;
}
.fab-container {
bottom: 16px;
right: 16px;
}
}
</style>