Files
frontend-app/pages/login_package/message/message.vue
T
2026-08-21 14:10:43 +08:00

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>