| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- <template>
- <HeaderNav headerText="商家昵称" headerRouter="/" />
- <div class="customer-service">
- <!-- 聊天消息区域 -->
- <div class="chat-box" ref="chatBoxRef">
- <div
- v-for="msg in msgList"
- :key="msg.id"
- class="msg-item"
- :class="{ self: msg.isSelf }">
- <img class="avatar" :src="msg.isSelf ? userAvatar : serviceAvatar" />
- <div class="msg-content">
- <!-- 文本消息 -->
- <div v-if="msg.messageType === 'text'" class="text">
- {{ msg.text }}
- </div>
- <!-- 图片消息 -->
- <img
- v-else-if="msg.messageType === 'image'"
- class="img-msg"
- :src="msg.text"
- alt="图片消息" />
- <!-- 时间/已读显示 -->
- <div class="time">
- <!-- 最后一条我的消息 已读 -->
- <template v-if="msg.isSelf && msg.read && msg.id === lastSelfMsgId">
- 已读
- </template>
- <!-- 其他显示时间 -->
- <template v-else>
- {{ formatTime(msg.time) }}
- </template>
- </div>
- </div>
- </div>
- </div>
- <!-- 底部输入栏 -->
- <div class="bottom-bar">
- <input
- v-model="inputValue"
- type="text"
- placeholder="请输入内容..."
- class="input pf500 fs14" />
- <img
- src="@/assets/icon/asset/send.png"
- class="send-btn"
- @click="sendMessage"
- alt="" />
- </div>
- </div>
- </template>
- <script setup>
- import { ref, onMounted, nextTick, onUnmounted, computed } from "vue";
- import { useRoute } from "vue-router";
- import { GetOTCChatDetails } from "@/api/otc";
- import HeaderNav from "@/views/index/components/HeaderNav.vue";
- const route = useRoute();
- // 用户头像(自己)
- const userAvatar = require("@/assets/img/asset/head-img.png");
- // 商家头像(对方)
- const serviceAvatar = require("@/assets/img/index/user/default-head.png");
- let ws = null;
- const msgList = ref([]);
- const inputValue = ref("");
- const chatBoxRef = ref(null);
- // 自动滚动到底部
- function scrollToBottom() {
- nextTick(() => {
- if (chatBoxRef.value) {
- chatBoxRef.value.scrollTop = chatBoxRef.value.scrollHeight;
- }
- });
- }
- // 格式化时间
- function formatTime(t) {
- if (!t) return "";
- const d = new Date(t);
- return `${d.getHours().toString().padStart(2, "0")}:${d
- .getMinutes()
- .toString()
- .padStart(2, "0")}`;
- }
- // 最后一条自己的消息 ID(用于显示“已读”)
- const lastSelfMsgId = computed(() => {
- const selfMsgs = msgList.value.filter((m) => m.isSelf);
- return selfMsgs.length ? selfMsgs[selfMsgs.length - 1].id : null;
- });
- async function loadHistory() {
- const res = await GetOTCChatDetails(route.params.chatId);
- msgList.value = res.list.map((item) => ({
- id: item.id,
- text: item.context,
- time: item.update_time,
- read: item.is_read,
- isSelf: !item.is_otc, // true=右侧(自己),false=左侧(商家)
- messageType: "text",
- }));
- scrollToBottom();
- }
- /* ==========================================================
- ② 发送消息
- ========================================================== */
- function sendMessage() {
- if (!inputValue.value.trim()) return;
- const msg = {
- id: Date.now(),
- otc: route.params.otcId,
- text: inputValue.value,
- time: Date.now(),
- isSelf: true,
- type: "chat",
- messageType: "text",
- read: false,
- };
- msgList.value.push(msg);
- // WebSocket 发送
- if (ws && ws.readyState === WebSocket.OPEN) {
- ws.send(JSON.stringify(msg));
- }
- inputValue.value = "";
- scrollToBottom();
- }
- /* ==========================================================
- ③ WebSocket 处理
- ========================================================== */
- const initWebSocket = () => {
- const token = localStorage.getItem("token");
- ws = new WebSocket(`ws://63.141.230.43:57676/ws/custom_chat/?token=${token}`);
- ws.onopen = () => {
- console.log("WebSocket 已连接");
- };
- ws.onmessage = (e) => {
- let data = {};
- try {
- data = JSON.parse(e.data);
- } catch {
- return;
- }
- console.log("收到数据:", data);
- /* --- 已读处理 --- */
- if (data.message?.type === "read") {
- const lastSelf = [...msgList.value].reverse().find((m) => m.isSelf && !m.read);
- if (lastSelf) lastSelf.read = true;
- return;
- }
- /* --- 普通消息 --- */
- const text = data.message?.text || "";
- const isImage = /^https?:\/\/.+\.(png|jpg|jpeg|gif|webp)(\?.*)?$/i.test(text);
- msgList.value.push({
- id: Date.now(),
- text,
- time: Date.now(),
- isSelf: false,
- messageType: isImage ? "image" : "text",
- read: false,
- });
- scrollToBottom();
- };
- ws.onerror = () => {
- console.error("WebSocket 错误");
- };
- ws.onclose = () => {
- console.log("WebSocket 已关闭");
- };
- };
- onMounted(() => {
- loadHistory(); // ← 添加历史记录加载
- initWebSocket(); // ← WebSocket 初始化
- });
- onUnmounted(() => {
- ws && ws.close();
- });
- </script>
- <style lang="less" scoped>
- .customer-service {
- display: flex;
- flex-direction: column;
- padding-top: 50px;
- width: 375px;
- height: 100vh;
- box-sizing: border-box;
- }
- /* 聊天内容区域 */
- .chat-box {
- flex: 1;
- overflow-y: auto;
- padding: 12px;
- background: #f5f5f5;
- }
- .msg-item {
- display: flex;
- margin-bottom: 12px;
- &.self {
- flex-direction: row-reverse;
- .msg-content {
- align-items: flex-end;
- }
- .text {
- background: #4dabf7;
- color: #fff;
- }
- }
- }
- .avatar {
- width: 40px;
- height: 40px;
- border-radius: 50%;
- }
- .msg-content {
- display: flex;
- flex-direction: column;
- margin: 0 10px;
- .text {
- max-width: 70vw;
- background: #ffffff;
- padding: 8px 12px;
- border-radius: 8px;
- line-height: 1.5;
- font-size: 14px;
- }
- .img-msg {
- max-width: 180px;
- max-height: 400px;
- display: block;
- border-radius: 10px;
- }
- .time {
- margin-top: 4px;
- font-size: 12px;
- color: #999;
- }
- }
- /* 底部栏 */
- .bottom-bar {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
- padding-left: 16px;
- padding-right: 16px;
- border-top: 1px solid #ddd;
- height: 70px;
- box-sizing: border-box;
- .input {
- padding-left: 12px;
- width: 283px;
- height: 48px;
- border: 1px solid #ddd;
- border-radius: 40px;
- outline: none;
- box-sizing: border-box;
- }
- .send-btn {
- margin-left: 12px;
- width: 48px;
- height: 48px;
- }
- }
- </style>
|