This commit is contained in:
wangdongbo 2026-01-30 13:52:35 +08:00
parent 3316e1bfa0
commit d6851d72f2
6 changed files with 505 additions and 249 deletions

View File

@ -16,6 +16,76 @@ $primary-color: #0877F1;
background-color: #f5f5f5;
}
/* 患者信息栏样式 */
.patient-info-bar {
position: relative;
background: #fff;
border-bottom: 1rpx solid #f0f0f0;
padding: 20rpx 32rpx;
z-index: 10;
flex-shrink: 0; /* 防止被压缩 */
}
.patient-info-content {
display: flex;
align-items: center;
justify-content: space-between;
}
.patient-basic-info {
display: flex;
align-items: center;
gap: 16rpx;
flex: 1;
min-width: 0; /* 允许文字截断 */
}
.patient-name {
font-size: 32rpx;
color: #333;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200rpx;
}
.patient-detail {
font-size: 28rpx;
color: #999;
white-space: nowrap;
flex-shrink: 0;
}
.patient-detail-btn {
display: flex;
align-items: center;
gap: 4rpx;
padding: 12rpx 24rpx;
background: linear-gradient(270deg, #1b5cc8 2.26%, #0877f1 94.33%);
border-radius: 40rpx;
transition: all 0.2s;
flex-shrink: 0; /* 防止按钮被压缩 */
}
.patient-detail-btn:active {
opacity: 0.8;
transform: scale(0.98);
}
.detail-btn-text {
font-size: 26rpx;
color: #fff;
font-weight: 500;
}
.arrow-icon {
font-size: 32rpx;
color: #fff;
font-weight: 600;
line-height: 1;
}
.chat-content {
flex: 1;
box-sizing: border-box;

View File

@ -0,0 +1,317 @@
<template>
<view class="header-container">
<view class="header-content">
<!-- 团队选择器 -->
<view class="team-selector" @click="showTeamPicker = true">
<text class="team-name">{{ currentTeamName }}</text>
<image class="arrow-icon" src="/static/zhuanhua.svg" mode="aspectFit" />
</view>
<!-- 右侧操作按钮 -->
<view class="header-actions">
<view class="action-btn" @click="handleAddPatient">
<image
class="invite-icon"
src="/static/work/qrcode.svg"
mode="aspectFit"
/>
<text class="action-text">邀请患者</text>
</view>
</view>
</view>
<!-- 标签页切换 -->
<view class="tabs-container">
<view
class="tab-item"
:class="{ active: activeTab === 'processing' }"
@click="handleTabChange('processing')"
>
<text class="tab-text">处理中</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'finished' }"
@click="handleTabChange('finished')"
>
<text class="tab-text">已结束</text>
</view>
</view>
<!-- 团队选择弹窗 -->
<view
v-if="showTeamPicker"
class="team-picker-overlay"
@click="showTeamPicker = false"
>
<view class="team-picker-content" @click.stop>
<view class="team-picker-header">
<text class="picker-title">选择团队</text>
</view>
<scroll-view class="team-list" scroll-y>
<view
v-for="team in teamList"
:key="team.teamId"
class="team-item"
:class="{ active: currentTeamId === team.teamId }"
@click="selectTeam(team)"
>
<text class="team-item-name">{{ team.name }}</text>
<text v-if="currentTeamId === team.teamId" class="check-icon"></text>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from "vue";
import { storeToRefs } from "pinia";
import useTeamStore from "@/store/team.js";
// Props
const props = defineProps({
activeTab: {
type: String,
default: "processing",
},
});
// Emits
const emit = defineEmits(["update:activeTab", "teamChange", "addPatient"]);
//
const teamStore = useTeamStore();
const { teams } = storeToRefs(teamStore);
//
const showTeamPicker = ref(false);
const currentTeamId = ref(""); // ""
// ""
const teamList = computed(() => {
const allOption = { teamId: "", name: "全部会话消息" };
return [allOption, ...(teams.value || [])];
});
//
const currentTeamName = computed(() => {
if (!currentTeamId.value) return "全部会话消息";
const team = teams.value.find((t) => t.teamId === currentTeamId.value);
return team ? team.name : "全部会话消息";
});
//
const selectTeam = (team) => {
currentTeamId.value = team.teamId;
showTeamPicker.value = false;
console.log("切换到团队:", team.name);
emit("teamChange", team.teamId);
};
//
const handleTabChange = (tab) => {
if (props.activeTab === tab) return;
emit("update:activeTab", tab);
};
//
const handleAddPatient = () => {
emit("addPatient");
};
</script>
<style scoped lang="scss">
.header-container {
background-color: #fff;
flex-shrink: 0;
}
.header-content {
display: flex;
align-items: center;
padding: 16rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.team-selector {
flex: 1;
display: flex;
align-items: center;
min-width: 0;
cursor: pointer;
&:active {
opacity: 0.7;
}
}
.team-name {
font-size: 32rpx;
font-weight: 500;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.arrow-icon {
width: 24rpx;
height: 24rpx;
margin-left: 8rpx;
flex-shrink: 0;
opacity: 0.6;
filter: brightness(0.5);
}
.header-actions {
display: flex;
align-items: center;
gap: 16rpx;
margin-left: 16rpx;
flex-shrink: 0;
}
.action-btn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 6rpx 12rpx;
// background-color: #f5f5f5;
border-radius: 8rpx;
&:active {
opacity: 0.7;
}
}
.invite-icon {
width: 32rpx;
height: 32rpx;
margin-bottom: 4rpx;
}
.action-text {
font-size: 20rpx;
color: #666;
line-height: 1.4;
text-align: center;
}
.tabs-container {
display: flex;
padding: 0 32rpx;
gap: 48rpx;
}
.tab-item {
position: relative;
padding: 20rpx 0;
cursor: pointer;
&.active {
.tab-text {
color: #1890ff;
font-weight: 500;
}
&::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 48rpx;
height: 4rpx;
background-color: #1890ff;
border-radius: 2rpx;
}
}
}
.tab-text {
font-size: 28rpx;
color: #666;
transition: color 0.3s;
}
.team-picker-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 200rpx;
}
.team-picker-content {
width: 600rpx;
max-height: 800rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
display: flex;
flex-direction: column;
}
.team-picker-header {
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
text-align: center;
}
.picker-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.team-list {
flex: 1;
overflow-y: auto;
}
.team-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
cursor: pointer;
&:active {
background-color: #f5f5f5;
}
&.active {
background-color: #e6f7ff;
.team-item-name {
color: #1890ff;
}
}
&:last-child {
border-bottom: none;
}
}
.team-item-name {
font-size: 30rpx;
color: #333;
flex: 1;
}
.check-icon {
font-size: 32rpx;
color: #1890ff;
font-weight: bold;
}
</style>

View File

@ -1,5 +1,18 @@
<template>
<view class="chat-page">
<!-- 患者信息栏 -->
<view class="patient-info-bar" v-if="patientInfo.name">
<view class="patient-info-content">
<view class="patient-basic-info">
<text class="patient-name">{{ patientInfo.name }}</text>
<text class="patient-detail">{{ patientInfo.sex }} · {{ patientInfo.age }}</text>
</view>
<view class="patient-detail-btn" @click="handleViewPatientDetail">
<text class="detail-btn-text">查看档案</text>
</view>
</view>
</view>
<!-- 聊天消息区域 -->
<scroll-view
class="chat-content"
@ -127,7 +140,11 @@
<!-- AI助手按钮组 -->
<AIAssistantButtons
v-if="!isEvaluationPopupOpen && !showConsultAccept && orderStatus === 'processing'"
v-if="
!isEvaluationPopupOpen &&
!showConsultAccept &&
orderStatus === 'processing'
"
:groupId="groupId"
:patientAccountId="chatInfo.userID || ''"
:corpId="corpId"
@ -204,9 +221,9 @@ const groupId = ref("");
const { chatMember, getGroupInfo } = useGroupChat(groupId);
//
const updateNavigationTitle = () => {
const updateNavigationTitle = (title = "群聊") => {
uni.setNavigationBarTitle({
title: "群聊",
title: title,
});
};
@ -222,6 +239,14 @@ const isEvaluationPopupOpen = ref(false);
//
const orderStatus = ref("");
//
const patientInfo = ref({
name: "",
sex: "",
age: "",
mobile: "",
});
// - pending
const showConsultAccept = computed(() => orderStatus.value === "pending");
@ -281,8 +306,25 @@ const fetchGroupOrderStatus = async () => {
if (result.success && result.data) {
orderStatus.value = result.data.orderStatus || "";
//
const teamName = result.data.team?.name || "群聊";
updateNavigationTitle(teamName);
//
if (result.data.patient) {
patientInfo.value = {
name: result.data.patient.name || "",
sex: result.data.patient.sex || "",
age: result.data.patient.age || "",
mobile: result.data.patient.mobile || "",
};
}
console.log("获取群组订单状态:", {
orderStatus: orderStatus.value,
teamName: teamName,
patientInfo: patientInfo.value,
groupId: groupId.value,
});
} else {
@ -401,7 +443,11 @@ const initTIMCallbacks = async () => {
});
// 0
if (timChatManager.tim && timChatManager.isLoggedIn && chatInfo.value.conversationID) {
if (
timChatManager.tim &&
timChatManager.isLoggedIn &&
chatInfo.value.conversationID
) {
timChatManager.tim
.setMessageRead({
conversationID: chatInfo.value.conversationID,
@ -409,9 +455,9 @@ const initTIMCallbacks = async () => {
.then(() => {
console.log("✓ 收到新消息后已标记为已读");
// 0
timChatManager.triggerCallback('onConversationListUpdated', {
timChatManager.triggerCallback("onConversationListUpdated", {
conversationID: chatInfo.value.conversationID,
unreadCount: 0
unreadCount: 0,
});
})
.catch((error) => {
@ -570,9 +616,9 @@ const loadMessageList = async () => {
.then(() => {
console.log("✓ 会话已标记为已读:", chatInfo.value.conversationID);
//
timChatManager.triggerCallback('onConversationListUpdated', {
timChatManager.triggerCallback("onConversationListUpdated", {
conversationID: chatInfo.value.conversationID,
unreadCount: 0
unreadCount: 0,
});
})
.catch((error) => {
@ -851,6 +897,15 @@ const handleRejectReasonConfirm = async (reason) => {
const handleRejectReasonCancel = () => {
showRejectReasonModal.value = false;
};
//
const handleViewPatientDetail = () => {
// TODO:
uni.showToast({
title: "患者详情功能开发中",
icon: "none",
});
};
//
const handleEndConsult = async () => {
try {

View File

@ -1,53 +1,11 @@
<template>
<view class="message-page">
<!-- 团队切换头部 -->
<view class="header-container">
<view class="team-selector" @click="showTeamPicker = true">
<text class="team-name">{{ currentTeamName }}</text>
<text class="arrow-icon"></text>
</view>
</view>
<!-- 标签页切换 -->
<view class="tabs-container">
<view
class="tab-item"
:class="{ active: activeTab === 'processing' }"
@click="switchTab('processing')"
>
<text class="tab-text">处理中</text>
<view v-if="activeTab === 'processing'" class="tab-indicator"></view>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'finished' }"
@click="switchTab('finished')"
>
<text class="tab-text">已结束</text>
<view v-if="activeTab === 'finished'" class="tab-indicator"></view>
</view>
</view>
<!-- 团队选择弹窗 -->
<view v-if="showTeamPicker" class="team-picker-overlay" @click="showTeamPicker = false">
<view class="team-picker-content" @click.stop>
<view class="team-picker-header">
<text class="picker-title">选择团队</text>
</view>
<scroll-view class="team-list" scroll-y>
<view
v-for="team in teamList"
:key="team._id"
class="team-item"
:class="{ active: currentTeamId === team._id }"
@click="selectTeam(team)"
>
<text class="team-item-name">{{ team.name }}</text>
<text v-if="currentTeamId === team._id" class="check-icon"></text>
</view>
</scroll-view>
</view>
</view>
<!-- 头部组件 -->
<message-header
v-model:activeTab="activeTab"
@team-change="handleTeamChange"
@add-patient="handleAddPatient"
/>
<!-- 消息列表 -->
<scroll-view
@ -131,8 +89,10 @@ import { onLoad, onShow, onHide } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import useAccountStore from "@/store/account.js";
import useTeamStore from "@/store/team.js";
import useInfoCheck from "@/hooks/useInfoCheck.js";
import { globalTimChatManager } from "@/utils/tim-chat.js";
import { mergeConversationWithGroupDetails } from "@/utils/conversation-merger.js";
import MessageHeader from "./components/message-header.vue";
//
const { account, openid, isIMInitialized } = storeToRefs(useAccountStore());
@ -140,22 +100,13 @@ const { initIMAfterLogin } = useAccountStore();
//
const teamStore = useTeamStore();
const { teams } = storeToRefs(teamStore);
const { getTeams } = teamStore;
//
const { withInfo } = useInfoCheck();
//
const showTeamPicker = ref(false);
const currentTeamId = ref(""); // ""
const teamList = computed(() => {
// ""
const allOption = { _id: "", name: "全部会话消息" };
return [allOption, ...(teams.value || [])];
});
const currentTeamName = computed(() => {
if (!currentTeamId.value) return "全部会话消息";
const team = teams.value.find((t) => t._id === currentTeamId.value);
return team ? team.name : "全部会话消息";
});
// IM
watch(isIMInitialized, (newValue) => {
@ -177,7 +128,7 @@ const activeTab = ref("processing");
// orderStatus
const filteredConversationList = computed(() => {
let filtered = [];
if (activeTab.value === "processing") {
// pending() processing()
filtered = conversationList.value.filter(
@ -198,23 +149,21 @@ const filteredConversationList = computed(() => {
if (currentTeamId.value) {
filtered = filtered.filter((conv) => conv.teamId === currentTeamId.value);
}
return filtered;
});
//
const selectTeam = (team) => {
currentTeamId.value = team._id;
showTeamPicker.value = false;
console.log("切换到团队:", team.name);
//
const handleTeamChange = (teamId) => {
currentTeamId.value = teamId;
console.log("切换到团队ID:", teamId);
};
//
const switchTab = (tab) => {
if (activeTab.value === tab) return;
activeTab.value = tab;
console.log("切换到标签页:", tab);
};
// - 使 withInfo 使
const handleAddPatient = withInfo(() => {
uni.navigateTo({
url: "/pages/work/team/invite/invite-patient",
});
});
// IM
const initIM = async () => {
@ -381,7 +330,10 @@ const setupConversationListener = () => {
//
avatar: existing.avatar || conversationData.avatar,
//
unreadCount: Math.max(existing.unreadCount || 0, conversationData.unreadCount || 0)
unreadCount: Math.max(
existing.unreadCount || 0,
conversationData.unreadCount || 0
),
};
needSort = true;
console.log(
@ -421,11 +373,11 @@ const setupConversationListener = () => {
//
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
// groupID
const currentGroupID = currentPage?.options?.groupID;
const isViewingThisConversation =
currentPage?.route === "pages/message/index" &&
const isViewingThisConversation =
currentPage?.route === "pages/message/index" &&
currentGroupID === conversation.groupID;
//
@ -562,7 +514,7 @@ onShow(async () => {
try {
//
await getTeams();
// IM
const imReady = await initIM();
if (!imReady) {
@ -606,151 +558,6 @@ onHide(() => {
flex-direction: column;
}
.header-container {
background-color: #fff;
padding: 20rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.team-selector {
display: flex;
align-items: center;
cursor: pointer;
&:active {
opacity: 0.7;
}
}
.team-name {
font-size: 36rpx;
font-weight: 500;
color: #333;
flex: 1;
}
.arrow-icon {
font-size: 20rpx;
color: #999;
margin-left: 12rpx;
transform: scale(0.8);
}
.team-picker-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 200rpx;
}
.team-picker-content {
width: 600rpx;
max-height: 800rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
display: flex;
flex-direction: column;
}
.team-picker-header {
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
text-align: center;
}
.picker-title {
font-size: 36rpx;
font-weight: 500;
color: #333;
}
.team-list {
flex: 1;
overflow-y: auto;
}
.team-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom: 1rpx solid #f0f0f0;
cursor: pointer;
&:active {
background-color: #f5f5f5;
}
&.active {
background-color: #e6f7ff;
.team-item-name {
color: #1890ff;
}
}
}
.team-item-name {
font-size: 32rpx;
color: #333;
flex: 1;
}
.check-icon {
font-size: 36rpx;
color: #1890ff;
font-weight: bold;
}
.tabs-container {
display: flex;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 28rpx 0;
position: relative;
cursor: pointer;
&.active {
.tab-text {
color: #1890ff;
font-weight: 500;
}
}
}
.tab-text {
font-size: 32rpx;
color: #666;
transition: color 0.3s;
}
.tab-indicator {
position: absolute;
bottom: 0;
width: 60rpx;
height: 6rpx;
background-color: #1890ff;
border-radius: 3rpx;
}
.message-list {
width: 100%;
flex: 1;
@ -784,7 +591,7 @@ onHide(() => {
.message-item {
display: flex;
align-items: center;
padding: 10rpx 32rpx;
padding: 24rpx 32rpx;
background-color: #fff;
border-bottom: 1rpx solid #f0f0f0;
@ -796,6 +603,7 @@ onHide(() => {
.avatar-container {
position: relative;
margin-right: 24rpx;
flex-shrink: 0;
}
.avatar {
@ -839,14 +647,28 @@ onHide(() => {
margin-bottom: 8rpx;
}
.name-info {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
}
.name {
font-size: 32rpx;
font-size: 30rpx;
font-weight: 500;
color: #333;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 1;
}
.patient-info {
font-size: 26rpx;
padding-left: 12rpx;
color: #999;
flex-shrink: 0;
}
.time {
@ -862,7 +684,7 @@ onHide(() => {
}
.preview-text {
font-size: 28rpx;
font-size: 26rpx;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
@ -878,10 +700,4 @@ onHide(() => {
font-size: 24rpx;
color: #999;
}
.patient-info {
font-size: 28rpx;
padding-left: 10rpx;
color: #999;
}
</style>

1
static/zhuanhua.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="174.60px" viewBox="0 0 1173 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M1166.247916 358.382794a40.871232 40.871232 0 0 1-50.713886 13.785174H40.952129a40.9258 40.9258 0 0 1 0-81.8516h958.345809l-302.775887-215.747174a40.9258 40.9258 0 1 1 46.614486-67.282014l399.026546 284.366098A40.843948 40.843948 0 0 1 1173.232586 331.242168v0.361511a40.612035 40.612035 0 0 1-6.98467 26.779115zM40.952129 645.006632h1091.354658a40.9258 40.9258 0 0 1 0 81.851599H168.306396l312.332061 222.568141a40.9258 40.9258 0 0 1-46.614485 67.282015L21.246356 722.561023a39.561606 39.561606 0 0 1-3.124003-2.666998A40.9258 40.9258 0 0 1 40.952129 645.006632z" /></svg>

After

Width:  |  Height:  |  Size: 840 B

View File

@ -60,19 +60,19 @@ export async function mergeConversationWithGroupDetails(conversationList, option
const formattedList = mergedList
.map((group) => ({
conversationID: group.conversationID || `GROUP${group.groupID}`,
groupID: group.groupID,
name: group.patientName
? `${group.patientName}的问诊`
: group.name || "问诊群聊",
avatar: group.avatar || "/static/default-avatar.png",
lastMessage: group.lastMessage || "暂无消息",
lastMessageTime: group.lastMessageTime || Date.now(),
groupID: group.groupID,
unreadCount: group.unreadCount || 0,
doctorId: group.doctorId,
patientName: group.patientName,
patientSex: group.patientSex,
patientAge: group.patientAge,
orderStatus: group.orderStatus,
teamId: group.teamId,
teamName: group.teamName,
teamMemberList: group.teamMemberList,
}))
.sort((a, b) => b.lastMessageTime - a.lastMessageTime)
@ -123,9 +123,6 @@ function mergeConversationData(conversation, groupDetailsMap) {
return {
// 保留原有的会话信息
...conversation,
// 合并后端的群组信息
_id: groupDetail._id,
corpId: groupDetail.corpId,
teamId: groupDetail.teamId,
customerId: groupDetail.customerId,
@ -144,7 +141,7 @@ function mergeConversationData(conversation, groupDetailsMap) {
teamName: groupDetail.team?.name,
teamMemberList: groupDetail.team?.memberList,
teamDescription: groupDetail.team?.description,
teamId: groupDetail.teamId,
// 时间信息
createdAt: groupDetail.createdAt,
updatedAt: groupDetail.updatedAt,