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

354 lines
7.8 KiB
Plaintext

<template>
<view class="message-item" :class="{ 'unread': !getIsRead(), 'urgent': item.is_urgent, 'starred': getIsStarred() }" @click="handleClick">
<!-- 消息类型指示器 -->
<view class="message-indicator" :style="{ backgroundColor: getTypeColor(getMessageType()) }"></view>
<!-- 消息头部 -->
<view class="message-header">
<view class="message-meta">
<text class="message-type">{{ getTypeName(getMessageType()) }}</text>
<text class="message-time">{{ formatTime(item.created_at) }}</text>
<text v-if="item.is_urgent" class="urgent-badge">紧急</text>
</view>
<view class="message-actions">
<text v-if="getIsStarred()" class="star-icon">★</text>
<text v-if="getIsArchived()" class="archive-icon"></text>
<text class="more-icon" @click.stop="toggleActionMenu">⋯</text>
</view>
</view>
<!-- 消息主体 -->
<view class="message-body">
<text class="message-title" v-if="item.title !== null">{{ item.title }}</text>
<text class="message-content">{{ getDisplayContent(item.content) }}</text>
<!-- 附件指示 -->
<view v-if="hasAttachments(item)" class="attachment-indicator">
<text class="attachment-icon"></text>
<text class="attachment-count">{{ getAttachmentCount(item) }}个附件</text>
</view>
</view>
<!-- 消息底部 -->
<view class="message-footer">
<text class="sender-info">{{ getSenderInfo(item) }}</text>
<view class="message-stats">
<text v-if="item.reply_count > 0" class="reply-count">{{ item.reply_count }}回复</text>
<text v-if="item.read_count > 0 && item.total_recipients > 0" class="read-status">
{{ item.read_count }}/{{ item.total_recipients }}已读
</text>
</view>
</view>
<!-- 操作菜单 -->
<view v-if="showActionMenu" class="action-menu" @click.stop="">
<view class="action-item" @click="handleAction('read')">
<text>{{ getIsRead() ? '标为未读' : '标为已读' }}</text>
</view>
<view class="action-item" @click="handleAction('star')">
<text>{{ getIsStarred() ? '取消收藏' : '收藏' }}</text> </view>
<view class="action-item" @click="handleAction('archive')">
<text>{{ getIsArchived() ? '取消归档' : '归档' }}</text>
</view>
<view class="action-item danger" @click="handleAction('delete')">
<text>删除</text>
</view>
</view>
</view>
</template>
<script lang="uts">
import { formatTime, getTypeName, getTypeColor, truncateText } from '../../utils/msgUtils.uts'
import type { MessageWithRecipient } from '../../utils/msgTypes.uts'
export default {
name: 'MessageItem', props: {
item: {
type: Object as PropType<MessageWithRecipient>,
required: true
},
showActionButtons: {
type: Boolean,
default: true
}
},
emits: ['click', 'action'],
data() {
return {
showActionMenu: false as boolean
}
},
methods: { handleClick() {
this.$emit('click', this.item)
},
toggleActionMenu() {
this.showActionMenu = !this.showActionMenu
},
handleAction(action: string) {
this.showActionMenu = false
this.$emit('action', {
action: action,
item: this.item
})
},
formatTime(dateStr: string): string {
return formatTime(dateStr)
},
getTypeName(typeId: string): string {
return getTypeName(typeId)
},
getTypeColor(typeId: string): string {
return getTypeColor(typeId)
},
getDisplayContent(content: string | null): string {
if (content === null) {
return '无内容'
}
if (content.length > 100) {
return content.substring(0, 100) + '...'
}
return content
},
hasAttachments(item: MessageWithRecipient): boolean {
if (item.attachments === null) {
return false
}
const attachments = item.attachments as UTSJSONObject
const files = attachments.getAny('files')
return files !== null
},
getAttachmentCount(item: MessageWithRecipient): number {
if (item.attachments === null) {
return 0
}
const attachments = item.attachments as UTSJSONObject
const files = attachments.getAny('files') as Array<UTSJSONObject> | null
return files !== null ? files.length : 0
},
getSenderInfo(item: MessageWithRecipient): string {
if (item.sender_name !== null) {
return `来自: ${item.sender_name}`
}
if (item.sender_type === 'system') {
return '系统消息'
} else if (item.sender_type === 'device') {
return '设备消息'
} else {
return '未知发送者'
}
},
// 获取消息类型代码
getMessageType(): string {
return this.item.message_type ?? this.item.message_type_id ?? 'default'
},
// 获取是否已读
getIsRead(): boolean {
return this.item.is_read ?? false
},
// 获取是否已收藏
getIsStarred(): boolean {
return this.item.is_starred ?? false
},
// 获取是否已归档
getIsArchived(): boolean {
return this.item.is_archived ?? false
}
}
}
</script>
<style scoped>
.message-item {
position: relative;
background-color: #ffffff;
border-radius: 8px;
margin-bottom: 12px;
padding: 16px;
border-left: 4px solid #e0e0e0;
}
.message-item.unread {
background-color: #f8f9ff;
border-left-color: #2196f3;
}
.message-item.urgent {
border-left-color: #f44336;
}
.message-item.starred {
background-color: #fffbf0;
}
.message-indicator {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
border-radius: 2px;
}
.message-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.message-meta {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.message-type {
font-size: 12px;
color: #666666;
background-color: #f5f5f5;
padding: 2px 6px;
border-radius: 4px;
}
.message-time {
font-size: 12px;
color: #999999;
}
.urgent-badge {
font-size: 10px;
color: #ffffff;
background-color: #f44336;
padding: 2px 6px;
border-radius: 4px;
}
.message-actions {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.star-icon {
color: #ff9800;
font-size: 16px;
}
.archive-icon {
font-size: 14px;
}
.more-icon {
font-size: 16px;
color: #666666;
padding: 4px;
}
.message-body {
margin-bottom: 8px;
}
.message-title {
font-size: 16px;
font-weight: 400;
color: #333333;
margin-bottom: 4px;
line-height: 1.4;
}
.message-content {
font-size: 14px;
color: #666666;
line-height: 1.5;
}
.attachment-indicator {
display: flex;
align-items: center;
gap: 4px;
margin-top: 8px;
}
.attachment-icon {
font-size: 14px;
color: #2196f3;
}
.attachment-count {
font-size: 12px;
color: #2196f3;
}
.message-footer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-top: 8px;
}
.sender-info {
font-size: 12px;
color: #999999;
}
.message-stats {
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
}
.reply-count,
.read-status {
font-size: 12px;
color: #999999;
}
.action-menu {
position: absolute;
top: 40px;
right: 16px;
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.action-item {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
}
.action-item:last-child {
border-bottom: none;
}
.action-item text {
font-size: 14px;
color: #333333;
}
.action-item.danger text {
color: #f44336;
}
.action-item:active {
background-color: #f5f5f5;
}
</style>