Files
akmon/pages/msg/detail.uvue
2026-01-20 08:04:15 +08:00

739 lines
16 KiB
Plaintext

<template>
<view class="msg-detail-page">
<!-- 导航栏 -->
<view class="nav-bar">
<view class="nav-left" @click="goBack">
<text class="nav-icon">←</text>
</view>
<view class="nav-center">
<text class="nav-title">消息详情</text>
</view>
<view class="nav-right">
<view class="nav-btn" @click="showActions">
<text class="nav-icon">⋮</text>
</view>
</view>
</view>
<!-- 加载状态 -->
<view class="loading-container" v-if="loading">
<text class="loading-text">加载中...</text>
</view>
<!-- 消息内容 -->
<scroll-view class="message-content" v-if="message !== null && !loading">
<!-- 消息头部信息 -->
<view class="message-header">
<view class="sender-info">
<view class="sender-avatar">
<text class="avatar-text">{{ getSenderInitial(message?.sender_name) }}</text>
</view>
<view class="sender-details">
<text class="sender-name">{{ message?.sender_name ?? '未知发送者' }}</text>
<text class="sender-type">{{ getSenderTypeText(message?.sender_type ?? '') }}</text>
</view>
</view>
<view class="message-meta">
<view class="meta-item">
<text class="meta-label">发送时间:</text>
<text class="meta-value">{{ formatTime(message?.created_at ?? '') }}</text>
</view>
<view class="meta-item" v-if="message?.scheduled_at != null">
<text class="meta-label">定时发送:</text>
<text class="meta-value">{{ formatTime(message?.scheduled_at ?? '') }}</text>
</view>
<view class="meta-item" v-if="message?.expires_at != null">
<text class="meta-label">过期时间:</text>
<text class="meta-value" :class="{ 'expired': isExpired(message?.expires_at ?? '') }">
{{ formatTime(message?.expires_at ?? '') }}
</text>
</view>
</view>
<!-- 消息状态标签 -->
<view class="status-tags">
<view class="status-tag" :class="getStatusClass(message?.status ?? '')">
<text class="tag-text">{{ getStatusText(message?.status ?? '') }}</text>
</view>
<view class="status-tag priority" v-if="(message?.priority ?? 0) > 50">
<text class="tag-text">{{ getPriorityText(message?.priority ?? 0) }}</text>
</view>
<view class="status-tag urgent" v-if="message?.is_urgent === true">
<text class="tag-text">紧急</text>
</view>
</view>
</view>
<!-- 消息标题 -->
<view class="message-title" v-if="message?.title != null && message?.title !== ''">
<text class="title-text">{{ message?.title ?? '' }}</text>
</view>
<!-- 消息正文 -->
<view class="message-body">
<text class="body-text">{{ message?.content ?? '' }}</text>
</view>
<!-- 附件信息 -->
<view class="attachments" v-if="hasAttachments">
<view class="section-title">
<text class="title-text">附件</text>
</view>
<view class="attachment-list">
<!-- 这里可以根据附件类型渲染不同的组件 -->
<view class="attachment-item" v-for="(attachment, index) in attachmentList" :key="index">
<text class="attachment-name">{{ attachment.name }}</text>
<text class="attachment-size">{{ attachment.size }}</text>
</view>
</view>
</view>
<!-- 元数据信息 -->
<view class="metadata" v-if="hasMetadata">
<view class="section-title">
<text class="title-text">详细信息</text>
</view>
<view class="metadata-list">
<view class="metadata-item" v-for="(item, index) in metadataList" :key="index">
<text class="metadata-key">{{ item.key }}:</text>
<text class="metadata-value">{{ item.value }}</text>
</view>
</view>
</view>
<!-- 回复消息 -->
<view class="replies" v-if="replies.length > 0">
<view class="section-title">
<text class="title-text">回复 ({{ replies.length }})</text>
</view>
<view class="reply-list">
<view class="reply-item" v-for="reply in replies" :key="reply.id">
<msg-item
:message="reply"
:compact="true"
@action="handleReplyAction"
></msg-item>
</view>
</view>
</view>
</scroll-view>
<!-- 操作按钮 -->
<view class="action-bar" v-if="message !== null">
<view class="action-btn" @click="markAsRead" v-if="!isRead">
<text class="btn-text">标记已读</text>
</view>
<view class="action-btn" @click="replyMessage">
<text class="btn-text">回复</text>
</view>
<view class="action-btn" @click="forwardMessage">
<text class="btn-text">转发</text>
</view>
<view class="action-btn danger" @click="deleteMessage">
<text class="btn-text">删除</text>
</view>
</view>
<!-- 操作菜单 -->
<view class="action-menu" v-if="showActionMenu" @click="hideActions">
<view class="menu-content" @click.stop>
<view class="menu-item" @click="copyContent">
<text class="menu-text">复制内容</text>
</view>
<view class="menu-item" @click="shareMessage">
<text class="menu-text">分享消息</text>
</view>
<view class="menu-item" @click="reportMessage">
<text class="menu-text">举报消息</text>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { MsgDataServiceReal } from '@/utils/msgDataServiceReal.uts'
import supa from '@/components/supadb/aksupainstance.uts'
import { MsgUtils } from '@/utils/msgUtils.uts'
import { Message } from '@/utils/msgTypes.uts'
import { setClipboard } from '@/utils/utils.uts'
// 页面参数
const messageId = ref<string>('')
// 响应式数据
const message = ref<Message | null>(null)
const replies = ref<Array<Message>>([])
const loading = ref<boolean>(false)
const isRead = ref<boolean>(false)
const showActionMenu = ref<boolean>(false)
// 计算属性
const hasAttachments = computed(() => {
const msg = message.value
return msg !== null && msg.attachments !== null
})
const hasMetadata = computed(() => {
const msg = message.value
return msg !== null && msg.metadata !== null
})
const attachmentList = computed<Array<UTSJSONObject>>(() => {
const msg = message.value
if (msg === null || msg.attachments === null) {
return []
}
// 解析附件数据
try {
const attachments = msg.attachments
// 这里需要根据实际的附件数据结构来解析
return []
} catch (e) {
return []
}
})
type MetadataItem = { key: string, value: string }
const metadataList = computed((): MetadataItem[] => {
const msg = message.value
if (msg === null || msg.metadata === null) {
return []
}
const list: MetadataItem[] = []
const metadata = msg.metadata
// UTSJSONObject 遍历
const keys = UTSJSONObject.keys(metadata!)
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const value = metadata!.get(key)
list.push({ key: key, value: value != null ? (typeof value === 'string' ? value : JSON.stringify(value)) : '' })
}
return list
})
// 标记消息为已读
async function markAsReadIfNeeded(): Promise<void> {
const msg = message.value
if (msg === null) return
// 获取当前用户ID
const getCurrentUserId = (): string => {
const session = supa.getSession()
const user = session.user
if (user !== null) {
const id = user.getString('id')
return id !== null ? id : 'anonymous'
}
return 'anonymous'
}
try {
await MsgDataServiceReal.markAsRead(msg.id, getCurrentUserId())
isRead.value = true
} catch (error) {
// 标记已读失败不影响主流程
console.error('Mark as read failed:', error)
}
}
// 操作函数
function goBack(): void {
uni.navigateBack()
}
// 加载消息详情
async function loadMessageDetail(): Promise<void> {
if (messageId.value === '') return
loading.value = true
try {
const response = await MsgDataServiceReal.getMessageById(messageId.value)
if (response.status >= 200 && response.status < 300 && response.data !== null) {
message.value = (response.data ?? null) as Message | null
// 自动标记为已读
await markAsReadIfNeeded()
} else {
let errMsg: string = '加载失败'
const err = response.error
if (err != null) {
if (typeof err === 'string') {
errMsg = err as string
} else if (typeof err === 'object' && err.message != null && typeof err.message === 'string') {
errMsg = err.message
}
}
uni.showToast({
title: errMsg,
icon: 'none'
})
// 延迟返回上一页
setTimeout(() => {
goBack()
}, 2000)
}
} catch (error) {
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 工具函数
function getSenderInitial(senderName: string | null): string {
if (senderName === null || senderName === '') {
return '?'
}
return senderName.charAt(0).toUpperCase()
}
function getSenderTypeText(senderType: string): string {
return MsgUtils.getSenderTypeText(senderType as string)
}
function formatTime(timeStr: string | null): string {
return MsgUtils.formatFullTime(timeStr)
}
function isExpired(expiresAt: string | null): boolean {
return MsgUtils.isMessageExpired(expiresAt)
}
function getStatusText(status: string): string {
return MsgUtils.getStatusText(status as string)
}
function getStatusClass(status: string): string {
return `status-${status}`
}
function getPriorityText(priority: number): string {
if (priority >= 90) return '紧急'
if (priority >= 70) return '重要'
if (priority >= 50) return '普通'
return '一般'
}
function showActions(): void {
showActionMenu.value = true
}
function hideActions(): void {
showActionMenu.value = false
}
async function markAsRead(): Promise<void> {
const msg = message.value
if (msg === null) return
const userId = 'current-user-id' // 实际应该从用户状态获取
const response = await MsgDataServiceReal.markAsRead(msg.id, userId)
if (response.status >= 200 && response.status < 300) {
isRead.value = true
uni.showToast({
title: '已标记为已读',
icon: 'success'
})
} else {
let errMsg: string = '操作失败'
const err = response.error
if (err != null) {
if (typeof err === 'string') {
errMsg = err as string
} else if (typeof err === 'object' && err.message != null && typeof err.message === 'string') {
errMsg = err.message
}
}
uni.showToast({
title: errMsg,
icon: 'none'
})
}
}
function replyMessage(): void {
const msg = message.value
if (msg === null) return
uni.navigateTo({
url: `/pages/msg/compose?replyTo=${msg.id}`
})
}
function forwardMessage(): void {
const msg = message.value
if (msg === null) return
uni.navigateTo({
url: `/pages/msg/compose?forward=${msg.id}`
})
}
async function deleteMessage(): Promise<void> {
const msg = message.value
if (msg === null) return
uni.showModal({
title: '确认删除',
content: '确定要删除这条消息吗?',
success: (res) => {
if (res.confirm && msg !== null) {
const userId = 'current-user-id' // 实际应该从用户状态获取
MsgDataServiceReal.deleteMessage(msg.id, userId).then(response => {
if (response.status >= 200 && response.status < 300) {
uni.showToast({
title: '删除成功',
icon: 'success'
})
setTimeout(() => {
goBack()
}, 1500)
} else {
let errMsg: string = '删除失败'
const err = response.error
if (err != null) {
if (typeof err === 'string') {
errMsg = err as string
} else if (typeof err === 'object' && err.message != null && typeof err.message === 'string') {
errMsg = err.message
}
}
uni.showToast({
title: errMsg,
icon: 'none'
})
}
})
}
}
})
}
function copyContent(): void {
const msg = message.value
if (msg === null || msg.content === null) return
setClipboard(msg.content as string)
uni.showToast({
title: '已复制到剪贴板',
icon: 'success'
})
hideActions()
}
function shareMessage(): void {
// 实现分享功能
hideActions()
}
function reportMessage(): void {
// 实现举报功能
hideActions()
}
function handleReplyAction(data: UTSJSONObject): void {
// 处理回复消息的操作
}
// 生命周期
onLoad((options) => {
if (options["id"] !== null) {
messageId.value = options.getString("id") ?? ''
loadMessageDetail()
}
})
</script>
<style>
.msg-detail-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.nav-bar {
display: flex;
justify-content: space-between;
align-items: center;
height: 44px;
padding: 0 16px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.nav-left, .nav-right {
width: 60px;
display: flex;
justify-content: center;
align-items: center;
}
.nav-center {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.nav-title {
font-size: 17px;
font-weight: 700;
color: #000000;
}
.nav-icon {
font-size: 18px;
color: #007AFF;
}
.nav-btn {
width: 40px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
}
.loading-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.loading-text {
color: #999999;
font-size: 16px;
}
.message-content {
flex: 1;
padding: 16px;
}
.message-header {
background-color: #ffffff;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.sender-info {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.sender-avatar {
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #2196F3;
display: flex;
justify-content: center;
align-items: center;
margin-right: 12px;
}
.avatar-text {
color: #ffffff;
font-size: 20px;
font-weight: bold;
}
.sender-details {
flex: 1;
}
.sender-name {
font-size: 16px;
font-weight: 700;
color: #333333;
margin-bottom: 2px;
}
.sender-type {
font-size: 14px;
color: #666666;
}
.message-meta {
margin-bottom: 12px;
}
.meta-item {
display: flex;
margin-bottom: 4px;
}
.meta-label {
font-size: 14px;
color: #666666;
width: 80px;
}
.meta-value {
font-size: 14px;
color: #333333;
flex: 1;
}
.meta-value.expired {
color: #ff4757;
}
.status-tags {
display: flex;
flex-wrap: wrap;
}
.status-tag {
padding: 4px 8px;
border-radius: 12px;
margin-right: 8px;
margin-bottom: 4px;
}
.status-sent {
background-color: #e3f2fd;
}
.status-delivered {
background-color: #e8f5e8;
}
.status-failed {
background-color: #ffebee;
}
.status-tag.priority {
background-color: #fff3e0;
}
.status-tag.urgent {
background-color: #ffebee;
}
.tag-text {
font-size: 12px;
color: #333333;
}
.message-title {
background-color: #ffffff;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.title-text {
font-size: 18px;
font-weight: 700;
color: #333333;
line-height: 1.4;
}
.message-body {
background-color: #ffffff;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.body-text {
font-size: 16px;
color: #333333;
line-height: 1.5;
}
.attachments, .metadata, .replies {
background-color: #ffffff;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.section-title {
margin-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
padding-bottom: 8px;
}
.attachment-item, .metadata-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid #f8f8f8;
}
.attachment-name, .metadata-key {
font-size: 14px;
color: #333333;
}
.attachment-size, .metadata-value {
font-size: 14px;
color: #666666;
}
.reply-item {
margin-bottom: 8px;
}
.action-bar {
display: flex;
padding: 12px 16px;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
}
.action-btn {
flex: 1;
height: 40px;
margin: 0 4px;
border-radius: 20px;
background-color: #2196F3;
display: flex;
justify-content: center;
align-items: center;
}
.action-btn.danger {
background-color: #ff4757;
}
.btn-text {
color: #ffffff;
font-size: 14px;
}
.action-menu {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
}
.menu-content {
background-color: #ffffff;
border-radius: 12px;
min-width: 200px;
max-width: 300px;
}
.menu-item {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
}
.menu-text {
font-size: 16px;
color: #333333;
}
</style>