357 lines
9.2 KiB
Vue
357 lines
9.2 KiB
Vue
<template>
|
|||
|
|
<view class="message-page">
|
||
|
|
<TopSafe></TopSafe>
|
||
|
|
<Header title="消息" :isReturn="true" @return="onReturnClick"></Header>
|
||
|
|
|
||
|
|
<view class="message-container">
|
||
|
|
<view class="message-item" v-for="item in messageList" :key="item.id" @click="onItemClick(item)">
|
||
|
|
<view class="item-icon-wrap" :class="item.iconType">
|
||
|
|
<image class="item-icon" :src="item.icon" mode="aspectFit" />
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<view class="item-content">
|
||
|
|
<view class="title-row">
|
||
|
|
<text class="item-title">{{ item.title }}</text>
|
||
|
|
<text class="item-badge" v-if="item.badge">{{ item.badge }}</text>
|
||
|
|
</view>
|
||
|
|
<text class="item-desc">{{ item.desc }}</text>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
</view>
|
||
|
|
|
||
|
|
<qiaobao-assistant page-key="packages/message/message" title="消息" />
|
||
|
|
</view>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
import Header from "@/components/common/header.vue";
|
||
|
|
import TopSafe from "@/components/common/top-safe.nvue";
|
||
|
|
import { getNewsList } from "@/api/news.js";
|
||
|
|
import {
|
||
|
|
CONTENT_TYPE,
|
||
|
|
getCurrentMember,
|
||
|
|
getMessages,
|
||
|
|
getUnreadCount,
|
||
|
|
} from "@/uni_modules/hashmall-customer-service/js_sdk/api.js";
|
||
|
|
import { getStorageFun, TOKEN_NAME, USER_DATA } from "@/utils/auth.js";
|
||
|
|
import {
|
||
|
|
KEFU_REALTIME_EVENT,
|
||
|
|
startKefuRealtime,
|
||
|
|
} from "@/uni_modules/hashmall-customer-service/js_sdk/kefu-realtime.js";
|
||
|
|
|
||
|
|
const KEFU_DEFAULT_DESC = "点击联系平台客服";
|
||
|
|
const NOTICE_DEFAULT_DESC = "暂无系统公告";
|
||
|
|
|
||
|
|
export default {
|
||
|
|
components: {
|
||
|
|
Header,
|
||
|
|
TopSafe,
|
||
|
|
},
|
||
|
|
data() {
|
||
|
|
return {
|
||
|
|
refreshing: false,
|
||
|
|
realtimeRefreshTimer: null,
|
||
|
|
messageList: [
|
||
|
|
{
|
||
|
|
id: "kefu",
|
||
|
|
title: "平台客服",
|
||
|
|
badge: "",
|
||
|
|
desc: KEFU_DEFAULT_DESC,
|
||
|
|
iconType: "kefu",
|
||
|
|
icon: "https://static.tbmall.xin/static/plat-customer-service.png",
|
||
|
|
path: "/pages/rwa_package/kefu/kefu",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: "system",
|
||
|
|
title: "系统公告",
|
||
|
|
badge: "",
|
||
|
|
desc: NOTICE_DEFAULT_DESC,
|
||
|
|
iconType: "system",
|
||
|
|
icon: "https://static.tbmall.xin/static/sys-customer-service.png",
|
||
|
|
path: "/pages/login_package/announcement/announcement",
|
||
|
|
},
|
||
|
|
],
|
||
|
|
};
|
||
|
|
},
|
||
|
|
onLoad() {
|
||
|
|
uni.$on(KEFU_REALTIME_EVENT, this.handleKefuRealtimeEvent);
|
||
|
|
},
|
||
|
|
onShow() {
|
||
|
|
this.refreshMessageCenter();
|
||
|
|
startKefuRealtime();
|
||
|
|
},
|
||
|
|
onUnload() {
|
||
|
|
uni.$off(KEFU_REALTIME_EVENT, this.handleKefuRealtimeEvent);
|
||
|
|
if (this.realtimeRefreshTimer) clearTimeout(this.realtimeRefreshTimer);
|
||
|
|
this.realtimeRefreshTimer = null;
|
||
|
|
},
|
||
|
|
methods: {
|
||
|
|
handleKefuRealtimeEvent(event) {
|
||
|
|
if (!event || !String(event.type || "").startsWith("kefu_")) return;
|
||
|
|
// A single business operation may emit a message and a conversation event.
|
||
|
|
// Coalesce them so the summary endpoint is read only once.
|
||
|
|
if (this.realtimeRefreshTimer) clearTimeout(this.realtimeRefreshTimer);
|
||
|
|
this.realtimeRefreshTimer = setTimeout(() => {
|
||
|
|
this.realtimeRefreshTimer = null;
|
||
|
|
this.loadKefuSummary();
|
||
|
|
}, 120);
|
||
|
|
},
|
||
|
|
getMessageItem(id) {
|
||
|
|
return this.messageList.find((item) => item.id === id);
|
||
|
|
},
|
||
|
|
formatBadge(value) {
|
||
|
|
const count = Math.max(0, Number(value) || 0);
|
||
|
|
if (!count) return "";
|
||
|
|
return count > 99 ? "99+" : String(count);
|
||
|
|
},
|
||
|
|
parseStoredUser() {
|
||
|
|
const value = getStorageFun(USER_DATA);
|
||
|
|
if (!value) return null;
|
||
|
|
if (typeof value === "object") {
|
||
|
|
return value.id ? value : { ...value, id: value.userId };
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const parsed = JSON.parse(value);
|
||
|
|
return parsed.id ? parsed : { ...parsed, id: parsed.userId };
|
||
|
|
} catch (error) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
withTimeout(promise, timeout = 5000) {
|
||
|
|
return Promise.race([
|
||
|
|
promise,
|
||
|
|
new Promise((resolve) => setTimeout(() => resolve(null), timeout)),
|
||
|
|
]);
|
||
|
|
},
|
||
|
|
normalizeKefuDesc(content, contentType) {
|
||
|
|
const type = Number(contentType || 0);
|
||
|
|
if (type === CONTENT_TYPE.IMAGE) return "[图片]";
|
||
|
|
if (type === CONTENT_TYPE.FILE) return "[文件]";
|
||
|
|
if (type === CONTENT_TYPE.AUDIO) return "[语音]";
|
||
|
|
if (type === CONTENT_TYPE.PRODUCT) return "[商品]";
|
||
|
|
if (type === CONTENT_TYPE.ORDER) return "[订单]";
|
||
|
|
if (type === CONTENT_TYPE.RECALLED) return "消息已撤回";
|
||
|
|
|
||
|
|
const text = String(content || "").trim();
|
||
|
|
if (!text) return KEFU_DEFAULT_DESC;
|
||
|
|
if (type === CONTENT_TYPE.QUOTE) {
|
||
|
|
try {
|
||
|
|
const quote = JSON.parse(text);
|
||
|
|
return String(quote.text || quote.content || "[引用消息]");
|
||
|
|
} catch (error) {
|
||
|
|
return text;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return text;
|
||
|
|
},
|
||
|
|
async loadKefuSummary() {
|
||
|
|
const item = this.getMessageItem("kefu");
|
||
|
|
if (!item) return;
|
||
|
|
|
||
|
|
if (!getStorageFun(TOKEN_NAME)) {
|
||
|
|
item.badge = "";
|
||
|
|
item.desc = KEFU_DEFAULT_DESC;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
let member = this.parseStoredUser();
|
||
|
|
if (!member || !member.id) {
|
||
|
|
try {
|
||
|
|
member = await this.withTimeout(getCurrentMember());
|
||
|
|
} catch (error) {
|
||
|
|
member = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!member || !member.id) return;
|
||
|
|
|
||
|
|
const results = await Promise.allSettled([
|
||
|
|
getUnreadCount(member.id),
|
||
|
|
getMessages({ userId: member.id, page: 1, pageSize: 20 }),
|
||
|
|
]);
|
||
|
|
|
||
|
|
if (results[0].status === "fulfilled") {
|
||
|
|
item.badge = this.formatBadge(results[0].value);
|
||
|
|
}
|
||
|
|
if (results[1].status === "fulfilled") {
|
||
|
|
const result = results[1].value || {};
|
||
|
|
const conversation = result.conversation || {};
|
||
|
|
const rows = (result.messages && result.messages.entitys) || [];
|
||
|
|
const latest = rows.length ? rows[rows.length - 1] : {};
|
||
|
|
item.desc = this.normalizeKefuDesc(
|
||
|
|
conversation.lastMessageContent || latest.content,
|
||
|
|
conversation.lastMessageContentType || latest.contentType,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
async loadNoticeSummary() {
|
||
|
|
const item = this.getMessageItem("system");
|
||
|
|
if (!item) return;
|
||
|
|
try {
|
||
|
|
const result = await getNewsList({
|
||
|
|
category: 2,
|
||
|
|
type: 0,
|
||
|
|
page: 1,
|
||
|
|
pageSize: 1,
|
||
|
|
});
|
||
|
|
const rows = (result && result.data && result.data.entitys) || [];
|
||
|
|
const latest = rows[0];
|
||
|
|
if (!latest) {
|
||
|
|
item.badge = "";
|
||
|
|
item.desc = NOTICE_DEFAULT_DESC;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
item.desc = latest.subTitle || latest.title || NOTICE_DEFAULT_DESC;
|
||
|
|
const readId = uni.getStorageSync("isReadNews");
|
||
|
|
item.badge = String(readId || "") === String(latest.id || "") ? "" : "1";
|
||
|
|
} catch (error) {
|
||
|
|
item.badge = "";
|
||
|
|
item.desc = NOTICE_DEFAULT_DESC;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
async refreshMessageCenter() {
|
||
|
|
if (this.refreshing) return;
|
||
|
|
this.refreshing = true;
|
||
|
|
try {
|
||
|
|
await Promise.allSettled([
|
||
|
|
this.loadKefuSummary(),
|
||
|
|
this.loadNoticeSummary(),
|
||
|
|
]);
|
||
|
|
} finally {
|
||
|
|
this.refreshing = false;
|
||
|
|
}
|
||
|
|
},
|
||
|
|
onReturnClick() {
|
||
|
|
const pages = getCurrentPages();
|
||
|
|
if (pages && pages.length > 1) {
|
||
|
|
uni.navigateBack({
|
||
|
|
delta: 1,
|
||
|
|
fail: () => {
|
||
|
|
uni.switchTab({ url: "/pages/home/home" });
|
||
|
|
},
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
uni.switchTab({ url: "/pages/home/home" });
|
||
|
|
}
|
||
|
|
},
|
||
|
|
onItemClick(item) {
|
||
|
|
if (item.path) {
|
||
|
|
uni.navigateTo({
|
||
|
|
url: item.path,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
},
|
||
|
|
},
|
||
|
|
};
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped lang="scss">
|
||
|
|
.message-page {
|
||
|
|
min-height: 100vh;
|
||
|
|
background-color: #f7f8fa;
|
||
|
|
box-sizing: border-box;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
}
|
||
|
|
|
||
|
|
.message-container {
|
||
|
|
background-color: #ffffff;
|
||
|
|
width: 100%;
|
||
|
|
box-sizing: border-box;
|
||
|
|
}
|
||
|
|
|
||
|
|
.message-item {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: row;
|
||
|
|
align-items: center;
|
||
|
|
padding: 30rpx 32rpx;
|
||
|
|
position: relative;
|
||
|
|
background-color: #ffffff;
|
||
|
|
box-sizing: border-box;
|
||
|
|
|
||
|
|
&:not(:last-child)::after {
|
||
|
|
content: "";
|
||
|
|
position: absolute;
|
||
|
|
left: 152rpx;
|
||
|
|
right: 0rpx;
|
||
|
|
bottom: 0;
|
||
|
|
height: 1rpx;
|
||
|
|
background-color: #f2f3f5;
|
||
|
|
}
|
||
|
|
|
||
|
|
&:active {
|
||
|
|
background-color: #f9f9f9;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.item-icon-wrap {
|
||
|
|
width: 96rpx;
|
||
|
|
height: 96rpx;
|
||
|
|
border-radius: 20rpx;
|
||
|
|
display: flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
flex-shrink: 0;
|
||
|
|
|
||
|
|
.item-icon {
|
||
|
|
width: 96rpx;
|
||
|
|
height: 96rpx;
|
||
|
|
display: block;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
|
||
|
|
.item-content {
|
||
|
|
flex: 1;
|
||
|
|
min-width: 0;
|
||
|
|
margin-left: 24rpx;
|
||
|
|
overflow: hidden;
|
||
|
|
display: flex;
|
||
|
|
flex-direction: column;
|
||
|
|
justify-content: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.title-row {
|
||
|
|
display: flex;
|
||
|
|
flex-direction: row;
|
||
|
|
align-items: center;
|
||
|
|
min-width: 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
.item-title {
|
||
|
|
font-size: 32rpx;
|
||
|
|
font-weight: 600;
|
||
|
|
color: #1d2129;
|
||
|
|
line-height: 44rpx;
|
||
|
|
}
|
||
|
|
|
||
|
|
.item-badge {
|
||
|
|
background-color: #f53f3f;
|
||
|
|
color: #ffffff;
|
||
|
|
font-size: 22rpx;
|
||
|
|
font-weight: 500;
|
||
|
|
min-width: 32rpx;
|
||
|
|
height: 32rpx;
|
||
|
|
line-height: 32rpx;
|
||
|
|
border-radius: 16rpx;
|
||
|
|
text-align: center;
|
||
|
|
padding: 0 8rpx;
|
||
|
|
margin-left: 12rpx;
|
||
|
|
box-sizing: border-box;
|
||
|
|
display: inline-flex;
|
||
|
|
align-items: center;
|
||
|
|
justify-content: center;
|
||
|
|
}
|
||
|
|
|
||
|
|
.item-desc {
|
||
|
|
font-size: 26rpx;
|
||
|
|
color: #86909c;
|
||
|
|
line-height: 36rpx;
|
||
|
|
margin-top: 8rpx;
|
||
|
|
white-space: nowrap;
|
||
|
|
overflow: hidden;
|
||
|
|
text-overflow: ellipsis;
|
||
|
|
width: 100%;
|
||
|
|
}
|
||
|
|
</style>
|