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>