新增消息已读状态

复用 chat_participants.last_read_at 推导已读,无需新表/迁移。

- 后端:MessageDTO 增加 is_read,toMessageDTOs 按「全部已读」语义计算
  (所有其他参与者均已读才算已读,1对1 即对方已读);MarkRead 成功后
  推送 conversation_read 事件(含 reader 与 read_at),NotifyConversation
  与 NotifyAllAdmins 双通道通知,使发送方/客服实时刷新已读
- 前端:ChatMessage 增加 is_read,ChatView/AdminChatsView/MobileChatView
  在自己的消息下渲染「已读/未读」;收到 conversation_read 且本端仍有
  未获回执的自己消息时才重载,避免无谓请求

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
yml2213
2026-06-16 22:22:05 +08:00
co-authored by Claude Opus 4.8
parent cc1c0d33de
commit e75a39b03a
9 changed files with 174 additions and 6 deletions
@@ -58,6 +58,7 @@ function handleSSEEvent(event: ChatEvent) {
sender_name: msg.sender_name,
sender_avatar: '',
is_self: isSelf,
is_read: false,
content_type: msg.content_type as ChatMessage['content_type'],
content: msg.content,
attachment_urls: msg.attachment_urls || [],
@@ -76,6 +77,13 @@ function handleSSEEvent(event: ChatEvent) {
if (event.type === 'conversation_updated' && event.conversation_id === conversationID.value) {
loadConversation()
}
if (event.type === 'conversation_read' && event.conversation_id === conversationID.value) {
// 对方已读:仅当本端仍有未获回执的自己消息时重载,刷新「已读」状态
const isSelfReader = event.reader_type === 'user' && event.reader_id === currentUserId
if (!isSelfReader && messages.value.some(m => m.is_self && !m.is_read)) {
loadMessages(false)
}
}
}
const { onEvent } = useChatSSE('user', '/api/chats/events')
@@ -305,6 +313,9 @@ function handleKeydown(e: KeyboardEvent) {
<ChatAttachmentImage v-for="url in item.attachment_urls" :key="url" :source="url" />
</div>
<span class="message-time">{{ formatDateMinute(item.created_at) }}</span>
<span v-if="item.is_self" class="read-status" :class="{ read: item.is_read }">
{{ item.is_read ? '已读' : '未读' }}
</span>
</div>
</template>
</div>
@@ -538,6 +549,16 @@ function handleKeydown(e: KeyboardEvent) {
font-size: 11px;
}
.read-status {
margin-top: 2px;
color: #a1a8b4;
font-size: 11px;
}
.read-status.read {
color: #67c23a;
}
.system-message {
max-width: 80%;
padding: 6px 12px;