Initial commit of akmon project
This commit is contained in:
850
pages/msg/compose.uvue
Normal file
850
pages/msg/compose.uvue
Normal 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
738
pages/msg/detail.uvue
Normal 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
891
pages/msg/index.uvue
Normal 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>
|
||||
Reference in New Issue
Block a user