|
|
@@ -16,9 +16,25 @@
|
|
|
</div>
|
|
|
<div v-else class="message-list">
|
|
|
<div v-for="(msg, index) in messages" :key="msg.id || index" :class="['message-item', isMine(msg) ? 'mine' : 'other']">
|
|
|
- <el-avatar class="msg-avatar" :src="isMine(msg) ? myAvatar : ownerAvatar" :size="40">
|
|
|
- <!-- <span class="avatar-fallback">{{ (isMine(msg) ? myName : ownerName).slice(0, 1) }}</span> -->
|
|
|
- </el-avatar>
|
|
|
+ <!-- 与 dynamicManagement/myDynamic 一致:有图用 headImg 链,无图用灰底 + Avatar 图标(勿用 localhost 假地址) -->
|
|
|
+ <div class="msg-avatar">
|
|
|
+ <template v-if="isMine(msg)">
|
|
|
+ <img v-if="myAvatarUrl" :src="myAvatarUrl" class="msg-avatar-img" alt="" />
|
|
|
+ <div v-else class="msg-avatar-fallback" aria-hidden="true">
|
|
|
+ <el-icon :size="22">
|
|
|
+ <Avatar />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <img v-if="ownerAvatarDisplay" :src="ownerAvatarDisplay" class="msg-avatar-img" alt="" />
|
|
|
+ <div v-else class="msg-avatar-fallback" aria-hidden="true">
|
|
|
+ <el-icon :size="22">
|
|
|
+ <Avatar />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
<div class="msg-body">
|
|
|
<div class="msg-time">
|
|
|
{{ msg.createdTime }}
|
|
|
@@ -96,17 +112,17 @@
|
|
|
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch } from "vue";
|
|
|
import { useRoute, useRouter } from "vue-router";
|
|
|
import { ElMessage } from "element-plus";
|
|
|
-import { ArrowLeft, Loading, Picture, VideoPlay } from "@element-plus/icons-vue";
|
|
|
+import { ArrowLeft, Avatar, Loading, Picture, VideoPlay } from "@element-plus/icons-vue";
|
|
|
import { useWebSocketStore } from "@/stores/modules/websocket";
|
|
|
import { getChatRecord, messageRead, socketStatus, uploadChatFile } from "@/api/modules/storeDecoration";
|
|
|
import { localGet } from "@/utils";
|
|
|
+import { getWebSocketBase } from "@/utils/wsBase";
|
|
|
|
|
|
const route = useRoute();
|
|
|
const router = useRouter();
|
|
|
const socketStore = useWebSocketStore();
|
|
|
|
|
|
-// WebSocket 基础地址(与商家端一致)
|
|
|
-const WS_BASE = (import.meta.env.VITE_WS_BASE || "ws://120.26.186.130:8000/alienStore/socket/").replace(/\/$/, "");
|
|
|
+const isWsReady = () => socketStore.isSocketOpen();
|
|
|
|
|
|
// 会话信息
|
|
|
const sendId = ref("");
|
|
|
@@ -128,14 +144,23 @@ let statusTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
|
|
const userInfo = computed(() => localGet("geeker-user")?.userInfo || {});
|
|
|
|
|
|
-// 自己头像与名称(用于气泡旁头像)
|
|
|
-const myAvatar = computed(
|
|
|
- () =>
|
|
|
- userInfo.value?.headImg ||
|
|
|
- userInfo.value?.avatar ||
|
|
|
- userInfo.value?.userImage ||
|
|
|
- "http://localhost:5173/static/activity/avatar.svg"
|
|
|
-);
|
|
|
+/** 与 myDynamic.vue「cachedHeadImg」一致:geeker-user 的 headImg → avatar;无有效地址则空串走图标占位 */
|
|
|
+function pickUserHeadUrl(u: Record<string, any> | undefined): string {
|
|
|
+ if (!u) return "";
|
|
|
+ const raw = u.headImg || u.avatar || u.userImage || "";
|
|
|
+ const s = String(raw).trim();
|
|
|
+ return s;
|
|
|
+}
|
|
|
+
|
|
|
+const myAvatarUrl = computed(() => pickUserHeadUrl(userInfo.value as Record<string, any>));
|
|
|
+
|
|
|
+const ownerAvatarDisplay = computed(() => {
|
|
|
+ const s = String(ownerAvatar.value ?? "").trim();
|
|
|
+ if (!s) return "";
|
|
|
+ // 历史占位:无效本地路径,与 myDynamic 无头像态一致改用语义占位
|
|
|
+ if (s.includes("localhost:5173/static/activity/avatar.svg")) return "";
|
|
|
+ return s;
|
|
|
+});
|
|
|
|
|
|
const isMine = (msg: any) => String(msg.senderId || "") === String(sendId.value);
|
|
|
|
|
|
@@ -151,7 +176,7 @@ const scrollToBottom = () => {
|
|
|
const handleSend = async () => {
|
|
|
const text = inputText.value?.trim();
|
|
|
if (!text || !receiverId.value) return;
|
|
|
- if (!socketStore.isConnected) {
|
|
|
+ if (!isWsReady()) {
|
|
|
ElMessage.warning("连接已断开,请稍后重试");
|
|
|
return;
|
|
|
}
|
|
|
@@ -203,7 +228,7 @@ const handleImageSelect = async (e: Event) => {
|
|
|
const file = target.files?.[0];
|
|
|
target.value = "";
|
|
|
if (!file || !receiverId.value) return;
|
|
|
- if (!socketStore.isConnected) {
|
|
|
+ if (!isWsReady()) {
|
|
|
ElMessage.warning("连接已断开,请稍后重试");
|
|
|
return;
|
|
|
}
|
|
|
@@ -253,7 +278,7 @@ const handleVideoSelect = async (e: Event) => {
|
|
|
const file = target.files?.[0];
|
|
|
target.value = "";
|
|
|
if (!file || !receiverId.value) return;
|
|
|
- if (!socketStore.isConnected) {
|
|
|
+ if (!isWsReady()) {
|
|
|
ElMessage.warning("连接已断开,请稍后重试");
|
|
|
return;
|
|
|
}
|
|
|
@@ -344,13 +369,18 @@ const initWebSocket = async () => {
|
|
|
return;
|
|
|
}
|
|
|
sendId.value = `store_${phone}`;
|
|
|
+ // 与打包 mode 无关:HTTPS 走 wss://当前域名/alienStore/socket,避免 .env.production 仍为 ws 导致混合内容拦截
|
|
|
+ const WS_BASE = getWebSocketBase();
|
|
|
const wsUrl =
|
|
|
WS_BASE.startsWith("wss") || WS_BASE.startsWith("ws")
|
|
|
? `${WS_BASE}/store_${phone}`
|
|
|
: WS_BASE.replace("https", "wss").replace("http", "ws") + `/store_${phone}`;
|
|
|
|
|
|
- if (!socketStore.isConnected || socketStore.lastConnectedUrl !== wsUrl) {
|
|
|
- await socketStore.connect(wsUrl);
|
|
|
+ if (!isWsReady() || socketStore.lastConnectedUrl !== wsUrl) {
|
|
|
+ const ok = await socketStore.connect(wsUrl);
|
|
|
+ if (!ok || !isWsReady()) {
|
|
|
+ ElMessage.error("即时通讯连接失败,请确认 Nginx 已代理 /alienStore/socket/ 后刷新重试");
|
|
|
+ }
|
|
|
}
|
|
|
if (cleanMessageFn) {
|
|
|
cleanMessageFn();
|
|
|
@@ -435,9 +465,7 @@ watch(
|
|
|
if (q?.receiverId) {
|
|
|
receiverId.value = String(q.receiverId);
|
|
|
ownerName.value = decodeURIComponent(String(q.uName || q.ownerName || "业主"));
|
|
|
- ownerAvatar.value = q.userImage
|
|
|
- ? decodeURIComponent(String(q.userImage))
|
|
|
- : "http://localhost:5173/static/activity/avatar.svg";
|
|
|
+ ownerAvatar.value = q.userImage ? decodeURIComponent(String(q.userImage)) : "";
|
|
|
requirementId.value = String(q.id || q.requirementId || "");
|
|
|
loadChatRecord();
|
|
|
}
|
|
|
@@ -448,9 +476,7 @@ watch(
|
|
|
onMounted(async () => {
|
|
|
receiverId.value = String(route.query.receiverId || "");
|
|
|
ownerName.value = decodeURIComponent(String(route.query.uName || route.query.ownerName || "业主"));
|
|
|
- ownerAvatar.value = route.query.userImage
|
|
|
- ? decodeURIComponent(String(route.query.userImage))
|
|
|
- : "http://localhost:5173/static/activity/avatar.svg";
|
|
|
+ ownerAvatar.value = route.query.userImage ? decodeURIComponent(String(route.query.userImage)) : "";
|
|
|
requirementId.value = String(route.query.id || route.query.requirementId || "");
|
|
|
|
|
|
if (!receiverId.value) {
|
|
|
@@ -520,17 +546,29 @@ onBeforeUnmount(() => {
|
|
|
align-items: flex-start;
|
|
|
.msg-avatar {
|
|
|
flex-shrink: 0;
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
overflow: hidden;
|
|
|
- .avatar-fallback {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: center;
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- font-size: 16px;
|
|
|
- color: #ffffff;
|
|
|
- background: #c0c4cc;
|
|
|
- }
|
|
|
+ border-radius: 50%;
|
|
|
+ }
|
|
|
+ .msg-avatar-img {
|
|
|
+ display: block;
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ object-fit: cover;
|
|
|
+ border-radius: 50%;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 与 myDynamic .user-avatar-large 无图态一致:浅灰圆底 + User 轮廓图标 */
|
|
|
+ .msg-avatar-fallback {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ color: #909399;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 50%;
|
|
|
}
|
|
|
&.mine {
|
|
|
flex-direction: row-reverse;
|