Files
lilishop-ui/im/src/components/chat/panel/OtherLink.vue
pikachu1995@126.com 97e611c072 refactor(im): 重构聊天面板UI组件并修复多处交互问题
- 将头像组件从 dicebear 外链改为本地首字+底色生成,新增 avatarColor 工具函数
- 重构订单卡片(footPrint.vue)布局,优化商品与订单信息展示结构
- 重构店铺详情卡片(storeDetail.vue),统一信息行与评分行样式
- 移除消息气泡中的昵称时间行,调整气泡背景色与头像内边距
- 修复 face 变量名与全局组件 <face> 冲突导致头像URL被当作标签渲染
- 将 unload 事件监听替换为 pagehide,兼容 Chrome 弃用策略
- 修复搜索关键词未转义正则元字符导致输入特殊字符时抛异常
- 修复 WebSocket onMessage 对无 text 字段的推送消息误调用 JSON.parse
- 将 setup 中同步调用全局 mixin 方法移至 onMounted,避免代理未就绪
- 商品/订单图片统一添加 onerror 默认图兜底
- 聊天列表消息预览支持 emoji 图片渲染(textReplaceEmoji)
- 搜索框增加 clearable 属性及"没有找到相关好友"空态提示
- 锁定 element-plus 版本为 2.14.1 防止自动升级引入兼容问题
- 日期格式化分隔符由斜杠改为横杠
- 提现设置页新增提现规则说明字段,满足微信审核公示要求
2026-10-29 10:56:28 +08:00

326 lines
7.7 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>
<el-tabs v-model="activeName" @tab-click="handleClick" :stretch=true>
<el-tab-pane :label="toUser.storeFlag ? '正在咨询' : '他的足迹'" name="history">
<div>
<GoodsLink :goodsDetail="goodsDetail" v-if="toUser.userId === goodsDetail.storeId"
@sendMessage="submitSendMessage" />
<FootPrint :list="footPrintList" @loadMore="loadMoreFootPrint()" :orderList="orderPrintList"
@sendMessage="submitSendMessage" />
</div>
</el-tab-pane>
<el-tab-pane label="店铺信息" name="UserInfo" v-if="toUser.storeFlag">
<div v-if="toUser.storeFlag">
<StoreDetail :storeInfo="storeInfo" />
</div>
</el-tab-pane>
</el-tabs>
</template>
<script setup>
import { computed, nextTick, onMounted, reactive, ref, watch } from "vue";
import { useRoute } from "vue-router";
import {
ServeGetStoreDetail,
ServeGetFootPrint,
ServeGetOrderPrint,
ServeGetGoodsDetail,
ServeStoreGetFootPrint,
ServeStoreGetOrderPrint,
} from "@/api/user";
import StoreDetail from "@/components/chat/panel/template/storeDetail.vue";
import FootPrint from "@/components/chat/panel/template/footPrint.vue";
import GoodsLink from "@/components/chat/panel/template/goodsLink.vue";
import SocketInstance from "@/im-server/socket-instance";
import { useTalkStore } from "@/stores/talk";
import { useDialogueStore } from "@/stores/dialogue";
import { getToken } from "@/utils/auth";
// 与 computed 属性同名,改用别名导入
import { isBuyerImMode as isBuyerImModeFn } from "@/utils/im-mode";
const SIDE_REQUEST_OPTIONS = { skipAuthDialog: true };
const props = defineProps({
toUser: {
type: Object,
default: null,
},
id: {
type: String,
default: "",
},
goodsParams: {
type: Object,
default: null,
},
});
const talkStore = useTalkStore();
const dialogueStore = useDialogueStore();
const route = useRoute();
const activeName = ref("history");
const storeInfo = ref({});
const memberInfo = ref({});
const footPrintParams = reactive({
pageSize: 20,
pageNumber: 1,
memberId: "",
storeId: "",
});
const goodsDetail = ref({});
const footPrintList = ref([]);
const orderPrintList = ref([]);
const talkItems = computed(() => talkStore.talkItems);
const index_name = computed(() => dialogueStore.index_name);
const isBuyerImMode = computed(() => isBuyerImModeFn(route));
watch(
() => props.toUser,
() => {
syncStoreFlagCache();
resetSidePanelData();
if (props.toUser.storeFlag) {
getStoreDetail();
}
getFootPrint();
if (props.goodsParams && props.toUser.storeFlag) {
getGoodsDetail();
}
}
);
onMounted(() => {
syncStoreFlagCache();
if (props.toUser.storeFlag) {
getStoreDetail();
}
getFootPrint();
if (props.goodsParams && props.toUser.storeFlag) {
getGoodsDetail();
}
});
function syncStoreFlagCache() {
localStorage.setItem(
"storeFlag",
isBuyerImMode.value ? "true" : "false"
);
}
function resetSidePanelData() {
footPrintList.value = [];
orderPrintList.value = [];
footPrintParams.pageNumber = 1;
}
function getStoreDetail() {
ServeGetStoreDetail(props.toUser.userId, SIDE_REQUEST_OPTIONS)
.then((res) => {
if (res.success) {
storeInfo.value = res.result;
}
})
.catch(() => {});
}
function loadMoreFootPrint() {
footPrintParams.pageNumber++;
getFootPrint();
}
function handleClick() {}
function getGoodsDetail() {
if (!props.toUser.storeFlag) {
return;
}
if (!props.goodsParams || !props.goodsParams.goodsId) {
return;
}
ServeGetGoodsDetail(props.goodsParams, SIDE_REQUEST_OPTIONS)
.then((res) => {
if (res.success) {
goodsDetail.value = res.result.data;
}
})
.catch(() => {});
}
function normalizeFootPrintRecords(records = []) {
const goodsId = props.goodsParams?.goodsId;
return records
.filter((item) => item != null)
.filter((item) => !goodsId || item.goodsId !== goodsId)
.map((item) => ({
...item,
btnHide: localStorage.getItem(item.goodsId) ? 0 : 1,
}));
}
function normalizeOrderRecords(records = []) {
return records.map((item) => ({
...item,
btnHide: 1,
}));
}
function getFootPrint() {
if (!getToken()) {
return;
}
if (!props.toUser?.userId || !props.id) {
return;
}
if (isBuyerImMode.value) {
footPrintParams.memberId = props.id;
footPrintParams.storeId = props.toUser.userId;
fetchBuyerSideData();
} else {
footPrintParams.memberId = props.toUser.userId;
footPrintParams.storeId = props.id;
fetchStoreSideData();
}
}
function fetchBuyerSideData() {
ServeGetFootPrint(footPrintParams, SIDE_REQUEST_OPTIONS)
.then((res) => {
if (res?.result?.records) {
footPrintList.value.push(
...normalizeFootPrintRecords(res.result.records)
);
}
})
.catch(() => {});
ServeGetOrderPrint(footPrintParams, SIDE_REQUEST_OPTIONS)
.then((res) => {
if (res?.code === 200 && res?.result?.records) {
orderPrintList.value.push(
...normalizeOrderRecords(res.result.records)
);
}
})
.catch(() => {});
}
function fetchStoreSideData() {
ServeStoreGetFootPrint(footPrintParams, SIDE_REQUEST_OPTIONS)
.then((res) => {
if (res?.result?.records) {
footPrintList.value.push(
...normalizeFootPrintRecords(res.result.records)
);
}
})
.catch(() => {});
ServeStoreGetOrderPrint(footPrintParams, SIDE_REQUEST_OPTIONS)
.then((res) => {
if (res?.code === 200 && res?.result?.records) {
orderPrintList.value.push(
...normalizeOrderRecords(res.result.records)
);
}
})
.catch(() => {});
}
function submitSendMessage(record, context, messageType) {
SocketInstance.emit("event_talk", record);
talkStore.UPDATE_TALK_ITEM({
index_name: index_name.value,
draft_text: "",
});
const insterChat = {
createTime: formateDateAndTimeToString(new Date()),
fromUser: props.id,
toUser: record.to,
isRead: false,
messageType: messageType,
text: context,
float: "right",
};
dialogueStore.PUSH_DIALOGUE(insterChat);
let el = document.getElementById("lumenChatPanel");
let isBottom =
Math.ceil(el.scrollTop) + el.clientHeight >= el.scrollHeight;
if (isBottom || record.to == props.id) {
nextTick(() => {
el.scrollTop = el.scrollHeight;
});
} else {
talkStore.SET_TLAK_UNREAD_MESSAGE({
content: context,
nickname: record.name,
});
}
}
function formateDateAndTimeToString(date) {
var hours = date.getHours();
var mins = date.getMinutes();
var secs = date.getSeconds();
var msecs = date.getMilliseconds();
if (hours < 10) hours = "0" + hours;
if (mins < 10) mins = "0" + mins;
if (secs < 10) secs = "0" + secs;
if (msecs < 10) secs = "0" + msecs;
return (
formatDateToString(date) + " " + hours + ":" + mins + ":" + secs
);
}
function formatDateToString(date) {
var year = date.getFullYear();
var month = date.getMonth() + 1;
var day = date.getDate();
if (month < 10) month = "0" + month;
if (day < 10) day = "0" + day;
return year + "-" + month + "-" + day;
}
</script>
<style scoped lang="less">
:deep(.el-tabs__nav ) {
height: 60px;
line-height: 60px;
}
:deep(.el-tab-pane ) {
// margin-left: 12px;
}
:deep(.el-tabs__nav-scroll) {
min-width: 362px;
}
:deep(.el-tabs__item) {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
:deep(.el-tabs__header) {
margin-bottom: 0;
}
/* 高度链:tabs 撑满面板,pane 高度传递给内部滚动容器 */
:deep(.el-tabs) {
height: 100%;
display: flex;
flex-direction: column;
}
:deep(.el-tabs__content) {
flex: 1;
min-height: 0;
}
:deep(.el-tab-pane) {
height: 100%;
}
</style>