取消enx活动图片 费率改为1%
This commit is contained in:
@@ -1,16 +1,19 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view class="page" />
|
||||
|
||||
<!-- <ChatWidget /> -->
|
||||
</div>
|
||||
</template>
|
||||
<script >
|
||||
import ChatWidget from '../src/components/ChatWidget.vue';
|
||||
import { Debounce, throttle } from '@/utils/publicMethods';
|
||||
import Vue from 'vue'
|
||||
export default {
|
||||
name: 'App',
|
||||
|
||||
|
||||
components: {
|
||||
ChatWidget
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
|
||||
68
mining-pool/src/api/customerService.js
Normal file
68
mining-pool/src/api/customerService.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
//历史聊天记录查询 用户查询七天前的聊天信息
|
||||
export function getHistory() {
|
||||
return request({
|
||||
url: `chat/message/find/history/message`,
|
||||
method: 'get',
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
//历史聊天记录查询 查询七天内记录
|
||||
export function getHistory7() {
|
||||
return request({
|
||||
url: `chat/message/find/recently/message`,
|
||||
method: 'get',
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
//用户点击对话框默认已读
|
||||
export function getReadMessage(data) {
|
||||
return request({
|
||||
url: `chat/message/read/message`,
|
||||
method: 'post',
|
||||
data
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
//聊天列表
|
||||
export function getRoomList() {
|
||||
return request({
|
||||
url: `/chat/rooms/find/room/list`,
|
||||
method: 'get',
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//重要聊天标记
|
||||
export function getUpdateRoom(data) {
|
||||
return request({
|
||||
url: `/chat/rooms/update/room`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
//图片上传接口
|
||||
export function getFileUpdate(data) {
|
||||
return request({
|
||||
url: `file/update`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
//图根据当前用户邮箱查询聊天室id
|
||||
export function getUserid() {
|
||||
return request({
|
||||
url: `chat/rooms/find/room/by/userid`,
|
||||
method: 'get',
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
893
mining-pool/src/components/ChatWidget.vue
Normal file
893
mining-pool/src/components/ChatWidget.vue
Normal file
@@ -0,0 +1,893 @@
|
||||
<template>
|
||||
<div class="chat-widget">
|
||||
<!-- 聊天图标 -->
|
||||
<div
|
||||
class="chat-icon"
|
||||
@click="toggleChat"
|
||||
:class="{ active: isChatOpen }"
|
||||
aria-label="打开客服聊天"
|
||||
tabindex="0"
|
||||
@keydown.enter="toggleChat"
|
||||
@keydown.space="toggleChat"
|
||||
>
|
||||
<i class="el-icon-chat-dot-round"></i>
|
||||
<span v-if="unreadMessages > 0" class="unread-badge">{{
|
||||
unreadMessages
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- 聊天对话框 -->
|
||||
<transition name="chat-slide">
|
||||
<div v-show="isChatOpen" class="chat-dialog">
|
||||
<div class="chat-header">
|
||||
<div class="chat-title">{{ $t("chat.title") || "在线客服" }}</div>
|
||||
<div class="chat-actions">
|
||||
<i class="el-icon-minus" @click="minimizeChat"></i>
|
||||
<i class="el-icon-close" @click="closeChat"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-body" ref="chatBody">
|
||||
<!-- 连接状态提示 -->
|
||||
<div v-if="connectionStatus === 'connecting'" class="chat-status connecting">
|
||||
<i class="el-icon-loading"></i>
|
||||
<p>正在连接客服系统...</p>
|
||||
</div>
|
||||
<div v-else-if="connectionStatus === 'error'" class="chat-status error">
|
||||
<i class="el-icon-warning"></i>
|
||||
<p>连接失败,请稍后重试</p>
|
||||
<button @click="connectWebSocket" class="retry-button">重试连接</button>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<template v-else>
|
||||
<div v-if="messages.length === 0" class="chat-empty">
|
||||
{{ $t("chat.welcome") || "欢迎使用在线客服,请问有什么可以帮您?" }}
|
||||
</div>
|
||||
<div
|
||||
v-for="(msg, index) in messages"
|
||||
:key="index"
|
||||
class="chat-message"
|
||||
:class="{
|
||||
'chat-message-user': msg.type === 'user',
|
||||
'chat-message-system': msg.type === 'system',
|
||||
}"
|
||||
>
|
||||
<div class="message-avatar">
|
||||
<i v-if="msg.type === 'system'" class="el-icon-service"></i>
|
||||
<i v-else class="el-icon-user"></i>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<!-- 文本消息 -->
|
||||
<div v-if="!msg.isImage" class="message-text">{{ msg.text }}</div>
|
||||
|
||||
<!-- 图片消息 -->
|
||||
<div v-else class="message-image">
|
||||
<img :src="msg.imageUrl" @click="previewImage(msg.imageUrl)" alt="聊天图片" />
|
||||
</div>
|
||||
|
||||
<div class="message-time">{{ formatTime(msg.time) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="chat-footer">
|
||||
<div class="chat-toolbar">
|
||||
<label for="imageUpload" class="image-upload-label" :class="{ 'disabled': connectionStatus !== 'connected' }">
|
||||
<i class="el-icon-picture-outline"></i>
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id="imageUpload"
|
||||
ref="imageUpload"
|
||||
accept="image/*"
|
||||
@change="handleImageUpload"
|
||||
style="display: none;"
|
||||
:disabled="connectionStatus !== 'connected'"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
class="chat-input"
|
||||
v-model="inputMessage"
|
||||
@keyup.enter="sendMessage"
|
||||
:placeholder="$t('chat.inputPlaceholder') || '请输入您的问题...'"
|
||||
:disabled="connectionStatus !== 'connected'"
|
||||
/>
|
||||
<button
|
||||
class="chat-send"
|
||||
@click="sendMessage"
|
||||
:disabled="connectionStatus !== 'connected' || !inputMessage.trim()"
|
||||
>
|
||||
{{ $t("chat.send") || "发送" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 图片预览 -->
|
||||
<div v-if="showImagePreview" class="image-preview-overlay" @click="closeImagePreview">
|
||||
<div class="image-preview-container">
|
||||
<img :src="previewImageUrl" class="preview-image" />
|
||||
<i class="el-icon-close preview-close" @click="closeImagePreview"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Client } from '@stomp/stompjs';
|
||||
import { getUserid } from '../api/customerService';
|
||||
export default {
|
||||
name: "ChatWidget",
|
||||
data() {
|
||||
return {
|
||||
isChatOpen: false,
|
||||
inputMessage: "",
|
||||
messages: [],
|
||||
unreadMessages: 0,
|
||||
// 图片预览相关
|
||||
showImagePreview: false,
|
||||
previewImageUrl: '',
|
||||
// WebSocket 相关
|
||||
stompClient: null,
|
||||
connectionStatus: 'disconnected', // disconnected, connecting, connected, error
|
||||
userType: 0, // 0 游客 1 登录用户 2 客服
|
||||
userEmail: '', // 用户标识
|
||||
// 自动回复配置
|
||||
autoResponses: {
|
||||
hello: "您好,有什么可以帮助您的?",
|
||||
你好: "您好,有什么可以帮助您的?",
|
||||
hi: "您好,有什么可以帮助您的?",
|
||||
挖矿: "您可以查看我们的挖矿教程,或者直接创建矿工账户开始挖矿。",
|
||||
算力: "您可以在首页查看当前的矿池算力和您的个人算力。",
|
||||
收益: "收益根据您的算力贡献按比例分配,详情可以查看收益计算器。",
|
||||
帮助: "您可以查看我们的帮助文档,或者提交工单咨询具体问题。",
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener("click", this.handleClickOutside);
|
||||
|
||||
},
|
||||
methods: {
|
||||
|
||||
async fetchUserid(){
|
||||
const res = await getUserid();
|
||||
if(res &&res.code == 200){
|
||||
this.roomId = res.data;
|
||||
console.log(res,"及附加覅");
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
|
||||
// 初始化 WebSocket 连接
|
||||
initWebSocket() {
|
||||
this.determineUserType();
|
||||
this.connectWebSocket();
|
||||
},
|
||||
|
||||
// 确定用户类型和邮箱
|
||||
determineUserType() {
|
||||
try {
|
||||
const token = JSON.parse(localStorage.getItem('token') || '{}');
|
||||
const userInfo = JSON.parse(localStorage.getItem('jurisdiction') || '{}');
|
||||
const email = JSON.parse(localStorage.getItem('userEmail') || '{}');
|
||||
if (token) {
|
||||
if (userInfo.roleKey === 'customer_service') {
|
||||
// 客服用户
|
||||
this.userType = 2;
|
||||
} else {
|
||||
// 登录用户
|
||||
this.userType = 1;
|
||||
}
|
||||
this.userEmail =email;
|
||||
}else{//游客
|
||||
this.userType = 0;
|
||||
this.userEmail = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
// 出错时默认为游客
|
||||
this.userType = 0;
|
||||
this.userEmail = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
},
|
||||
|
||||
// 连接 WebSocket
|
||||
connectWebSocket() {
|
||||
this.connectionStatus = 'connecting';
|
||||
|
||||
try {
|
||||
const wsUrl = `${process.env.VUE_APP_BASE_API}chat/ws`;
|
||||
|
||||
// 创建 STOMP 客户端
|
||||
this.stompClient = new Client({
|
||||
brokerURL: wsUrl,
|
||||
connectHeaders: {
|
||||
'email': this.userEmail,
|
||||
'type': this.userType
|
||||
},
|
||||
debug: function(str) {
|
||||
console.log('STOMP: ' + str);
|
||||
},
|
||||
reconnectDelay: 5000,
|
||||
heartbeatIncoming: 4000,
|
||||
heartbeatOutgoing: 4000
|
||||
});
|
||||
|
||||
// 连接成功回调
|
||||
this.stompClient.onConnect = (frame) => {
|
||||
console.log('连接成功: ' + frame);
|
||||
this.connectionStatus = 'connected';
|
||||
|
||||
// 订阅个人消息频道
|
||||
this.stompClient.subscribe(`${process.env.VUE_APP_BASE_API}user/queue/${this.userEmail}`, this.onMessageReceived);
|
||||
|
||||
// 根据用户类型显示不同的欢迎消息
|
||||
let welcomeMessage = '';
|
||||
switch(this.userType) {
|
||||
case 0:
|
||||
welcomeMessage = '您当前以游客身份访问,请问有什么可以帮您?';
|
||||
break;
|
||||
case 1:
|
||||
welcomeMessage = '欢迎回来,请问有什么可以帮您?';
|
||||
break;
|
||||
case 2:
|
||||
welcomeMessage = '您已以客服身份登录系统';
|
||||
break;
|
||||
}
|
||||
this.addSystemMessage(welcomeMessage);
|
||||
};
|
||||
|
||||
// 连接错误回调
|
||||
this.stompClient.onStompError = (frame) => {
|
||||
console.error('连接错误: ' + frame.headers.message);
|
||||
this.connectionStatus = 'error';
|
||||
this.addSystemMessage('连接客服系统失败,请稍后重试。');
|
||||
};
|
||||
|
||||
// 启动连接
|
||||
this.stompClient.activate();
|
||||
} catch (error) {
|
||||
console.error('初始化 WebSocket 失败:', error);
|
||||
this.connectionStatus = 'error';
|
||||
}
|
||||
},
|
||||
|
||||
// 断开 WebSocket 连接
|
||||
disconnectWebSocket() {
|
||||
if (this.stompClient && this.stompClient.connected) {
|
||||
this.stompClient.deactivate();
|
||||
this.connectionStatus = 'disconnected';
|
||||
}
|
||||
},
|
||||
|
||||
// 接收消息处理
|
||||
onMessageReceived(message) {
|
||||
console.log('收到消息:', message.body);
|
||||
try {
|
||||
const data = JSON.parse(message.body);
|
||||
|
||||
// 添加客服消息
|
||||
this.messages.push({
|
||||
type: 'system',
|
||||
text: data.content,
|
||||
isImage: data.type === 'image',
|
||||
imageUrl: data.type === 'image' ? data.content : null,
|
||||
time: new Date(),
|
||||
});
|
||||
|
||||
// 如果聊天窗口没有打开,显示未读消息数
|
||||
if (!this.isChatOpen) {
|
||||
this.unreadMessages++;
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('解析消息失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 切换聊天窗口
|
||||
toggleChat() {
|
||||
this.isChatOpen = !this.isChatOpen;
|
||||
|
||||
if (this.isChatOpen) {
|
||||
this.unreadMessages = 0;
|
||||
|
||||
// 如果未连接,则连接 WebSocket
|
||||
if (this.connectionStatus === 'disconnected') {
|
||||
this.initWebSocket();
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
minimizeChat() {
|
||||
this.isChatOpen = false;
|
||||
},
|
||||
|
||||
closeChat() {
|
||||
this.isChatOpen = false;
|
||||
this.messages = [];
|
||||
this.disconnectWebSocket();
|
||||
},
|
||||
|
||||
// 发送消息
|
||||
sendMessage() {
|
||||
if (!this.inputMessage.trim() || this.connectionStatus !== 'connected') return;
|
||||
|
||||
const messageText = this.inputMessage.trim();
|
||||
|
||||
// 添加用户消息到界面
|
||||
this.messages.push({
|
||||
type:0,// 0 文本 1图片
|
||||
text: messageText,
|
||||
isImage: false,
|
||||
time: new Date(),
|
||||
email:"",// 接收者邮箱?
|
||||
receiveUserType:2,// 接受用户类型0 游客 1 登录用户 2 客服人员
|
||||
sendUserType:this.userType,// 发送者类型0 游客 1 登录用户 2 客服人员
|
||||
roomId:this.roomId,// 聊天室ID
|
||||
});
|
||||
|
||||
// 通过 WebSocket 发送消息
|
||||
if (this.stompClient && this.stompClient.connected) {
|
||||
this.stompClient.publish({
|
||||
destination: '/send/message',
|
||||
body: JSON.stringify({
|
||||
content: messageText,
|
||||
type: 'text'
|
||||
})
|
||||
});
|
||||
} else {
|
||||
// 如果连接失败,使用自动回复
|
||||
this.handleAutoResponse(messageText);
|
||||
}
|
||||
|
||||
// 清空输入框
|
||||
this.inputMessage = "";
|
||||
|
||||
// 滚动到底部
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
},
|
||||
|
||||
// 添加系统消息
|
||||
addSystemMessage(text) {
|
||||
this.messages.push({
|
||||
type: "system",
|
||||
text: text,
|
||||
isImage: false,
|
||||
time: new Date(),
|
||||
});
|
||||
|
||||
// 滚动到底部
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
},
|
||||
|
||||
// 自动回复 (仅在无法连接服务器时使用)
|
||||
handleAutoResponse(message) {
|
||||
setTimeout(() => {
|
||||
let response = "抱歉,我暂时无法回答这个问题。请联系真人客服或提交工单。";
|
||||
|
||||
// 检查是否匹配自动回复关键词
|
||||
for (const [keyword, reply] of Object.entries(this.autoResponses)) {
|
||||
if (message.toLowerCase().includes(keyword.toLowerCase())) {
|
||||
response = reply;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加系统回复
|
||||
this.messages.push({
|
||||
type: "system",
|
||||
text: response,
|
||||
isImage: false,
|
||||
time: new Date(),
|
||||
});
|
||||
|
||||
if (!this.isChatOpen) {
|
||||
this.unreadMessages++;
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
if (this.$refs.chatBody) {
|
||||
this.$refs.chatBody.scrollTop = this.$refs.chatBody.scrollHeight;
|
||||
}
|
||||
},
|
||||
|
||||
formatTime(date) {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
},
|
||||
|
||||
handleClickOutside(event) {
|
||||
if (this.isChatOpen) {
|
||||
const chatElement = this.$el.querySelector('.chat-dialog');
|
||||
const chatIcon = this.$el.querySelector('.chat-icon');
|
||||
this.fetchUserid();
|
||||
if (chatElement && !chatElement.contains(event.target) && !chatIcon.contains(event.target)) {
|
||||
this.isChatOpen = false;
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 处理图片上传
|
||||
handleImageUpload(event) {
|
||||
if (this.connectionStatus !== 'connected') return;
|
||||
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
// 检查是否为图片
|
||||
if (!file.type.startsWith('image/')) {
|
||||
this.$message({
|
||||
message: this.$t('chat.onlyImages') || '只能上传图片文件!',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件大小 (限制为5MB)
|
||||
const maxSize = 5 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
this.$message({
|
||||
message: this.$t('chat.imageTooLarge') || '图片大小不能超过5MB!',
|
||||
type: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const imageUrl = e.target.result;
|
||||
|
||||
// 添加用户图片消息到界面
|
||||
this.messages.push({
|
||||
type:1,// 0 文本 1图片
|
||||
text: "",
|
||||
isImage: true,
|
||||
imageUrl: imageUrl,
|
||||
time: new Date(),
|
||||
email:"",// 接收者邮箱?
|
||||
receiveUserType:2,// 接受用户类型0 游客 1 登录用户 2 客服人员
|
||||
sendUserType:this.userType,// 发送者类型0 游客 1 登录用户 2 客服人员
|
||||
roomId:this.roomId,// 聊天室ID
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 通过 WebSocket 发送图片消息
|
||||
if (this.stompClient && this.stompClient.connected) {
|
||||
this.stompClient.publish({
|
||||
destination: '/send/message',
|
||||
body: JSON.stringify({
|
||||
content: imageUrl,
|
||||
type: 'image'
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
this.$refs.imageUpload.value = '';
|
||||
},
|
||||
|
||||
// 预览图片
|
||||
previewImage(imageUrl) {
|
||||
this.previewImageUrl = imageUrl;
|
||||
this.showImagePreview = true;
|
||||
},
|
||||
|
||||
// 关闭图片预览
|
||||
closeImagePreview() {
|
||||
this.showImagePreview = false;
|
||||
this.previewImageUrl = '';
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.disconnectWebSocket();
|
||||
document.removeEventListener("click", this.handleClickOutside);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chat-widget {
|
||||
position: fixed;
|
||||
bottom: 40px;
|
||||
right: 60px;
|
||||
z-index: 1000;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
.chat-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
background-color: #AC85E0;
|
||||
color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
|
||||
i {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
background-color: #6E3EDB;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: #6E3EDB;
|
||||
}
|
||||
}
|
||||
|
||||
.unread-badge {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: -5px;
|
||||
background-color: #e74c3c;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-dialog {
|
||||
position: absolute;
|
||||
bottom: 80px;
|
||||
right: 0;
|
||||
width: 350px;
|
||||
height: 450px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
background-color: #AC85E0;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.chat-actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
|
||||
i {
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 15px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.chat-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
i {
|
||||
font-size: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8px 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
&.connecting i {
|
||||
color: #AC85E0;
|
||||
}
|
||||
|
||||
&.error {
|
||||
i {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #e74c3c;
|
||||
}
|
||||
}
|
||||
|
||||
.retry-button {
|
||||
margin-top: 16px;
|
||||
padding: 8px 16px;
|
||||
background-color: #AC85E0;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #6E3EDB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-empty {
|
||||
color: #777;
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.chat-message {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
|
||||
&.chat-message-user {
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.message-content {
|
||||
background-color: #AC85E0;
|
||||
color: white;
|
||||
border-radius: 18px 18px 0 18px;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
text-align: right;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
&.chat-message-system {
|
||||
.message-content {
|
||||
background-color: white;
|
||||
border-radius: 18px 18px 18px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #e0e0e0;
|
||||
margin: 0 10px;
|
||||
|
||||
i {
|
||||
font-size: 18px;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 70%;
|
||||
padding: 10px 15px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.message-text {
|
||||
line-height: 1.4;
|
||||
font-size: 14px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message-image {
|
||||
img {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.chat-footer {
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-toolbar {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.image-upload-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
color: #AC85E0;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-input {
|
||||
flex: 1;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 20px;
|
||||
padding: 8px 15px;
|
||||
outline: none;
|
||||
|
||||
&:focus:not(:disabled) {
|
||||
border-color: #AC85E0;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-send {
|
||||
background-color: #AC85E0;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
padding: 8px 15px;
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: #6E3EDB;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// 图片预览
|
||||
.image-preview-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
z-index: 1100;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image-preview-container {
|
||||
position: relative;
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
max-width: 100%;
|
||||
max-height: 90vh;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.preview-close {
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: 0;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
// 动画效果
|
||||
.chat-slide-enter-active, .chat-slide-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.chat-slide-enter, .chat-slide-leave-to {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.chat-widget {
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
.chat-dialog {
|
||||
width: 300px;
|
||||
height: 400px;
|
||||
bottom: 70px;
|
||||
}
|
||||
|
||||
.message-image img {
|
||||
max-width: 150px;
|
||||
max-height: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
mining-pool/src/i18n/ChatWidget.js
Normal file
32
mining-pool/src/i18n/ChatWidget.js
Normal file
@@ -0,0 +1,32 @@
|
||||
export const ChatWidget_zh = {
|
||||
chat:{
|
||||
|
||||
title: '在线客服',
|
||||
welcome: '欢迎使用在线客服,请问有什么可以帮您?',
|
||||
placeholder: '请输入您的消息...',
|
||||
send: '发送',
|
||||
close: '关闭',
|
||||
inputPlaceholder:"请输入您的问题...",
|
||||
onlyImages:"只能上传图片文件!",
|
||||
imageTooLarge:"图片大小不能超过5MB!",
|
||||
imageReceived:"已收到您的图片,我们会尽快处理您的问题。",
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
|
||||
export const ChatWidget_en = {
|
||||
|
||||
|
||||
chat:{
|
||||
title: 'Online Customer Service',
|
||||
welcome: 'Welcome to the online customer service, what can I help you with?',
|
||||
placeholder: 'Please enter your message...',
|
||||
send: 'Send',
|
||||
close: 'Close',
|
||||
inputPlaceholder:"Please enter your question...",
|
||||
onlyImages:"Only image files can be uploaded!",
|
||||
imageTooLarge:"The image size cannot exceed 5MB!",
|
||||
imageReceived:"We have received your image, and we will handle your question as soon as possible.",
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {workOrder_zh,workOrder_en} from'./submitWorkOrder'
|
||||
import {alerts_zh,alerts_en} from'./alerts'
|
||||
import {seo_zh,seo_en} from'./seo'
|
||||
import {chooseUs_zh,chooseUs_en} from'./dataDisplay'
|
||||
import {ChatWidget_zh,ChatWidget_en} from'./ChatWidget'
|
||||
|
||||
|
||||
|
||||
@@ -30,7 +31,7 @@ export default {
|
||||
...alerts_zh,
|
||||
...seo_zh,
|
||||
...chooseUs_zh,
|
||||
|
||||
...ChatWidget_zh,
|
||||
|
||||
|
||||
},
|
||||
@@ -50,7 +51,7 @@ export default {
|
||||
...alerts_en,
|
||||
...seo_en,
|
||||
...chooseUs_en,
|
||||
|
||||
...ChatWidget_en,
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -118,6 +118,22 @@ const childrenRoutes = [
|
||||
|
||||
}
|
||||
},
|
||||
{//在线客服
|
||||
path: 'customerService',
|
||||
name: 'CustomerService',
|
||||
component: () => import('../views/customerService/index.vue'),
|
||||
meta: {title: '在线客服',
|
||||
description:i18n.t(`seo.apiFile`),
|
||||
allAuthority:[`all`],
|
||||
// keywords: 'M2Pool 矿池,API 文档,认证 token,接口调用,API file,authentication token,Interface call'
|
||||
keywords:{
|
||||
en: 'API file,authentication token,Interface call',
|
||||
zh: 'M2Pool 矿池,API 文档,认证 token,接口调用'
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
{//接入矿池页面
|
||||
path: '/:lang/AccessMiningPool',
|
||||
name: 'AccessMiningPool',
|
||||
|
||||
@@ -160,7 +160,10 @@ window.addEventListener('offline', () => {
|
||||
|
||||
service.defaults.retry = 2;// 重试次数
|
||||
service.defaults.retryDelay = 2000;
|
||||
service.defaults.shouldRetry = (error) => true
|
||||
service.defaults.shouldRetry = (error) => {
|
||||
// 只有网络错误或超时错误才进行重试
|
||||
return error.message === "Network Error" || error.message.includes("timeout");
|
||||
};
|
||||
|
||||
localStorage.setItem('superReportError', "")
|
||||
let superReportError = localStorage.getItem('superReportError')
|
||||
@@ -286,7 +289,6 @@ service.interceptors.response.use(res => {
|
||||
|
||||
|
||||
let { message } = error;
|
||||
|
||||
if (message == "Network Error" || message.includes("timeout")) {
|
||||
if (!navigator.onLine) {
|
||||
// 断网状态,添加到重试队列
|
||||
@@ -296,7 +298,7 @@ service.interceptors.response.use(res => {
|
||||
params: error.config.params,
|
||||
data: error.config.data
|
||||
});
|
||||
|
||||
|
||||
// 根据URL确定请求类型并记录回调
|
||||
let callback = null;
|
||||
if (error.config.url.includes('getPoolPower')) {
|
||||
@@ -313,7 +315,7 @@ service.interceptors.response.use(res => {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (!pendingRequests.has(requestKey)) {
|
||||
pendingRequests.set(requestKey, {
|
||||
config: error.config,
|
||||
@@ -321,17 +323,31 @@ service.interceptors.response.use(res => {
|
||||
retryCount: 0,
|
||||
callback: callback
|
||||
});
|
||||
|
||||
|
||||
console.log('请求已加入断网重连队列:', error.config.url);
|
||||
}
|
||||
} else if ((error.config.retry > 0 && error.config)) {
|
||||
// 保留现有的重试逻辑
|
||||
error.config.retry--;
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(service(error.config));
|
||||
}, 2000);
|
||||
});
|
||||
} else {
|
||||
// 网络已连接,但请求失败,尝试重试
|
||||
// 确保 config 中有 __retryCount 字段
|
||||
error.config.__retryCount = error.config.__retryCount || 0;
|
||||
|
||||
// 判断是否可以重试
|
||||
if (error.config.__retryCount < service.defaults.retry && service.defaults.shouldRetry(error)) {
|
||||
// 增加重试计数
|
||||
error.config.__retryCount += 1;
|
||||
|
||||
console.log(`[请求重试] ${error.config.url} - 第 ${error.config.__retryCount} 次重试`);
|
||||
|
||||
// 创建新的Promise等待一段时间后重试
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
resolve(service(error.config));
|
||||
}, service.defaults.retryDelay);
|
||||
});
|
||||
}
|
||||
|
||||
// 达到最大重试次数,不再重试
|
||||
console.log(`[请求失败] ${error.config.url} - 已达到最大重试次数`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,63 +391,11 @@ service.interceptors.response.use(res => {
|
||||
// 避免完全不提示,可以在控制台记录被抑制的错误
|
||||
console.log('[错误提示] 已抑制重复错误:', message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// let { message } = error;
|
||||
// if (message == "Network Error") {
|
||||
// // message = "后端接口网络连接异常,请刷新重试";
|
||||
// const now = Date.now();
|
||||
// if (now - lastNetworkErrorTime > NETWORK_ERROR_THROTTLE_TIME) {
|
||||
// lastNetworkErrorTime = now; // 更新最后提示时间
|
||||
// Message({
|
||||
// message: window.vm.$i18n.t(`home.NetworkError`),
|
||||
// type: 'error',
|
||||
// duration: 4 * 1000,
|
||||
// showClose: true
|
||||
// });
|
||||
// }
|
||||
|
||||
// }
|
||||
// else if (message.includes("timeout")) {
|
||||
// // message = "系统接口请求超时,请刷新重试";
|
||||
// Message({
|
||||
// message: window.vm.$i18n.t(`home.requestTimeout`),
|
||||
// type: 'error',
|
||||
// duration: 5 * 1000,
|
||||
// showClose: true
|
||||
// })
|
||||
|
||||
// }
|
||||
// else if (message.includes("Request failed with status code")) {
|
||||
// // message = "系统接口" + message.substr(message.length - 3) + "异常";
|
||||
// Message({
|
||||
// message: "系统接口" + message.substr(message.length - 3) + "异常",
|
||||
// type: 'error',
|
||||
// duration: 5 * 1000,
|
||||
// showClose: true
|
||||
// })
|
||||
// } else {
|
||||
|
||||
// Message({
|
||||
// message: message,
|
||||
// type: 'error',
|
||||
// duration: 5 * 1000,
|
||||
// showClose: true
|
||||
// })
|
||||
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return Promise.reject(error)
|
||||
|
||||
}
|
||||
|
||||
0
mining-pool/src/views/customerService/index.js
Normal file
0
mining-pool/src/views/customerService/index.js
Normal file
1044
mining-pool/src/views/customerService/index.vue
Normal file
1044
mining-pool/src/views/customerService/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,10 +3,10 @@
|
||||
<section v-if="$isMobile">
|
||||
|
||||
<div class="imgTop">
|
||||
<!-- <img src="../../assets/mobile/home/home.png" alt="mining" loading="lazy" /> -->
|
||||
|
||||
<img src="../../assets/mobile/home/home.png" alt="mining" loading="lazy" />
|
||||
<!--
|
||||
<img v-if="lang == 'zh'" src="../../assets/img/enx推广.png" alt="mining" loading="lazy"/>
|
||||
<img v-else src="../../assets/img/enx英文推广.png" alt="mining" loading="lazy"/>
|
||||
<img v-else src="../../assets/img/enx英文推广.png" alt="mining" loading="lazy"/> -->
|
||||
|
||||
</div>
|
||||
|
||||
@@ -329,13 +329,12 @@
|
||||
|
||||
</section>
|
||||
<div class="content" v-else v-loading="minerChartLoading">
|
||||
|
||||
<div class="bgBox">
|
||||
|
||||
<!--
|
||||
<img v-if="lang == 'zh'" class="bgBoxImg2Img" src="../../assets/img/enx推广.png" alt="mining" loading="lazy"/>
|
||||
<img v-else class="bgBoxImg2Img" src="../../assets/img/enx英文推广.png" alt="mining" loading="lazy"/>
|
||||
|
||||
|
||||
<!-- <img class="bgBoxImg" src="../../assets/img/enx推广.png" style="width: 100%;height: 100%;" alt="mining" loading="lazy"/> -->
|
||||
<img v-else class="bgBoxImg2Img" src="../../assets/img/enx英文推广.png" alt="mining" loading="lazy"/> -->
|
||||
<img class="bgImg" src="../../assets/img/home.png" alt="mining" loading="lazy"/>
|
||||
</div>
|
||||
<el-row>
|
||||
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
|
||||
@@ -785,6 +784,7 @@ export default {
|
||||
p{
|
||||
width: 100% ;
|
||||
background: transparent ;
|
||||
padding-left: 8px;
|
||||
|
||||
}
|
||||
i{
|
||||
@@ -1295,16 +1295,44 @@ export default {
|
||||
@media screen and (min-width:800px) and (max-width: 1279px) {
|
||||
.imgTop {
|
||||
width: 100%;
|
||||
// padding-left: 10%;
|
||||
text-align: center;
|
||||
padding-left: 20%;
|
||||
text-align: left;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
width: auto ;
|
||||
height:300px;
|
||||
}
|
||||
}
|
||||
#chart {
|
||||
height: 400px !important;
|
||||
}
|
||||
.describeBox2{
|
||||
width: 100%;
|
||||
font-size: 0.9rem;
|
||||
padding: 8px;
|
||||
margin: 0 auto;
|
||||
p{
|
||||
width: 100% ;
|
||||
background: transparent ;
|
||||
padding-left: 8px;
|
||||
|
||||
}
|
||||
i{
|
||||
color: #5721e4;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.describeTitle{
|
||||
color: #5721e4;
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.view{
|
||||
color: #5721e4;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.moveCurrencyBox {
|
||||
@@ -2046,7 +2074,7 @@ export default {
|
||||
.bgBox {
|
||||
// background: gold;
|
||||
width: 100%;
|
||||
// height: 380px;
|
||||
height: 300px;
|
||||
box-sizing: border-box;
|
||||
// background-position: 50% 28%;
|
||||
// background-size: cover;
|
||||
@@ -2059,12 +2087,20 @@ export default {
|
||||
// background-position: 13.2vw 0 ;
|
||||
// background-repeat: no-repeat;
|
||||
// margin-top: 50px;
|
||||
margin: 30px 0px;
|
||||
// margin: 30px 0px;
|
||||
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
.bgImg{
|
||||
height: 100%;
|
||||
width: auto ;
|
||||
position: absolute;
|
||||
left: 25vw;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.bgBoxImg {
|
||||
height: 100%;
|
||||
width: auto;
|
||||
position: absolute;
|
||||
left: 23%;
|
||||
transition: all 0.3s linear;
|
||||
|
||||
@@ -69,7 +69,7 @@ export default {
|
||||
value:"enx",
|
||||
label:"Entropyx(Enx)",
|
||||
img:`${this.$baseApi}img/enx.svg`,
|
||||
rate:"0",
|
||||
rate:"1%",
|
||||
address:"",
|
||||
mode:"PPLNS+PROPDIF",
|
||||
quota:"5000",
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<template slot="title">
|
||||
<div class="collapseTitle">
|
||||
<span ><img :src="item.img" alt="coin" loading="lazy"> {{item.label}}</span>
|
||||
<span v-if="item.value === 'enx'"> {{ $t(`course.timeLimited`) }} 0%</span>
|
||||
<span v-else>{{item.rate}}</span>
|
||||
<!-- <span v-if="item.value === 'enx'"> {{ $t(`course.timeLimited`) }} 0%</span> -->
|
||||
<span >{{item.rate}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<section class="contentBox2">
|
||||
@@ -76,8 +76,8 @@
|
||||
<li v-for="item in rateList" :key="item.value">
|
||||
<span class="coin"><img :src="item.img" alt="coin" loading="lazy"> {{item.label}}</span>
|
||||
<span>{{item.address}}</span>
|
||||
<span v-if="item.value === 'enx'"> {{ $t(`course.timeLimited`) }} 0%</span>
|
||||
<span v-else>{{item.rate}}</span>
|
||||
<!-- <span v-if="item.value === 'enx'"> {{ $t(`course.timeLimited`) }} 0%</span> -->
|
||||
<span >{{item.rate}}</span>
|
||||
<span>{{item.mode}}</span>
|
||||
<span>{{item.quota}}</span>
|
||||
</li>
|
||||
|
||||
@@ -1,153 +1,397 @@
|
||||
<template>
|
||||
<div style="width: 1300px; height: 600px">
|
||||
<div id="chart" style="width: 100%; height: 100%; min-width: 500px"></div>
|
||||
<div>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<!-- 用户ID输入部分 -->
|
||||
<div class="user-input-container">
|
||||
<div class="input-group" :class="{ 'disabled': isConnected }">
|
||||
<input
|
||||
v-model="email"
|
||||
placeholder="请输入您的用户邮箱"
|
||||
:disabled="isConnected"
|
||||
/>
|
||||
<input
|
||||
v-model="targetEmail"
|
||||
placeholder="请输入目标用户邮箱"
|
||||
:disabled="isConnected"
|
||||
@keyup.enter="!isConnected && !isConnecting && connectWebSocket()"
|
||||
/>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<button
|
||||
@click="connectWebSocket"
|
||||
:disabled="isConnected || isConnecting || !email || !targetEmail"
|
||||
:class="{ 'disabled': isConnected || isConnecting || !email || !targetEmail }"
|
||||
>
|
||||
{{ isConnecting ? '连接中...' : '连接' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="isConnected"
|
||||
@click="disconnectWebSocket"
|
||||
class="disconnect-btn"
|
||||
>
|
||||
断开连接
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="connectionError" class="error-message">
|
||||
{{ connectionError }}
|
||||
</div>
|
||||
|
||||
<div v-if="isConnected">
|
||||
<!-- WebSocket 聊天部分 -->
|
||||
<div class="chat-container">
|
||||
<div class="message-list" ref="messageList">
|
||||
<div v-for="(msg, index) in receivedMessages" :key="index" class="message"
|
||||
:class="{ 'error-message': msg.error }">
|
||||
<div v-if="typeof msg === 'string'">{{ msg }}</div>
|
||||
<div v-else>
|
||||
<div class="message-header">
|
||||
<span class="message-sender">{{ msg.sender || 'Unknown' }}</span>
|
||||
<span class="message-time">{{ formatTime(msg.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="message-content">{{ msg.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-container">
|
||||
<textarea
|
||||
v-model="message"
|
||||
@keyup.enter="sendMessage"
|
||||
placeholder="输入消息..."
|
||||
rows="3"
|
||||
></textarea>
|
||||
<button @click="sendMessage" :disabled="!message.trim()">
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as echarts from "echarts";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
countDownTime: 60,
|
||||
timer: null,
|
||||
};
|
||||
},
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
||||
import { Stomp } from '@stomp/stompjs'
|
||||
|
||||
mounted() {
|
||||
let base = +new Date(1968, 9, 3);
|
||||
let oneDay = 24 * 3600 * 1000;
|
||||
let date = [];
|
||||
let data = [Math.random() * 300];
|
||||
for (let i = 1; i < 20000; i++) {
|
||||
var now = new Date((base += oneDay));
|
||||
date.push([now.getFullYear(), now.getMonth() + 1, now.getDate()].join("/"));
|
||||
data.push(Math.round((Math.random() - 0.5) * 20 + data[i - 1]));
|
||||
const props = defineProps({
|
||||
msg: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const message = ref('')
|
||||
const receivedMessages = ref([])
|
||||
const email = ref('')
|
||||
const targetEmail = ref('')
|
||||
const isConnected = ref(false)
|
||||
const isConnecting = ref(false)
|
||||
const connectionError = ref('')
|
||||
const messageList = ref(null)
|
||||
|
||||
// 创建一个响应式的 stompClient
|
||||
const stompClient = ref(null)
|
||||
|
||||
// 添加连接时间变量
|
||||
const connectTime = ref(null)
|
||||
|
||||
// 自动滚动到底部
|
||||
const scrollToBottom = async () => {
|
||||
await nextTick()
|
||||
if (messageList.value) {
|
||||
messageList.value.scrollTop = messageList.value.scrollHeight
|
||||
}
|
||||
}
|
||||
|
||||
// 监听消息列表变化,自动滚动
|
||||
watch(receivedMessages, () => {
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timestamp) => {
|
||||
if (!timestamp) return ''
|
||||
try {
|
||||
const date = new Date(timestamp)
|
||||
return date.toLocaleTimeString()
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// 连接WebSocket
|
||||
const connectWebSocket = () => {
|
||||
if (!email.value || !targetEmail.value) {
|
||||
connectionError.value = '请输入用户邮箱和目标用户邮箱'
|
||||
return
|
||||
}
|
||||
|
||||
connectionError.value = ''
|
||||
isConnecting.value = true
|
||||
connectTime.value = new Date() // 记录连接时间
|
||||
|
||||
try {
|
||||
stompClient.value = Stomp.client('ws://localhost:8101/chat/ws')
|
||||
stompClient.value.heartbeat.outgoing = 20000
|
||||
stompClient.value.heartbeat.incoming = 0
|
||||
|
||||
const connectHeaders = {
|
||||
'email': email.value,
|
||||
'type': 2 //0 游客 1 登录用户 2 客服
|
||||
}
|
||||
|
||||
// 添加 10 个空数据在前后
|
||||
for (let i = 0; i < 100; i++) {
|
||||
date.unshift(null);
|
||||
data.unshift(null);
|
||||
}
|
||||
for (let i = 0; i < 100; i++) {
|
||||
date.push(null);
|
||||
data.push(null);
|
||||
}
|
||||
|
||||
var option = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
position: function (pt) {
|
||||
return [pt[0], "10%"];
|
||||
},
|
||||
stompClient.value.connect(
|
||||
connectHeaders,
|
||||
function(frame) {
|
||||
console.log('连接成功: ' + frame)
|
||||
console.log('连接时间:', connectTime.value?.toLocaleString())
|
||||
isConnected.value = true
|
||||
isConnecting.value = false
|
||||
|
||||
// 添加系统消息
|
||||
receivedMessages.value.push({
|
||||
sender: 'System',
|
||||
content: '已连接到聊天服务器',
|
||||
timestamp: new Date().toISOString(),
|
||||
system: true
|
||||
})
|
||||
|
||||
// 订阅自己管道的消息
|
||||
stompClient.value.subscribe(`/user/queue/${email.value}`, function(message) {
|
||||
console.log('收到消息:', message.body)
|
||||
try {
|
||||
const parsedMessage = JSON.parse(message.body)
|
||||
receivedMessages.value.push(parsedMessage)
|
||||
} catch (error) {
|
||||
console.error('消息解析失败:', error)
|
||||
receivedMessages.value.push({
|
||||
sender: 'System',
|
||||
content: `消息格式错误: ${message.body}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: true
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
title: {
|
||||
left: "center",
|
||||
text: "Large Area Chart",
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {
|
||||
yAxisIndex: "none",
|
||||
},
|
||||
restore: {},
|
||||
saveAsImage: {},
|
||||
},
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
// boundaryGap: [0.5, 0.5],
|
||||
data: date,
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: "value",
|
||||
boundaryGap: [0, "100%"],
|
||||
axisLine: {
|
||||
show: true, // 显示 Y 轴线条
|
||||
},
|
||||
},
|
||||
{
|
||||
position: "right",
|
||||
type: "value",
|
||||
boundaryGap: [0, "100%"],
|
||||
},
|
||||
],
|
||||
dataZoom: [
|
||||
{
|
||||
type: "inside",
|
||||
start: 0,
|
||||
end: 10,
|
||||
},
|
||||
{
|
||||
start: 0,
|
||||
end: 10,
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: "Fake Data",
|
||||
type: "line",
|
||||
symbol: "none",
|
||||
sampling: "lttb",
|
||||
itemStyle: {
|
||||
color: "rgb(255, 70, 131)",
|
||||
},
|
||||
data: data,
|
||||
},
|
||||
],
|
||||
};
|
||||
this.myChart = echarts.init(document.getElementById("chart"));
|
||||
this.myChart.setOption(option);
|
||||
window.addEventListener(
|
||||
"resize", () => {
|
||||
if (this.myChart) this.myChart.resize();
|
||||
function(error) {
|
||||
console.error('连接失败:', error)
|
||||
isConnected.value = false
|
||||
isConnecting.value = false
|
||||
connectionError.value = `连接失败: ${error.headers?.message || error.message || '未知错误'}`
|
||||
}
|
||||
);
|
||||
},
|
||||
methods: {
|
||||
//初始化图表
|
||||
inCharts() {
|
||||
if (this.myChart == null) {
|
||||
this.myChart = echarts.init(document.getElementById("chart"));
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('初始化WebSocket客户端失败:', error)
|
||||
isConnected.value = false
|
||||
isConnecting.value = false
|
||||
connectionError.value = `初始化失败: ${error.message || '未知错误'}`
|
||||
}
|
||||
}
|
||||
|
||||
// 添加断开连接方法
|
||||
const disconnectWebSocket = () => {
|
||||
if (stompClient.value?.connected) {
|
||||
stompClient.value.disconnect(() => {
|
||||
const disconnectTime = new Date()
|
||||
const duration = connectTime.value ?
|
||||
Math.floor((disconnectTime.getTime() - connectTime.value.getTime()) / 1000) : 0
|
||||
|
||||
console.log('断开连接时间:', disconnectTime.toLocaleString())
|
||||
console.log(`连接持续时间: ${Math.floor(duration / 60)}分${duration % 60}秒`)
|
||||
|
||||
isConnected.value = false
|
||||
receivedMessages.value = []
|
||||
connectTime.value = null
|
||||
|
||||
// 添加提示
|
||||
connectionError.value = '已断开连接'
|
||||
setTimeout(() => {
|
||||
connectionError.value = ''
|
||||
}, 3000)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息方法
|
||||
const sendMessage = () => {
|
||||
if (!message.value.trim()) return
|
||||
|
||||
if (stompClient.value?.connected) {
|
||||
try {
|
||||
const messageObj = {
|
||||
email: targetEmail.value,
|
||||
content: message.value.trim()
|
||||
}
|
||||
|
||||
stompClient.value.send(
|
||||
`/point/send/message`,
|
||||
{},
|
||||
JSON.stringify(messageObj)
|
||||
)
|
||||
|
||||
// 添加自己发送的消息到显示列表
|
||||
receivedMessages.value.push({
|
||||
sender: email.value,
|
||||
content: message.value.trim(),
|
||||
timestamp: new Date().toISOString(),
|
||||
isSelf: true
|
||||
})
|
||||
|
||||
message.value = ''
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error)
|
||||
|
||||
receivedMessages.value.push({
|
||||
sender: 'System',
|
||||
content: `发送失败: ${error.message || '未知错误'}`,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: true
|
||||
})
|
||||
}
|
||||
} else {
|
||||
connectionError.value = '连接已断开,请重新连接'
|
||||
isConnected.value = false
|
||||
}
|
||||
}
|
||||
|
||||
this.option.series[0].name = this.$t(`home.computingPower`);
|
||||
this.option.series[1].name = this.$t(`home.currencyPrice`);
|
||||
// 组件卸载时断开连接
|
||||
onUnmounted(() => {
|
||||
if (stompClient.value?.connected) {
|
||||
stompClient.value.disconnect()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
this.myChart.setOption(this.option);
|
||||
// 回调函数,在渲染完成后执行
|
||||
this.myChart.on("finished", () => {
|
||||
// 在这里执行显示给用户的操作
|
||||
// console.log('图表渲染完成,显示给用户');
|
||||
this.minerChartLoading = false;
|
||||
});
|
||||
// window.addEventListener("resize", () => {
|
||||
// if (this.myChart) this.myChart.resize();
|
||||
// });
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
window.addEventListener(
|
||||
"resize",
|
||||
throttle(() => {
|
||||
if (this.myChart) this.myChart.resize();
|
||||
}, 200)
|
||||
);
|
||||
},
|
||||
startCountDown() {
|
||||
this.timer = setInterval(() => {
|
||||
if (this.countDownTime <= 0) {
|
||||
//当监测到countDownTime为0时,清除计数器并且移除sessionStorage,然后执行提交试卷逻辑
|
||||
clearInterval(this.timer);
|
||||
sessionStorage.removeItem("exam_time");
|
||||
alert("提交试卷");
|
||||
} else if (this.countDownTime > 0) {
|
||||
//每秒让countDownTime -1秒,并设置到sessionStorage中
|
||||
this.countDownTime--;
|
||||
window.sessionStorage.setItem("exam_time", this.countDownTime);
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
.user-input-container {
|
||||
max-width: 400px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.chat-container {
|
||||
max-width: 600px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.message-list {
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #eee;
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 8px 0;
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 8px;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.8em;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.message-sender {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.message[class*="isSelf"] {
|
||||
background-color: #dcf8c6;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: #ffebee;
|
||||
color: #d32f2f;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.disconnect-btn {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.disconnect-btn:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
input:disabled, textarea:disabled {
|
||||
background-color: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -105,9 +105,9 @@ export default {
|
||||
console.log(res,"文件返回");
|
||||
|
||||
this.ruleForm.files = res.data.data.id
|
||||
if (this.ruleForm.files) {//成功拿到返回ID
|
||||
this.fetchSubmitWork(this.ruleForm)
|
||||
}
|
||||
// if (this.ruleForm.files) {//成功拿到返回ID
|
||||
// this.fetchSubmitWork(this.ruleForm)
|
||||
// }
|
||||
})
|
||||
} else {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user