892 lines
21 KiB
Plaintext
892 lines
21 KiB
Plaintext
<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>
|