Compare commits
3 Commits
f6486a69ea
...
0a3a521a32
| Author | SHA1 | Date |
|---|---|---|
|
|
0a3a521a32 | |
|
|
e99120c34e | |
|
|
3322089431 |
|
|
@ -240,6 +240,10 @@ export default {
|
|||
this.chatList = this.normalizeDialogueList(list);
|
||||
// 同步到全局会话列表,便于后续实时合并
|
||||
this.$store.commit("set_UserMsgList", this.chatList);
|
||||
this.$store.commit("upsert_DialogueTypeMap", {
|
||||
list: this.chatList,
|
||||
type: 1,
|
||||
});
|
||||
this.totalCount = res?.data?.item2 || list.length;
|
||||
} catch (error) {
|
||||
console.error("[在线咨询] 获取会话列表失败", error);
|
||||
|
|
|
|||
|
|
@ -10,63 +10,42 @@
|
|||
enable-back-to-top
|
||||
>
|
||||
<view class="main-content">
|
||||
<!-- 服务状态卡片 -->
|
||||
<view class="status-card">
|
||||
<view class="status-title">人工服务</view>
|
||||
<view class="status-content">
|
||||
<view class="status-badge" :class="isOnline ? 'online' : 'offline'">
|
||||
{{ isOnline ? '在线' : '离线' }}
|
||||
<!-- 会话列表(人工转接) -->
|
||||
<view
|
||||
class="chat-item"
|
||||
v-for="(item, index) in displayChatList"
|
||||
:key="item.id || index"
|
||||
@click="openChat(item)"
|
||||
>
|
||||
<view class="chat-avatar-wrapper">
|
||||
<image class="chat-avatar" :src="item.avatar"></image>
|
||||
<view class="unread-badge" v-if="item.unreadCount > 0">
|
||||
{{ item.unreadCount > 99 ? '99+' : item.unreadCount }}
|
||||
</view>
|
||||
<view class="status-desc">
|
||||
{{ isOnline ? '客服正在为您服务' : '当前暂无客服在线' }}
|
||||
</view>
|
||||
|
||||
<view class="chat-content">
|
||||
<view class="chat-header">
|
||||
<text class="chat-name">{{ item.name }}</text>
|
||||
<text class="chat-time">{{ item.displayTime }}</text>
|
||||
</view>
|
||||
<view class="chat-preview">
|
||||
<text class="preview-text">{{ item.displayPreview }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 人工客服列表 -->
|
||||
<view class="section-title">在线客服</view>
|
||||
<view class="agent-list">
|
||||
<view class="agent-item" v-for="(agent, index) in agentList" :key="index" @click="handleTransfer(agent)">
|
||||
<view class="agent-avatar-wrapper">
|
||||
<image class="agent-avatar" :src="agent.avatar || defaultAvatar"></image>
|
||||
<view class="online-dot" v-if="agent.online"></view>
|
||||
</view>
|
||||
<view class="agent-info">
|
||||
<view class="agent-name">{{ agent.name }}</view>
|
||||
<view class="agent-status">{{ agent.statusText }}</view>
|
||||
</view>
|
||||
<view class="agent-action">
|
||||
<u-button
|
||||
size="mini"
|
||||
type="primary"
|
||||
:disabled="!agent.online"
|
||||
@click.stop="handleTransfer(agent)"
|
||||
>
|
||||
{{ agent.online ? '转接' : '离线' }}
|
||||
</u-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 无数据提示 -->
|
||||
<view class="empty-tip" v-if="agentList.length === 0">
|
||||
暂无客服在线
|
||||
<view class="empty-container" v-if="chatList.length === 0">
|
||||
<image class="empty-image" src="/static/common/icon/empty-chat.png"></image>
|
||||
<text class="empty-text">暂无人工转接消息</text>
|
||||
<text class="empty-hint">人工客服接入后会显示在这里</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="page-tabbar">
|
||||
<TabBar :currentPath="'/pages/manual-transfer/index'" @change="handleTabChange" />
|
||||
<TabBar :currentPath="'/pages/transfer/index'" @change="handleTabChange" />
|
||||
</view>
|
||||
|
||||
<u-modal
|
||||
v-model="showModal"
|
||||
:show-cancel-button="true"
|
||||
title="人工转接"
|
||||
:content="modalContent"
|
||||
@confirm="confirmTransfer"
|
||||
@cancel="showModal = false"
|
||||
></u-modal>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
|
@ -80,68 +59,266 @@ export default {
|
|||
TabBar,
|
||||
PageHeader,
|
||||
},
|
||||
computed: {
|
||||
dialogueTypeMap() {
|
||||
return this.$store?.state?.vuex_dialogueTypeMap || {};
|
||||
},
|
||||
unreadMap() {
|
||||
const map = {};
|
||||
const list = this.$store?.state?.vuex_userMsgList || [];
|
||||
list.forEach((item) => {
|
||||
const id = this.getDialogueId(item);
|
||||
const count =
|
||||
item?.unReadCount ??
|
||||
item?.unreadCount ??
|
||||
item?.unread ??
|
||||
null;
|
||||
if (id && typeof count === "number") {
|
||||
map[id] = count;
|
||||
}
|
||||
});
|
||||
return map;
|
||||
},
|
||||
displayChatList() {
|
||||
// 未完成首帧加载前不渲染,避免显示上一页的会话
|
||||
if (!this.hasLoaded) return [];
|
||||
|
||||
const realtime = this.$store?.state?.vuex_userMsgList || [];
|
||||
const baseList = this.chatList || [];
|
||||
|
||||
const rtMap = {};
|
||||
realtime.forEach((item) => {
|
||||
const id = this.getDialogueId(item);
|
||||
if (id) rtMap[id] = item;
|
||||
});
|
||||
|
||||
const merged = baseList.map((item) => {
|
||||
const id = this.getDialogueId(item);
|
||||
const rt = id ? rtMap[id] : null;
|
||||
const source = rt || item;
|
||||
// 只展示标记为人工转接(2)的会话
|
||||
const type = this.dialogueTypeMap[id];
|
||||
if (type && Number(type) !== 2) return null;
|
||||
const unread =
|
||||
typeof this.unreadMap[id] === "number"
|
||||
? this.unreadMap[id]
|
||||
: source?.unReadCount ??
|
||||
source?.unreadCount ??
|
||||
source?.unread ??
|
||||
0;
|
||||
|
||||
return {
|
||||
...item,
|
||||
...rt,
|
||||
id,
|
||||
dialogueManagementId: id,
|
||||
DialogueManagementId: id,
|
||||
userId:
|
||||
source?.receiverId ||
|
||||
source?.friendId ||
|
||||
source?.userId ||
|
||||
item?.userId ||
|
||||
"",
|
||||
avatar:
|
||||
source?.receiverHeadSculptureUrl ||
|
||||
source?.avatar ||
|
||||
source?.friendAvatar ||
|
||||
"/static/avatar/default-avatar.png",
|
||||
displayTime: this.formatTime(
|
||||
source?.lastMessageTime ||
|
||||
source?.lastSendTime ||
|
||||
source?.sendDate ||
|
||||
item?.lastMessageTime ||
|
||||
""
|
||||
),
|
||||
displayPreview:
|
||||
source?.lastMessage ||
|
||||
source?.message ||
|
||||
item?.lastMessage ||
|
||||
"暂无消息",
|
||||
unreadCount: unread,
|
||||
};
|
||||
}).filter(Boolean);
|
||||
|
||||
realtime.forEach((item) => {
|
||||
const id = this.getDialogueId(item);
|
||||
if (!id) return;
|
||||
const exists = merged.some((row) => row.id === id);
|
||||
const type = this.dialogueTypeMap[id];
|
||||
if (!exists && (!type || Number(type) === 2)) {
|
||||
const unread =
|
||||
typeof this.unreadMap[id] === "number"
|
||||
? this.unreadMap[id]
|
||||
: item?.unReadCount ??
|
||||
item?.unreadCount ??
|
||||
item?.unread ??
|
||||
0;
|
||||
merged.unshift({
|
||||
...item,
|
||||
id,
|
||||
dialogueManagementId: id,
|
||||
DialogueManagementId: id,
|
||||
userId:
|
||||
item?.receiverId ||
|
||||
item?.friendId ||
|
||||
item?.userId ||
|
||||
"",
|
||||
avatar:
|
||||
item?.receiverHeadSculptureUrl ||
|
||||
item?.avatar ||
|
||||
item?.friendAvatar ||
|
||||
"/static/avatar/default-avatar.png",
|
||||
displayTime: this.formatTime(
|
||||
item?.lastMessageTime ||
|
||||
item?.lastSendTime ||
|
||||
item?.sendDate ||
|
||||
""
|
||||
),
|
||||
displayPreview:
|
||||
item?.lastMessage || item?.message || "暂无消息",
|
||||
unreadCount: unread,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return merged;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
modalContent: '',
|
||||
selectedAgent: null,
|
||||
defaultAvatar: "/static/avatar/default-avatar.png",
|
||||
isOnline: true,
|
||||
agentList: [
|
||||
{
|
||||
id: 1,
|
||||
name: "客服小王",
|
||||
avatar: "",
|
||||
online: true,
|
||||
statusText: "空闲中",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "客服小李",
|
||||
avatar: "",
|
||||
online: true,
|
||||
statusText: "忙碌中",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "客服小张",
|
||||
avatar: "",
|
||||
online: false,
|
||||
statusText: "离线",
|
||||
},
|
||||
],
|
||||
chatList: [],
|
||||
totalCount: 0,
|
||||
isLoading: false,
|
||||
hasLoaded: false,
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.loadChatList().finally(() => {
|
||||
this.hasLoaded = true;
|
||||
});
|
||||
},
|
||||
onShow() {
|
||||
if (this.hasLoaded) {
|
||||
this.refreshChatList();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleTabChange(path, index) {
|
||||
console.log("切换到标签页:", path, index);
|
||||
},
|
||||
handleTransfer(agent) {
|
||||
if (!agent.online) {
|
||||
uni.showToast({
|
||||
title: '该客服当前离线',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
async openChat(item) {
|
||||
if (item?.id) {
|
||||
try {
|
||||
await this.$u.api.ReadMessageApi({
|
||||
dialogueManagementId: item.id,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[人工转接] 标记已读失败", err);
|
||||
}
|
||||
}
|
||||
this.selectedAgent = agent;
|
||||
this.modalContent = `确认转接到${agent.name}吗?`;
|
||||
this.showModal = true;
|
||||
},
|
||||
confirmTransfer() {
|
||||
// 这里可以执行实际的转接逻辑
|
||||
uni.showToast({
|
||||
title: '正在为您转接...',
|
||||
icon: 'none'
|
||||
|
||||
this.$store.dispatch("selectTeacherChatItem", {
|
||||
id: item.id,
|
||||
receiverId: item.userId,
|
||||
});
|
||||
this.showModal = false;
|
||||
|
||||
// 模拟跳转到聊天页面
|
||||
setTimeout(() => {
|
||||
// uni.navigateTo({
|
||||
// url: '/pages/message/dialogBox/dialogBox?agentId=' + this.selectedAgent.id
|
||||
// });
|
||||
}, 1000);
|
||||
},
|
||||
async loadChatList() {
|
||||
if (this.isLoading) return;
|
||||
this.isLoading = true;
|
||||
try {
|
||||
const res = await this.$u.api.GetDialogueListApi({
|
||||
"Item1.OnlineConsultationType": 2,
|
||||
});
|
||||
const list = (res && res.data && res.data.item1) || [];
|
||||
this.chatList = this.normalizeDialogueList(list);
|
||||
this.$store.commit("set_UserMsgList", this.chatList);
|
||||
this.$store.commit("upsert_DialogueTypeMap", {
|
||||
list: this.chatList,
|
||||
type: 2,
|
||||
});
|
||||
this.totalCount = res?.data?.item2 || list.length;
|
||||
} catch (error) {
|
||||
console.error("[人工转接] 获取会话列表失败", error);
|
||||
uni.showToast({
|
||||
title: "获取会话列表失败",
|
||||
icon: "none",
|
||||
});
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
refreshChatList() {
|
||||
this.loadChatList();
|
||||
},
|
||||
normalizeDialogueList(list = []) {
|
||||
return list.map((item, index) => {
|
||||
const unread =
|
||||
typeof item?.unReadCount === "number"
|
||||
? item.unReadCount
|
||||
: item?.unreadCount || 0;
|
||||
|
||||
const id =
|
||||
item?.dialogueManagementId ||
|
||||
item?.DialogueManagementId ||
|
||||
item?.dialogueId ||
|
||||
item?.friendId ||
|
||||
item?.id ||
|
||||
index;
|
||||
|
||||
return {
|
||||
id,
|
||||
dialogueManagementId: id,
|
||||
DialogueManagementId: id,
|
||||
userId: item?.receiverId || item?.friendId || item?.userId || "",
|
||||
friendId: item?.receiverId || item?.friendId || item?.userId || "",
|
||||
name:
|
||||
item?.receiverName ||
|
||||
item?.friendName ||
|
||||
item?.userName ||
|
||||
item?.title ||
|
||||
"",
|
||||
avatar: this.buildAvatarUrl(
|
||||
item?.receiverHeadSculptureUrl ||
|
||||
item?.avatar ||
|
||||
item?.friendAvatar
|
||||
),
|
||||
lastMessage: item?.lastMessage || "暂无消息",
|
||||
lastMessageTime: this.formatTime(
|
||||
item?.lastMessageTime || item?.lastSendTime || item?.startTime
|
||||
),
|
||||
unreadCount: unread,
|
||||
};
|
||||
});
|
||||
},
|
||||
buildAvatarUrl(url) {
|
||||
const fallback = "/static/avatar/default-avatar.png";
|
||||
if (!url) return fallback;
|
||||
if (/^https?:\/\//i.test(url)) return url;
|
||||
const baseUrl = this.$u?.http?.config?.baseUrl || "";
|
||||
if (baseUrl) return `${baseUrl}/${url}`;
|
||||
return url.startsWith("/") ? url : `/${url}`;
|
||||
},
|
||||
formatTime(timeStr) {
|
||||
if (!timeStr) return "";
|
||||
const date = new Date(timeStr);
|
||||
if (!isNaN(date.getTime())) {
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const day = date.getDate().toString().padStart(2, "0");
|
||||
const hours = date.getHours().toString().padStart(2, "0");
|
||||
const minutes = date.getMinutes().toString().padStart(2, "0");
|
||||
return `${month}/${day} ${hours}:${minutes}`;
|
||||
}
|
||||
return String(timeStr).replace("T", " ").slice(5, 16);
|
||||
},
|
||||
getDialogueId(item) {
|
||||
return (
|
||||
item?.dialogueManagementId ||
|
||||
item?.DialogueManagementId ||
|
||||
item?.dialogueId ||
|
||||
item?.friendId ||
|
||||
item?.id ||
|
||||
""
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -188,123 +365,113 @@ export default {
|
|||
z-index: 100;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.status-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.status-content {
|
||||
.chat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.status-badge.online {
|
||||
background-color: #52c41a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.status-badge.offline {
|
||||
background-color: #999;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.status-desc {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin: 15px 0 10px 0;
|
||||
}
|
||||
|
||||
.agent-list {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.agent-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.agent-item:last-child {
|
||||
border-bottom: none;
|
||||
.chat-item:active {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.agent-avatar-wrapper {
|
||||
.chat-avatar-wrapper {
|
||||
position: relative;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.agent-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background-color: #eee;
|
||||
.chat-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 8px;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
.online-dot {
|
||||
.unread-badge {
|
||||
position: absolute;
|
||||
bottom: 2px;
|
||||
right: 2px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background-color: #52c41a;
|
||||
border-radius: 50%;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
background: #ff4d4f;
|
||||
border-radius: 9px;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
|
||||
.agent-info {
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.agent-name {
|
||||
font-size: 15px;
|
||||
.chat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.chat-name {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agent-status {
|
||||
.chat-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chat-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-text {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.empty-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100px 20px;
|
||||
}
|
||||
|
||||
.empty-image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.agent-action {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
padding: 50px 20px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ const store = new Vuex.Store({
|
|||
vuex_msgUser: lifeData.vuex_msgUser ? lifeData.vuex_msgUser : {},
|
||||
// 消息窗口滚动位置
|
||||
vuex_msgScrollTop: 0,
|
||||
// 记录会话ID与在线咨询类型的映射,区分在线咨询/人工转接
|
||||
vuex_dialogueTypeMap: {},
|
||||
// 自定义tabbar数据
|
||||
vuex_iPhone: lifeData.vuex_iPhone ? lifeData.vuex_iPhone : false,
|
||||
// tabbar相关配置
|
||||
|
|
@ -309,6 +311,21 @@ const store = new Vuex.Store({
|
|||
saveLifeData("vuex_teacherInfo", state.vuex_teacherInfo);
|
||||
saveLifeData("vuex_user", state.vuex_user);
|
||||
},
|
||||
// 合并会话类型映射(在线咨询类型)
|
||||
upsert_DialogueTypeMap(state, { list = [], type }) {
|
||||
if (!type) return;
|
||||
const next = { ...(state.vuex_dialogueTypeMap || {}) };
|
||||
list.forEach((item) => {
|
||||
const id =
|
||||
item?.dialogueManagementId ||
|
||||
item?.DialogueManagementId ||
|
||||
item?.dialogueId ||
|
||||
item?.friendId ||
|
||||
item?.id;
|
||||
if (id) next[id] = type;
|
||||
});
|
||||
state.vuex_dialogueTypeMap = next;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
// 获取聊天列表(最近联系人)
|
||||
|
|
|
|||
Loading…
Reference in New Issue