babyhealth/pages/message/message.vue
2026-02-06 20:21:10 +08:00

1018 lines
22 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
class="tab-item"
:class="{ active: activeTab === 'addressList' }"
@click="switchTab('addressList')"
>
<text>通讯录</text>
</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>
<view v-if="activeTab === 'addressList'" class="address-list">
<view v-if="Object.keys(groupedAddressList).length === 0" class="address-empty">
<text>暂无联系人</text>
</view>
<view v-else>
<view
v-for="(group, letter) in groupedAddressList"
:key="letter"
class="address-group"
:id="`group-${letter}`"
>
<view class="group-header">
<text class="group-letter">{{ letter }}</text>
</view>
<view
class="address-item"
v-for="(person, index) in group"
:key="person.id"
>
<view class="address-avatar">
<image :src="person.avatar" class="avatar-img" mode="aspectFill" />
</view>
<view class="address-info">
<view class="address-name">{{ person.name }}</view>
<view class="address-role">{{ person.role }}</view>
</view>
<view class="address-actions">
<view class="address-phone" v-if="person.phone">
<i class="fas fa-phone"></i>
<text>{{ person.phone }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 右侧字母导航 -->
<view class="alphabet-nav">
<view
class="alphabet-item"
v-for="letter in alphabetList"
:key="letter"
:class="{ active: groupedAddressList[letter] }"
@click="scrollToGroup(letter)"
>
<text>{{ letter }}</text>
</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, computed } 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 addressList = reactive([
{
id: 1,
name: "张三",
avatar: "/static/avatar/zhangsan.png",
role: "人事部·经理",
phone: "13800138000",
firstLetter: "Z"
},
{
id: 2,
name: "李四",
avatar: "/static/avatar/lisi.png",
role: "财务部·财务专员",
phone: "13800138001",
firstLetter: "L"
},
{
id: 3,
name: "王五",
avatar: "/static/avatar/wangwu.png",
role: "技术部·前端开发",
phone: "13800138002",
firstLetter: "W"
},
{
id: 4,
name: "陈六",
avatar: "/static/avatar/chenliu.png",
role: "技术部·后端开发",
phone: "13800138003",
firstLetter: "C"
},
{
id: 5,
name: "刘七",
avatar: "/static/avatar/liuqi.png",
role: "市场部·经理",
phone: "13800138004",
firstLetter: "L"
},
{
id: 6,
name: "赵八",
avatar: "/static/avatar/zhaoba.png",
role: "运营部·专员",
phone: "13800138005",
firstLetter: "Z"
},
{
id: 7,
name: "孙九",
avatar: "/static/avatar/sunjiu.png",
role: "设计部·UI设计师",
phone: "13800138006",
firstLetter: "S"
},
{
id: 8,
name: "周十",
avatar: "/static/avatar/zhoushi.png",
role: "人事部·专员",
phone: "13800138007",
firstLetter: "Z"
},
{
id: 9,
name: "吴十一",
avatar: "/static/avatar/wushiyi.png",
role: "财务部·出纳",
phone: "13800138008",
firstLetter: "W"
},
{
id: 10,
name: "郑十二",
avatar: "/static/avatar/zhengshier.png",
role: "技术部·测试工程师",
phone: "13800138009",
firstLetter: "Z"
}
]);
// 方法
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 closeAllSwipes = () => {};
// 通讯录按首字母分组
const groupedAddressList = computed(() => {
const groups = {};
addressList.forEach(person => {
const letter = person.firstLetter;
if (!groups[letter]) {
groups[letter] = [];
}
groups[letter].push(person);
});
// 按字母顺序排序
const sortedGroups = {};
Object.keys(groups).sort().forEach(letter => {
sortedGroups[letter] = groups[letter].sort((a, b) => a.name.localeCompare(b.name));
});
return sortedGroups;
});
// 字母列表
const alphabetList = ref(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']);
// 滚动到指定分组
const scrollToGroup = (letter) => {
if (groupedAddressList.value[letter]) {
const query = uni.createSelectorQuery();
query.select(`#group-${letter}`).boundingClientRect();
query.exec((res) => {
if (res[0]) {
uni.pageScrollTo({
scrollTop: res[0].top - 100,
duration: 300
});
}
});
}
};
// 菜单相关方法
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,
addressList,
groupedAddressList,
alphabetList,
showMenu,
currentChat,
currentIndex,
handleSearch,
handleNotification,
switchTab,
openChat,
openNotification,
closeAllSwipes,
scrollToGroup,
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;
}
/* 通讯录样式 */
.address-list {
padding: 20rpx 30rpx;
padding-right: 90rpx;
}
.address-item {
display: flex;
align-items: center;
background: var(--white);
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: var(--shadow);
border: 1rpx solid var(--border-light);
}
.address-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
margin-right: 20rpx;
}
.avatar-img {
width: 100%;
height: 100%;
display: block;
}
.address-info {
flex: 1;
min-width: 0;
}
.address-name {
font-size: 30rpx;
font-weight: 600;
color: var(--text-color);
margin-bottom: 6rpx;
}
.address-role {
font-size: 24rpx;
color: var(--text-secondary);
}
.address-actions {
display: flex;
align-items: center;
gap: 12rpx;
}
.address-phone {
display: inline-flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 12rpx;
background: var(--gray-lighter);
border-radius: 12rpx;
color: var(--text-secondary);
font-size: 24rpx;
}
.address-empty {
text-align: center;
color: var(--text-muted);
font-size: 26rpx;
padding: 60rpx 20rpx;
}
/* 通讯录分组样式 */
.address-group {
margin-bottom: 30rpx;
}
.group-header {
background: var(--gray-lighter);
padding: 16rpx 24rpx;
margin-bottom: 12rpx;
border-radius: 8rpx;
position: sticky;
top: 0;
z-index: 10;
}
.group-letter {
font-size: 28rpx;
font-weight: 600;
color: var(--primary-color);
}
/* 右侧字母导航 */
.alphabet-nav {
position: fixed;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
z-index: 100;
display: flex;
flex-direction: column;
gap: 4rpx;
}
.alphabet-item {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(0, 0, 0, 0.1);
font-size: 20rpx;
color: var(--text-secondary);
transition: all 0.2s ease;
}
.alphabet-item.active {
background: var(--primary-color);
color: var(--white);
}
.alphabet-item:active {
transform: scale(0.9);
}
</style>