Initial commit of akmon project
This commit is contained in:
436
components/message/MessageList.uvue
Normal file
436
components/message/MessageList.uvue
Normal file
@@ -0,0 +1,436 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user