2025-10-27 23:13:08 +08:00

684 lines
13 KiB
Vue
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="message-page">
<!-- 统一顶部导航 -->
<view class="unified-header">
<view class="header-content">
<view class="header-left">
<!-- <i class="fas fa-search header-icon" @click="handleSearch"></i> -->
</view>
<view class="header-title">消息</view>
<view class="header-right">
<!-- <i class="fas fa-bell header-icon" @click="handleNotification">
<view class="badge" v-if="unreadCount > 0">{{ unreadCount }}</view>
</i> -->
</view>
</view>
</view>
<!-- 分类标签 -->
<view class="tabs-container">
<view class="tabs">
<view
class="tab-item"
:class="{ active: activeTab === 'chat' }"
@click="switchTab('chat')"
>
<text>会话</text>
<view class="tab-badge" v-if="chatUnreadCount > 0">{{ chatUnreadCount }}</view>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'notification' }"
@click="switchTab('notification')"
>
<text>通知</text>
<view class="tab-badge" v-if="notificationUnreadCount > 0">{{ notificationUnreadCount }}</view>
</view>
</view>
</view>
<!-- 页面内容 -->
<scroll-view scroll-y class="unified-content" @click="closeAllSwipes">
<!-- 会话列表 -->
<view v-if="activeTab === 'chat'" class="chat-list">
<view
class="chat-item"
v-for="(chat, index) in chatList"
:key="index"
>
<!-- 左侧点击区域 -->
<view class="chat-left" @click="openChat(chat)">
<view class="chat-avatar">
<view class="avatar">
<i class="fas fa-user avatar-icon"></i>
</view>
<view class="chat-badge" v-if="chat.unread > 0">{{ chat.unread }}</view>
</view>
<view class="chat-content">
<view class="chat-header">
<text class="chat-name">{{chat.name}}</text>
<text class="chat-time">{{chat.time}}</text>
</view>
<view class="chat-preview">
<text class="chat-message">{{chat.lastMessage}}</text>
</view>
</view>
</view>
<!-- 右侧操作区域 -->
<view class="chat-right">
<view class="more-btn" @click="showActionMenu(chat, index)">
<i class="fas fa-ellipsis-v more-icon"></i>
</view>
</view>
</view>
</view>
<!-- 通知列表 -->
<view v-if="activeTab === 'notification'" class="notification-list">
<view
class="notification-item"
v-for="(notification, index) in notificationList"
:key="index"
@click="openNotification(notification)"
>
<view class="notification-icon" :style="{ backgroundColor: notification.color + '20' }">
<i :class="notification.iconClass" class="notification-icon-fa" :style="{ color: notification.color }"></i>
</view>
<view class="notification-content">
<view class="notification-header">
<text class="notification-title">{{notification.title}}</text>
<text class="notification-time">{{notification.time}}</text>
</view>
<view class="notification-preview">
<text class="notification-message">{{notification.content}}</text>
</view>
<view class="notification-source">
<text>{{notification.source}}</text>
</view>
</view>
<view class="notification-status" v-if="!notification.read">
<view class="unread-dot"></view>
</view>
</view>
</view>
</scroll-view>
<!-- 操作菜单弹窗 -->
<view class="action-menu" v-if="showMenu" @click="hideActionMenu">
<view class="menu-mask"></view>
<view class="menu-content" @click.stop>
<view class="menu-item" @click="pinChat(currentChat, currentIndex)">
<i class="fas fa-thumbtack menu-icon"></i>
<text>{{ currentChat && currentChat.pinned ? '取消置顶' : '置顶' }}</text>
</view>
<view class="menu-item delete-item" @click="deleteChat(currentChat, currentIndex)">
<i class="fas fa-trash menu-icon"></i>
<text>删除</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { ref, reactive } from 'vue'
export default {
setup() {
// 响应式数据
const activeTab = ref('chat')
const unreadCount = ref(5)
const chatUnreadCount = ref(3)
const notificationUnreadCount = ref(2)
// 会话列表数据
const chatList = reactive([
{
id: 1,
name: '技术部群聊',
avatar: '/static/avatar/group1.png',
lastMessage: '张工:项目进度如何?',
time: '10:30',
unread: 2,
pinned: false
},
{
id: 2,
name: '李经理',
avatar: '/static/avatar/manager.png',
lastMessage: '好的,我马上处理',
time: '09:45',
unread: 1,
pinned: true
},
{
id: 3,
name: '财务部群聊',
avatar: '/static/avatar/group2.png',
lastMessage: '报销单据已审核',
time: '昨天',
unread: 0,
pinned: false
}
])
// 菜单相关数据
const showMenu = ref(false)
const currentChat = ref(null)
const currentIndex = ref(-1)
// 通知列表数据
const notificationList = reactive([
{
id: 1,
title: '审批通知',
content: '您的请假申请已通过部门经理审批',
source: '人事部',
time: '2小时前',
iconClass: 'fas fa-file-alt',
color: '#10b981',
read: false
},
{
id: 2,
title: '考勤通知',
content: '今日考勤打卡成功上班时间09:00',
source: '考勤系统',
time: '3小时前',
iconClass: 'fas fa-clock',
color: '#3b82f6',
read: false
},
{
id: 3,
title: '系统公告',
content: '系统将于今晚22:00-24:00进行维护升级',
source: 'IT部门',
time: '1天前',
iconClass: 'fas fa-info-circle',
color: '#8b5cf6',
read: true
}
])
// 方法
const handleSearch = () => {
uni.showToast({
title: '搜索功能',
icon: 'none'
})
}
const handleNotification = () => {
uni.showToast({
title: '通知设置',
icon: 'none'
})
}
const switchTab = (tab) => {
activeTab.value = tab
}
const openChat = (chat) => {
// 跳转到 chat.vue 页面,并传递会话 ID
uni.navigateTo({
// url: `/pages/message/chat/chat?id=${chat.id}`
url: `/pages/message/chat`
})
}
const openNotification = (notification) => {
uni.showToast({
title: `查看通知:${notification.title}`,
icon: 'none'
})
}
// 菜单相关方法
const showActionMenu = (chat, index) => {
currentChat.value = chat
currentIndex.value = index
showMenu.value = true
}
const hideActionMenu = () => {
showMenu.value = false
currentChat.value = null
currentIndex.value = -1
}
// 操作按钮方法
const pinChat = (chat, index) => {
chat.pinned = !chat.pinned
hideActionMenu()
// 重新排序:置顶的放在前面
const pinnedChats = chatList.filter(item => item.pinned)
const unpinnedChats = chatList.filter(item => !item.pinned)
// 清空原数组并重新添加
chatList.splice(0, chatList.length, ...pinnedChats, ...unpinnedChats)
uni.showToast({
title: chat.pinned ? '已置顶' : '已取消置顶',
icon: 'success'
})
}
const deleteChat = (chat, index) => {
hideActionMenu()
uni.showModal({
title: '确认删除',
content: `确定要删除与"${chat.name}"的会话吗?`,
success: (res) => {
if (res.confirm) {
chatList.splice(index, 1)
uni.showToast({
title: '已删除',
icon: 'success'
})
}
}
})
}
return {
activeTab,
unreadCount,
chatUnreadCount,
notificationUnreadCount,
chatList,
notificationList,
showMenu,
currentChat,
currentIndex,
handleSearch,
handleNotification,
switchTab,
openChat,
openNotification,
showActionMenu,
hideActionMenu,
pinChat,
deleteChat
}
}
}
</script>
<style lang="scss" scoped>
.message-page {
height: 100vh;
background-color: var(--background);
position: relative;
padding-top: calc(var(--status-bar-height) + 88rpx);
}
/* 支持安全区域的设备 */
@supports (padding: max(0px)) {
.message-page {
padding-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
}
}
.navbar-content {
display: flex;
align-items: center;
justify-content: space-between;
}
.search-box {
flex: 1;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 25rpx;
padding: 15rpx 20rpx;
margin-right: 20rpx;
display: flex;
align-items: center;
}
.search-icon {
font-size: 32rpx;
margin-right: 10rpx;
color: rgba(255, 255, 255, 0.8);
}
.search-placeholder {
color: rgba(255, 255, 255, 0.8);
font-size: 28rpx;
}
.notification {
position: relative;
padding: 10rpx;
}
.notification-icon {
font-size: 40rpx;
color: var(--white);
}
.badge {
position: absolute;
top: 0rpx;
right: 15rpx;
background-color: var(--error);
color: var(--white);
font-size: 20rpx;
padding: 2rpx 8rpx;
border-radius: 50%;
// min-width: 30rpx;
text-align: center;
line-height: 1.2;
}
.tabs-container {
background: var(--white);
padding: 0 30rpx;
border-bottom: 1rpx solid var(--border-light);
}
.tabs {
display: flex;
}
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0;
position: relative;
font-size: 28rpx;
color: var(--text-secondary);
}
.tab-item.active {
color: var(--primary-color);
font-weight: 600;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 4rpx;
background-color: var(--primary-color);
border-radius: 2rpx;
}
.tab-badge {
position: absolute;
top: 20rpx;
right: 80rpx;
background-color: var(--error);
color: var(--white);
font-size: 20rpx;
padding: 2rpx 8rpx;
border-radius: 20rpx;
min-width: 30rpx;
text-align: center;
line-height: 1.2;
}
.page-content {
height: calc(100vh - 200rpx);
}
.chat-list, .notification-list {
padding: 20rpx 30rpx;
}
.chat-item, .notification-item {
background: var(--white);
border-radius: 16rpx;
// padding: 30rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
box-shadow: var(--shadow);
}
.notification-item{
padding: 30rpx;
}
.chat-left {
flex: 1;
display: flex;
align-items: center;
padding: 30rpx;
border-right: 1rpx solid var(--border-light);
}
.chat-right {
padding: 30rpx;
}
.more-btn {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: background-color 0.2s ease;
}
.more-btn:active {
background-color: var(--gray-lighter);
}
.chat-avatar {
position: relative;
margin-right: 20rpx;
}
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
background: var(--primary-color);
display: flex;
align-items: center;
justify-content: center;
}
.avatar-icon {
color: var(--white);
font-size: 32rpx;
}
.chat-badge {
position: absolute;
top: -5rpx;
right: -5rpx;
background-color: var(--error);
color: var(--white);
font-size: 20rpx;
padding: 2rpx 8rpx;
border-radius: 20rpx;
min-width: 30rpx;
text-align: center;
line-height: 1.2;
}
.more-icon {
font-size: 32rpx;
color: var(--text-muted);
}
.chat-content {
flex: 1;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
}
.chat-name {
font-size: 30rpx;
font-weight: 600;
color: var(--text-color);
}
.chat-time {
font-size: 24rpx;
color: var(--text-muted);
}
.chat-preview {
margin-bottom: 10rpx;
}
.chat-message {
font-size: 26rpx;
color: var(--text-secondary);
}
.chat-actions {
padding: 10rpx;
}
.notification-icon {
width: 60rpx;
height: 60rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.notification-icon-fa {
font-size: 40rpx;
}
.unread-dot {
width: 16rpx;
height: 16rpx;
background-color: var(--error);
border-radius: 50%;
}
.notification-content {
flex: 1;
}
.notification-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
}
.notification-title {
font-size: 30rpx;
font-weight: 600;
color: var(--text-color);
}
.notification-time {
font-size: 24rpx;
color: var(--text-muted);
}
.notification-preview {
margin-bottom: 10rpx;
}
.notification-message {
font-size: 26rpx;
color: var(--text-secondary);
}
.notification-source {
margin-bottom: 10rpx;
}
.notification-source text {
font-size: 22rpx;
color: var(--text-muted);
background: var(--gray-lighter);
padding: 4rpx 12rpx;
border-radius: 12rpx;
}
.notification-status {
padding: 10rpx;
}
/* 操作菜单弹窗 */
.action-menu {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: flex-end;
justify-content: center;
}
.menu-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.menu-content {
position: relative;
background: var(--white);
border-radius: 20rpx 20rpx 0 0;
padding: 40rpx 0 20rpx;
width: 100%;
max-width: 750rpx;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
.menu-item {
display: flex;
align-items: center;
padding: 30rpx 40rpx;
font-size: 32rpx;
color: var(--text-color);
transition: background-color 0.2s ease;
}
.menu-item:active {
background-color: var(--gray-lighter);
}
.menu-item.delete-item {
color: var(--error);
}
.menu-icon {
font-size: 36rpx;
margin-right: 20rpx;
width: 40rpx;
text-align: center;
}
</style>