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

851 lines
19 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>