Initial commit of akmon project

This commit is contained in:
2026-01-20 08:04:15 +08:00
commit 77a2bab985
1309 changed files with 343305 additions and 0 deletions

850
pages/msg/compose.uvue Normal file
View File

@@ -0,0 +1,850 @@
<template>
<view class="compose-page">
<!-- 导航栏 -->
<view class="nav-bar">
<view class="nav-left" @click="goBack">
<text class="nav-text">取消</text>
</view>
<view class="nav-center">
<text class="nav-title">{{ pageTitle }}</text>
</view>
<view class="nav-right" @click="sendMessage">
<text class="nav-text" :class="{ 'disabled': !canSend }">发送</text>
</view>
</view>
<!-- 表单内容 -->
<scroll-view class="form-content">
<!-- 消息类型选择 -->
<view class="form-section">
<view class="section-title">
<text class="title-text">消息类型</text>
</view>
<picker-view class="type-picker" :value="[typeIndex]" @change="onTypeChange">
<picker-view-column>
<view v-for="(type, index) in messageTypes" :key="type.id">
<text class="picker-text">{{ type.name }}</text>
</view>
</picker-view-column>
</picker-view>
</view>
<!-- 接收者选择 -->
<view class="form-section">
<view class="section-title">
<text class="title-text">发送给</text>
</view>
<view class="receiver-input">
<input
class="input-field"
type="text"
placeholder="输入接收者ID或选择群组"
:value="formData.receiverId"
/>
<view class="select-btn" @click="selectReceiver">
<text class="btn-text">选择</text>
</view>
</view>
</view>
<!-- 消息标题 -->
<view class="form-section">
<view class="section-title">
<text class="title-text">标题 (可选)</text>
</view>
<input
class="title-input"
type="text"
placeholder="请输入消息标题"
:value="formData.title"
maxlength="200"
/>
<view class="char-count">
<text class="count-text">{{ formData.title.length }}/200</text>
</view>
</view>
<!-- 消息内容 -->
<view class="form-section">
<view class="section-title">
<text class="title-text">内容 *</text>
</view>
<textarea
class="content-textarea"
placeholder="请输入消息内容"
:value="formData.content"
maxlength="5000"
:auto-height="true"
/>
<view class="char-count">
<text class="count-text">{{ formData.content.length }}/5000</text>
</view>
</view>
<!-- 消息选项 -->
<view class="form-section">
<view class="section-title">
<text class="title-text">消息选项</text>
</view>
<!-- 优先级设置 -->
<view class="option-item">
<text class="option-label">优先级</text>
<picker-view class="priority-picker" :value="[priorityIndex]" @change="onPriorityChange">
<picker-view-column>
<view v-for="(priority, index) in priorityOptions" :key="index">
<text class="picker-text">{{ priority.label }}</text>
</view>
</picker-view-column>
</picker-view>
</view>
<!-- 紧急标记 -->
<view class="option-item">
<text class="option-label">紧急消息</text>
<switch :value="formData.isUrgent" />
</view>
<!-- 推送通知 -->
<view class="option-item">
<text class="option-label">推送通知</text>
<switch :value="formData.pushNotification" />
</view>
<!-- 邮件通知 -->
<view class="option-item">
<text class="option-label">邮件通知</text>
<switch :value="formData.emailNotification" />
</view>
</view>
<!-- 定时发送 -->
<view class="form-section">
<view class="section-title">
<text class="title-text">定时发送</text>
</view>
<view class="option-item">
<text class="option-label">启用定时发送</text>
<switch :value="formData.enableSchedule" @change="onScheduleToggle" />
</view>
<view class="schedule-time" v-if="formData.enableSchedule">
<picker-date
:value="scheduleDate"
@change="onScheduleDateChange"
></picker-date>
<picker-time
:value="scheduleTime"
@change="onScheduleTimeChange"
></picker-time>
</view>
</view>
<!-- 过期时间 -->
<view class="form-section">
<view class="section-title">
<text class="title-text">过期时间</text>
</view>
<view class="option-item">
<text class="option-label">设置过期时间</text>
<switch :value="formData.enableExpiry" @change="onExpiryToggle" />
</view>
<view class="expiry-time" v-if="formData.enableExpiry">
<picker-date
:value="expiryDate"
@change="onExpiryDateChange"
></picker-date>
<picker-time
:value="expiryTime"
@change="onExpiryTimeChange"
></picker-time>
</view>
</view>
<!-- 原消息引用 (回复/转发时显示) -->
<view class="form-section" v-if="originalMessage !== null">
<view class="section-title">
<text class="title-text">{{ isReply ? '回复消息' : '转发消息' }}</text>
</view>
<view class="original-message">
<view class="original-header">
<text class="original-sender">{{ originalMessage?.sender_name ?? '未知发送者' }}</text>
<text class="original-time">{{ originalMessage?.created_at!=null ? formatTime(new Date(originalMessage!!.created_at!!)) : '' }}</text>
</view>
<view class="original-content">
<text class="original-text">{{ getMessageSummary(originalMessage?.content ?? '', 100) }}</text>
</view>
</view>
</view>
</scroll-view>
<!-- 接收者选择弹窗 -->
<view class="receiver-modal" v-if="showReceiverModal" @click="hideReceiverModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">选择接收者</text>
<view class="modal-close" @click="hideReceiverModal">
<text class="close-icon">×</text>
</view>
</view>
<view class="modal-body">
<view class="receiver-type-tabs">
<view
class="tab-item"
:class="{ 'active': receiverTab === 'user' }"
@click="switchReceiverTab('user')"
>
<text class="tab-text">用户</text>
</view>
<view
class="tab-item"
:class="{ 'active': receiverTab === 'group' }"
@click="switchReceiverTab('group')"
>
<text class="tab-text">群组</text>
</view>
</view>
<scroll-view class="receiver-list">
<view
class="receiver-item"
v-for="receiver in receiverList"
:key="receiver.id"
@click="selectReceiverItem(receiver)"
>
<view class="receiver-info">
<text class="receiver-name">{{ receiver.name }}</text>
<text class="receiver-desc">{{ receiver.description }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { MsgDataServiceReal } from '@/utils/msgDataServiceReal.uts'
import { MsgUtils } from '@/utils/msgUtils.uts'
import supa from '@/components/supadb/aksupainstance.uts'
import {
Message,
MessageType,
SendMessageParams
} from '@/utils/msgTypes.uts'
// 页面参数
const replyToId = ref<string>('')
const forwardId = ref<string>('')
// 响应式数据
const messageTypes = ref<Array<MessageType>>([])
const originalMessage = ref<Message | null>(null)
const loading = ref<boolean>(false)
const showReceiverModal = ref<boolean>(false)
const receiverTab = ref<string>('user')
const receiverList = ref<Array<MessageType>>([])
// 表单数据类型声明,兼容 UTS Android 类型推断
type FormData = {
messageTypeId: string
receiverId: string
title: string
content: string
priority: number
isUrgent: boolean
pushNotification: boolean
emailNotification: boolean
enableSchedule: boolean
enableExpiry: boolean
}
const formData = ref({
messageTypeId: '',
receiverId: '',
title: '',
content: '',
priority: 0,
isUrgent: false,
pushNotification: true,
emailNotification: false,
enableSchedule: false,
enableExpiry: false
} as FormData)
// UI状态
const typeIndex = ref<number>(0)
const priorityIndex = ref<number>(1)
const scheduleDate = ref<string>('')
const scheduleTime = ref<string>('')
const expiryDate = ref<string>('')
const expiryTime = ref<string>('')
// 优先级选项
const priorityOptions = [
{ label: '低优先级', value: 10 },
{ label: '普通', value: 50 },
{ label: '重要', value: 70 },
{ label: '紧急', value: 90 }
]
const isReply = computed(() => {
return replyToId.value !== ''
})
// 计算属性
const pageTitle = computed(() => {
if (originalMessage.value !== null) {
return isReply.value ? '回复消息' : '转发消息'
}
return '写消息'
})
const canSend = computed(() => {
return formData.value.content.trim() !== '' && formData.value.messageTypeId !== ''
})
// 日期/时间格式化函数,放在顶部,确保所有调用前可用
function formatDate(date: Date): string {
const y = date.getFullYear()
const m = (date.getMonth() + 1).toString().padStart(2, '0')
const d = date.getDate().toString().padStart(2, '0')
return y + '-' + m + '-' + d
}
function formatTime(date: Date): string {
const h = date.getHours().toString().padStart(2, '0')
const min = date.getMinutes().toString().padStart(2, '0')
return h + ':' + min
}
// 加载消息类型
async function loadMessageTypes(): Promise<void> {
const response = await MsgDataServiceReal.getMessageTypes()
if (response.data !== null) {
if (Array.isArray(response.data)) {
const dataArray = response.data as Array<any>
const types: Array<MessageType> = []
for (let i = 0; i < dataArray.length; i++) {
types.push(dataArray[i] as MessageType)
}
messageTypes.value = types
} else {
messageTypes.value = []
}
} else {
messageTypes.value = []
}
if (messageTypes.value.length > 0) {
formData.value.messageTypeId = messageTypes.value[0].id
}
}
// 加载原消息
async function loadOriginalMessage(): Promise<void> {
const messageId = replyToId.value !== '' ? replyToId.value : forwardId.value
if (messageId === '') return
const response = await MsgDataServiceReal.getMessageById(messageId)
if (response.data !== null) {
originalMessage.value = (response.data ?? null) as Message | null
// 预填充表单数据
const msg = originalMessage.value
if (isReply.value) {
formData.value.title = `回复: ${(msg != null && typeof msg.title === 'string') ? msg.title : ''}`
formData.value.receiverId = (msg != null && typeof msg.sender_id === 'string') ? (msg.sender_id as string) : ''
} else {
formData.value.title = `转发: ${(msg != null && typeof msg.title === 'string') ? msg.title : ''}`
formData.value.content = `转发消息:\n\n${(msg != null && typeof msg.content === 'string') ? msg.content : ''}`
}
}
}
// 工具函数
function goBack(): void {
uni.navigateBack()
}
// 事件处理
function onTypeChange(event: UniPickerViewChangeEvent): void {
const index = event.detail.value[0]
typeIndex.value = index
if (index < messageTypes.value.length) {
formData.value.messageTypeId = messageTypes.value[index].id
}
}
function onPriorityChange(event: UniPickerViewChangeEvent): void {
const index = event.detail.value[0]
priorityIndex.value = index
if (index < priorityOptions.length) {
formData.value.priority = parseInt(priorityOptions[index].value.toString())
}
}
function hideReceiverModal(): void {
showReceiverModal.value = false
}
function onScheduleToggle(): void {
if (formData.value.enableSchedule) {
const now = new Date()
scheduleDate.value = formatDate(now)
scheduleTime.value = formatTime(now)
}
}
function onExpiryToggle(): void {
if (formData.value.enableExpiry) {
const tomorrow = new Date()
tomorrow.setDate(tomorrow.getDate() + 1)
expiryDate.value = formatDate(tomorrow)
expiryTime.value = formatTime(tomorrow)
}
}
function onScheduleDateChange(date: string): void {
scheduleDate.value = date
}
function onScheduleTimeChange(time: string): void {
scheduleTime.value = time
}
function onExpiryDateChange(date: string): void {
expiryDate.value = date
}
function onExpiryTimeChange(time: string): void {
expiryTime.value = time
}
function loadReceiverList(): void {
// 这里应该根据receiverTab加载用户或群组列表
receiverList.value = []
}
function selectReceiverItem(receiver:MessageType): void {
formData.value.receiverId = receiver.id
hideReceiverModal()
}
// 发送消息
async function sendMessage(): Promise<void> {
if (!canSend.value) return
// 验证表单
const validationError = MsgUtils.validateSendParams(formData.value.title, formData.value.content)
if (validationError !== null) {
uni.showToast({
title: validationError,
icon: 'none'
})
return
}
loading.value = true
try {
const params: SendMessageParams = {
message_type_id: formData.value.messageTypeId,
receiver_type: 'user', // 根据实际选择确定
receiver_id: formData.value.receiverId === '' ? null : formData.value.receiverId,
title: formData.value.title === '' ? null : formData.value.title,
content: formData.value.content,
priority: formData.value.priority,
is_urgent: formData.value.isUrgent,
push_notification: formData.value.pushNotification,
email_notification: formData.value.emailNotification
}
// 设置定时发送
if (formData.value.enableSchedule && scheduleDate.value !== '' && scheduleTime.value !== '') {
params.scheduled_at = `${scheduleDate.value} ${scheduleTime.value}`
}
// 设置过期时间
if (formData.value.enableExpiry && expiryDate.value !== '' && expiryTime.value !== '') {
params.expires_at = `${expiryDate.value} ${expiryTime.value}`
}
const response = await MsgDataServiceReal.sendMessage(params)
let isSuccess = false
if ( response.status >= 200 && response.status < 300) {
isSuccess = true
}
if (isSuccess) {
uni.showToast({
title: '发送成功',
icon: 'success'
})
setTimeout(() => {
goBack()
}, 1500)
} else {
let errMsg = response.error?.message ?? '发送失败'
uni.showToast({
title: errMsg,
icon: 'none'
})
}
} catch (error) {
uni.showToast({
title: '发送失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
// 接收者选择
function selectReceiver(): void {
showReceiverModal.value = true
loadReceiverList()
}
function switchReceiverTab(tab: string): void {
receiverTab.value = tab
loadReceiverList()
}
function getMessageSummary(content: string | null, maxLength: number): string {
return MsgUtils.getMessageSummary(content, maxLength)
}
// 初始化数据
async function loadInitialData(): Promise<void> {
await loadMessageTypes()
if (replyToId.value !== '' || forwardId.value !== '') {
await loadOriginalMessage()
}
}
// 生命周期
onLoad((options: OnLoadOptions) => {
if (options["replyTo"] !== null) {
const val = options.getString("replyTo")
if (val !== null) {
replyToId.value = val
}
}
if (options["forward"] !== null) {
const val = options.getString("forward")
if (val !== null) {
forwardId.value = val
}
}
loadInitialData()
})
</script>
<style>
.compose-page {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
.nav-bar {
display: flex;
flex-direction: row;
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: 80px;
}
.nav-center {
flex: 1;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.nav-text {
font-size: 16px;
color: #007AFF;
}
.nav-text.disabled {
color: #cccccc;
}
.nav-title {
font-size: 17px;
font-weight: 700;
color: #000000;
}
.form-content {
flex: 1;
padding: 16px;
}
.form-section {
background-color: #ffffff;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
.section-title {
margin-bottom: 12px;
}
.title-text {
font-size: 16px;
font-weight: 700;
color: #333333;
}
.type-picker, .priority-picker {
height: 100px;
}
.picker-text {
font-size: 16px;
color: #333333;
text-align: center;
line-height: 40px;
}
.receiver-input {
display: flex;
align-items: center;
}
.input-field {
flex: 1;
height: 40px;
padding: 0 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
background-color: #f8f8f8;
}
.select-btn {
margin-left: 12px;
padding: 0 16px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
background-color: #2196F3;
border-radius: 6px;
}
.btn-text {
color: #ffffff;
font-size: 14px;
}
.title-input {
width: 100%;
height: 40px;
padding: 0 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
background-color: #f8f8f8;
}
.content-textarea {
width: 100%;
min-height: 120px;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
background-color: #f8f8f8;
line-height: 1.5;
}
.char-count {
margin-top: 8px;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.count-text {
font-size: 12px;
color: #999999;
}
.option-item {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.option-label {
font-size: 16px;
color: #333333;
}
.schedule-time, .expiry-time {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 12px;
}
.original-message {
background-color: #f8f8f8;
border-radius: 6px;
padding: 12px;
}
.original-header {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 8px;
}
.original-sender {
font-size: 14px;
font-weight: 700;
color: #333333;
}
.original-time {
font-size: 12px;
color: #999999;
}
.original-content {
padding: 8px 0;
}
.original-text {
font-size: 14px;
color: #666666;
line-height: 1.4;
}
.receiver-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.modal-content {
background-color: #ffffff;
border-radius: 12px;
width: 90%;
max-width: 400px;
max-height: 80%;
display: flex;
flex-direction: column;
}
.modal-header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e0e0e0;
}
.modal-title {
font-size: 18px;
font-weight: 700;
color: #333333;
}
.modal-close {
width: 30px;
height: 30px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.close-icon {
font-size: 24px;
color: #999999;
}
.modal-body {
flex: 1;
display: flex;
flex-direction: column;
}
.receiver-type-tabs {
display: flex;
flex-direction: row;
border-bottom: 1px solid #e0e0e0;
}
.tab-item {
flex: 1;
height: 44px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.tab-item.active {
border-bottom: 2px solid #2196F3;
}
.tab-text {
font-size: 16px;
color: #666666;
}
.tab-item.active .tab-text {
color: #2196F3;
font-weight: 700;
}
.receiver-list {
flex: 1;
}
.receiver-item {
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
}
.receiver-info {
display: flex;
flex-direction: column;
}
.receiver-name {
font-size: 16px;
color: #333333;
margin-bottom: 4px;
}
.receiver-desc {
font-size: 14px;
color: #999999;
}
</style>

738
pages/msg/detail.uvue Normal file
View File

@@ -0,0 +1,738 @@
<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>

891
pages/msg/index.uvue Normal file
View File

@@ -0,0 +1,891 @@
<template>
<view class="msg-page">
<!-- 使用 scroll-view 替代 list-view 以避免渲染问题 -->
<scroll-view
class="message-container"
:scroll-y="true"
:refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="handleRefresh"
@scrolltolower="loadMoreMessages"
>
<!-- 头部导航 -->
<view class="header-item">
<view class="header">
<view class="header-left">
<text class="title">消息中心</text>
<view class="unread-badge" v-if="unreadCount > 0">
<text class="badge-text">{{ unreadCount > 99 ? '99+' : unreadCount }}</text>
</view>
</view>
<view class="header-right">
<view class="header-btn" @click="handleSearch">
<text class="icon">🔍</text>
</view>
<view class="header-btn" @click="handleRefresh">
<text class="icon" :class="{ 'rotating': loading }">↻</text>
</view>
<view class="header-btn" @click="handleCompose">
<text class="icon">✏️</text>
</view>
</view>
</view>
</view>
<!-- 搜索栏 -->
<view class="search-item" v-if="showSearch">
<view class="search-bar">
<input
class="search-input"
type="text"
placeholder="搜索消息..."
v-model="searchKeyword"
@confirm="performSearch"
/>
<view class="search-btn" @click="performSearch">
<text class="btn-text">搜索</text>
</view>
</view>
</view>
<!-- 消息类型筛选(吸顶效果) -->
<view class="filter-bar-select">
<view class="picker-trigger" @click="showTypeActionSheet">
<text>{{ currentTypeFilterName }}</text>
<text class="picker-arrow">▼</text>
</view>
</view>
<!-- 空状态 -->
<view class="empty-item" v-if="messages.length === 0 && !loading">
<view class="empty-state">
<text class="empty-icon">📪</text>
<text class="empty-text">暂无消息</text>
<view class="empty-btn" @click="handleRefresh">
<text class="btn-text">刷新试试</text>
</view>
</view>
</view>
<!-- 消息项 -->
<view
v-for="(message, index) in messages"
:key="`msg-${message?.id ?? index}-${index}`"
class="message-list-item"
>
<view class="message-item">
<MessageItem
:item="message"
@click="openMessage"
@action="handleMessageAction"
/>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more-item" v-if="hasMore && !loading">
<view class="load-more">
<text class="load-text">上拉加载更多</text>
</view>
</view>
<!-- 加载中 -->
<view class="loading-item" v-if="loading">
<view class="loading">
<text class="loading-text">加载中...</text>
</view>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view class="bottom-bar" v-if="selectionMode">
<view class="bottom-left">
<text class="selected-count">已选择 {{ selectedMessages.length }} 条</text>
</view>
<view class="bottom-right">
<view class="bottom-btn" @click="markSelectedAsRead">
<text class="btn-text">标记已读</text>
</view>
<view class="bottom-btn danger" @click="deleteSelected">
<text class="btn-text">删除</text>
</view>
<view class="bottom-btn" @click="cancelSelection">
<text class="btn-text">取消</text>
</view>
</view>
</view>
<!-- 浮动操作按钮 -->
<view class="fab" @click="handleCompose" v-if="!selectionMode">
<text class="fab-icon">✏️</text>
</view>
</view>
</template>
<script setup lang="uts">
import { MsgDataServiceReal } from '@/utils/msgDataServiceReal.uts'
import { MsgUtils } from '@/utils/msgUtils.uts'
import supa from '@/components/supadb/aksupainstance.uts'
import {getCurrentUserId} from '@/utils/store.uts'
import MessageItem from '@/components/message/MessageItem.uvue'
import {
Message,
MessageWithRecipient,
MessageType,
MessageListParams,
MessageStats
} from '@/utils/msgTypes.uts'
// 定义消息操作数据类型
type MessageActionData = {
action: string
item: MessageWithRecipient
}
// 响应式数据
const messages = ref<Array<MessageWithRecipient>>([])
const messageTypes = ref<Array<MessageType>>([])
const loading = ref<boolean>(false)
const refreshing = ref<boolean>(false)
const hasMore = ref<boolean>(true)
const unreadCount = ref<number>(0)
// UI 状态
const showSearch = ref<boolean>(false)
const searchKeyword = ref<string>('')
const currentTypeFilter = ref<string>('')
const currentTypeFilterName = computed(() => {
if (currentTypeFilter.value == null || currentTypeFilter.value === '') {
return '全部类型'
}
const found = messageTypes.value.find(t => t.code === currentTypeFilter.value)
return found?.name ?? '全部类型'
})
const selectionMode = ref<boolean>(false)
const selectedMessages = ref<Array<string>>([])
const currentPage = ref<number>(1)
const pageSize = ref<number>(20)
// 加载消息列表
async function loadMessages(reset: boolean): Promise<void> {
if (loading.value) return
loading.value = true
try {
if (reset) {
currentPage.value = 1
// 安全地清空数组避免Vue渲染问题
messages.value.splice(0, messages.value.length)
}
const params: MessageListParams = {
limit: pageSize.value,
offset: (currentPage.value - 1) * pageSize.value,
message_type: currentTypeFilter.value === '' ? null : currentTypeFilter.value,
receiver_type: null, // 如有需要可补充
status: null, // 如有需要可补充
is_urgent: null, // 如有需要可补充
search: searchKeyword.value === '' ? null : searchKeyword.value
}
const response = await MsgDataServiceReal.getMessages(params)
console.log('获取消息响应:', response)
if (response.status >= 200 && response.status < 300 && response.data !== null) {
const data = response.data as Array<Message>;
if (!Array.isArray(data)) {
console.error('Invalid data format: expected array but got', typeof data);
return;
}
console.log(`收到 ${data.length} 条消息数据`)
// 现在 response.data 应该已经是 Array<Message> 类型
const newMessages: Array<MessageWithRecipient> = []
// 逐个处理消息,将 Message 转换为 MessageWithRecipient
for (let i = 0; i < data.length; i++) {
const msg: Message = data[i]
// 基本验证
if (msg == null || msg.id == null || msg.id === '') {
console.warn(`跳过无效消息 ${i}:`, msg)
continue
}
// 创建 MessageWithRecipient 对象
const messageItem: MessageWithRecipient = {
// 复制所有 Message 字段
id: msg.id,
message_type_id: msg.message_type_id,
sender_type: msg.sender_type,
sender_id: msg.sender_id,
sender_name: msg.sender_name,
receiver_type: msg.receiver_type,
receiver_id: msg.receiver_id,
title: msg.title,
content: msg.content,
content_type: msg.content_type,
attachments: msg.attachments,
media_urls: msg.media_urls,
metadata: msg.metadata,
device_data: msg.device_data,
location_data: msg.location_data,
priority: msg.priority,
expires_at: msg.expires_at,
is_broadcast: msg.is_broadcast,
is_urgent: msg.is_urgent,
conversation_id: msg.conversation_id,
parent_message_id: msg.parent_message_id,
thread_count: msg.thread_count,
status: msg.status,
total_recipients: msg.total_recipients,
delivered_count: msg.delivered_count,
read_count: msg.read_count,
reply_count: msg.reply_count,
delivery_options: msg.delivery_options,
push_notification: msg.push_notification,
email_notification: msg.email_notification,
sms_notification: msg.sms_notification,
created_at: msg.created_at,
updated_at: msg.updated_at,
scheduled_at: msg.scheduled_at,
delivered_at: msg.delivered_at,
// 添加 MessageWithRecipient 扩展字段
is_read: false,
is_starred: false,
is_archived: false,
is_deleted: false,
read_at: null,
replied_at: null,
message_type: msg.message_type_id,
recipients: null
}
newMessages.push(messageItem)
}
console.log(`成功处理 ${newMessages.length} 条消息`)
// 安全更新消息数组
if (reset) {
// 先清空,再添加
messages.value.splice(0, messages.value.length)
messages.value.push(...newMessages)
} else {
// 直接添加新消息
messages.value.push(...newMessages)
}
hasMore.value = response.hasmore ?? false
if (newMessages.length > 0) {
currentPage.value = currentPage.value + 1
}
} else {
let errMsg = '加载失败'
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'
})
}
} catch (error) {
console.log(error)
uni.showToast({
title: '加载失败',
icon: 'none'
})
} finally {
loading.value = false
refreshing.value = false
}
}
// 加载统计信息
async function loadStats(): Promise<void> {
const response = await MsgDataServiceReal.getMessageStats(getCurrentUserId())
if (response.status >= 200 && response.status < 300 && response.data !== null) {
unreadCount.value = ((response.data as MessageStats)?.unread_messages ?? 0)
}
}
// 刷新消息
async function handleRefresh(): Promise<void> {
refreshing.value = true
await loadMessages(true)
await loadStats()
}
// 加载更多消息
async function loadMoreMessages(): Promise<void> {
if (hasMore.value && !loading.value) {
await loadMessages(false)
}
}
// 搜索功能
function handleSearch(): void {
showSearch.value = !showSearch.value
if (!showSearch.value) {
searchKeyword.value = ''
handleRefresh()
}
}
async function performSearch(): Promise<void> {
await loadMessages(true)
}
// 类型筛选
async function filterByType(typeCode: string): Promise<void> {
currentTypeFilter.value = typeCode
await loadMessages(true)
}
// 显示类型选择操作表
function showTypeActionSheet(): void {
const options = [{ name: '全部类型', code: '' }, ...messageTypes.value.map(t => ({ name: t.name, code: t.code }))]
let actopt = options.map(o => o.name)
uni.showActionSheet({
itemList: actopt,
success: function(res) {
const idx = res.tapIndex ?? 0
// 防止索引越界
if (idx >= 0 && idx < options.length) {
const selected = options[idx]
currentTypeFilter.value = selected.code
loadMessages(true)
} else {
console.error('ActionSheet 选择索引越界:', idx, '数组长度:', options.length)
}
}
})
}
// 选择模式
function toggleSelection(messageId: string): void {
const index = selectedMessages.value.indexOf(messageId)
if (index > -1) {
// 更安全的数组移除方式,防止索引越界
try {
if (index >= 0 && index < selectedMessages.value.length) {
selectedMessages.value.splice(index, 1)
}
} catch (error) {
console.error('移除选中项时出错:', error)
// 回退到过滤方式
selectedMessages.value = selectedMessages.value.filter(id => id !== messageId)
}
} else {
// 确保不重复添加
if (!selectedMessages.value.includes(messageId)) {
selectedMessages.value.push(messageId)
}
}
}
// 打开消息详情
function openMessage(message: MessageWithRecipient): void {
if (selectionMode.value) {
toggleSelection(message.id)
return
}
uni.navigateTo({
url: `/pages/msg/detail?id=${message.id}`
})
}
// 标记消息为已读
async function markMessageAsRead(messageId: string): Promise<void> {
try {
const response = await MsgDataServiceReal.batchOperation([messageId], 'read', getCurrentUserId())
if (response.status >= 200 && response.status < 300) {
// 更新本地消息状态
const message = messages.value.find(m => m.id === messageId)
if (message != null) {
message.is_read = true
unreadCount.value = Math.max(0, unreadCount.value - 1)
}
}
} catch (error) {
console.error('标记已读失败:', error)
}
}
// 切换消息收藏状态
async function toggleMessageStar(messageId: string): Promise<void> {
try {
const message = messages.value.find(m => m.id === messageId)
if (message == null) return
const action = (message.is_starred ?? false) ? 'unstar' : 'star'
const response = await MsgDataServiceReal.batchOperation([messageId], action, getCurrentUserId())
if (response.status >= 200 && response.status < 300) {
message.is_starred = !(message.is_starred ?? false)
}
} catch (error) {
console.error('切换收藏状态失败:', error)
}
}
// 删除消息
async function deleteMessage(messageId: string): Promise<void> {
try {
const response = await MsgDataServiceReal.batchOperation([messageId], 'delete', getCurrentUserId())
if (response.status >= 200 && response.status < 300) {
// 使用过滤方式删除消息,避免索引越界问题
try {
const index = messages.value.findIndex(m => m.id === messageId)
if (index >= 0 && index < messages.value.length) {
messages.value.splice(index, 1)
}
} catch (error) {
console.error('删除消息时索引越界:', error)
// 回退到过滤方式
messages.value = messages.value.filter(m => m.id !== messageId)
}
}
} catch (error) {
console.error('删除消息失败:', error)
}
}
// 归档消息
async function archiveMessage(messageId: string): Promise<void> {
try {
const response = await MsgDataServiceReal.batchOperation([messageId], 'archive', getCurrentUserId())
if (response.status >= 200 && response.status < 300) {
const message = messages.value.find(m => m.id === messageId)
if (message != null) {
message.is_archived = true
}
}
} catch (error) {
console.error('归档消息失败:', error)
}
}
// 处理消息操作
function handleMessageAction(data: MessageActionData): void {
const action = data.action
const item = data.item
const messageId = item.id
switch (action) {
case 'read':
markMessageAsRead(messageId)
break
case 'star':
toggleMessageStar(messageId)
break
case 'delete':
deleteMessage(messageId)
break
case 'archive':
archiveMessage(messageId)
break
default:
console.log('未知操作:', action)
}
}
function cancelSelection(): void {
selectionMode.value = false
selectedMessages.value = []
}
// 批量操作
async function markSelectedAsRead(): Promise<void> {
if (selectedMessages.value.length === 0) return
try { const response = await MsgDataServiceReal.batchOperation(
selectedMessages.value,
'read',
getCurrentUserId()
)
if (response.status >= 200 && response.status < 300) {
uni.showToast({
title: '已标记为已读',
icon: 'success'
})
await loadStats()
await loadMessages(true)
} else {
let errMsg = '操作失败'
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'
})
}
} catch (error) {
uni.showToast({
title: '操作失败',
icon: 'none'
})
}
cancelSelection()
}
async function deleteSelected(): Promise<void> {
if (selectedMessages.value.length === 0) return
uni.showModal({
title: '确认删除',
content: `确定要删除选中的 ${selectedMessages.value.length} 条消息吗?`,
success: (res) => {
if (res.confirm) {
MsgDataServiceReal.batchOperation(
selectedMessages.value,
'delete',
getCurrentUserId()
).then(response => {
if (response.status >= 200 && response.status < 300) {
uni.showToast({
title: '删除成功',
icon: 'success'
})
loadStats()
loadMessages(true)
} else {
let errMsg = '删除失败'
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'
})
}
cancelSelection()
}).catch(error => {
uni.showToast({
title: '删除失败',
icon: 'none'
})
cancelSelection()
})
}
}
})
}
// 加载消息类型
async function loadMessageTypes(): Promise<void> {
const response = await MsgDataServiceReal.getMessageTypes()
if (response.status >= 200 && response.status < 300 && Array.isArray(response.data)) {
const dataArray = response.data as Array<any>
const types: Array<MessageType> = []
for (let i = 0; i < dataArray.length; i++) {
types.push(dataArray[i] as MessageType)
}
messageTypes.value = types
} else {
messageTypes.value = []
}
}
// 写消息
function handleCompose(): void {
uni.navigateTo({
url: '/pages/msg/compose'
})
}
// 初始化加载
async function loadInitialData(): Promise<void> {
// 消息数据服务现在直接使用 aksupainstance.uts 中的 supa 实例,无需手动初始化
await loadMessageTypes()
await loadMessages(true)
await loadStats()
}
// 生命周期
onMounted(() => {
loadInitialData()
})
</script>
<style>
.msg-page {
display: flex;
flex-direction: column;
background-color: #f5f5f5;
flex: 1;
width: 100%;
height: 100vh;
}
.message-container {
flex: 1;
background-color: #f5f5f5;
}
.header-item {
background-color: #ffffff;
}
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.header-left {
display: flex;
flex-direction: row;
align-items: center;
}
.title {
font-size: 18px;
font-weight: bold;
color: #333333;
}
.unread-badge {
margin-left: 8px;
background-color: #ff4757;
border-radius: 10px;
min-width: 20px;
height: 20px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.badge-text {
color: #ffffff;
font-size: 12px;
}
.header-right {
display: flex;
align-items: center;
flex-direction: row;
}
.header-btn {
width: 40px;
height: 40px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-left: 8px;
}
.icon {
font-size: 18px;
color: #666666;
}
.rotating {
transform: rotate(360deg);
}
.search-item {
background-color: #ffffff;
}
.search-bar {
display: flex;
flex-direction: row;
padding: 12px 16px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.search-input {
flex: 1;
height: 36px;
padding: 0 12px;
border: 1px solid #e0e0e0;
border-radius: 18px;
background-color: #f8f8f8;
}
.search-btn {
margin-left: 12px;
padding: 0 16px;
height: 36px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background-color: #2196F3;
border-radius: 18px;
}
.btn-text {
color: #ffffff;
font-size: 14px;
}
/* 吸顶筛选条样式 */
.filter-bar-select {
display: flex;
flex-direction: row;
align-items: center;
padding: 12px 16px;
background-color: #ffffff;
border-bottom: 1px solid #e0e0e0;
}
.picker-trigger {
flex: 1;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
border: 1px solid #e0e0e0;
border-radius: 18px;
background-color: #f8f8f8;
}
.picker-arrow {
font-size: 14px;
color: #666666;
}
.empty-item {
background-color: #f5f5f5;
}
.empty-state {
padding: 80px 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-text {
font-size: 16px;
color: #999999;
margin-bottom: 20px;
}
.empty-btn {
padding: 8px 20px;
background-color: #2196F3;
border-radius: 20px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.message-list-item {
background-color: #ffffff;
}
.message-item {
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
}
.load-more-item, .loading-item {
background-color: #f5f5f5;
}
.load-more, .loading {
padding: 20px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.load-text, .loading-text {
color: #999999;
font-size: 14px;
}
.bottom-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #ffffff;
border-top: 1px solid #e0e0e0;
}
.selected-count {
color: #666666;
font-size: 14px;
}
.bottom-right {
display: flex;
flex-direction: row;
}
.bottom-btn {
padding: 6px 12px;
margin-left: 8px;
border-radius: 4px;
background-color: #2196F3;
}
.bottom-btn.danger {
background-color: #ff4757;
}
.fab {
position: absolute;
bottom: 80px;
right: 20px;
width: 56px;
height: 56px;
border-radius: 28px;
background-color: #2196F3;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
z-index: 999;
}
.fab-icon {
color: #ffffff;
font-size: 24px;
}
</style>