取消enx活动图片 费率改为1%

This commit is contained in:
2025-04-22 14:26:41 +08:00
parent 4d436c725e
commit 368d0a8a10
45 changed files with 2581 additions and 234 deletions

View File

@@ -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 {

View 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',
})
}

View 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>

View 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.",
}
}

View File

@@ -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,

View File

@@ -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',

View File

@@ -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)
}

File diff suppressed because it is too large Load Diff

View File

@@ -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;

View File

@@ -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",

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 {