Initial commit of akmon project
This commit is contained in:
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