1.1需求完成

This commit is contained in:
2025-07-04 13:46:11 +08:00
parent 62260e8483
commit c6f765f858
52 changed files with 1993 additions and 1012 deletions

View File

@@ -53,7 +53,7 @@
<router-view v-if="key !== '/zh' && key !== '/en' &&key !== '/zh/login' && key !== '/en/login' && key !== '/zh/register' && key !== '/en/register'" :key="key" />
<div v-else style="color: #333;font-size: 16px;text-align: center;line-height: 100vh;">
选择左侧导航栏查看页面
{{ $t("backendSystem.leftNavigation") }}
</div>
</el-main>
@@ -118,6 +118,9 @@ export default {
}
this.userEmail = userEmail;
});
//网络变化
window.addEventListener("online", this.handleNetworkChange);
},
methods: {
async fetchSignOut() {
@@ -178,7 +181,19 @@ export default {
this.$message.error(this.$t("common.langChangeFailed"));
}
},
// 处理网络状态变化
handleNetworkChange() {
if (navigator.onLine) {
// === 强制重置状态,兜底 ===
location.reload(); // 重新加载当前页面
}
},
},
beforeDestroy() {
window.removeEventListener("online", this.handleNetworkChange);
},
};
</script>

View File

@@ -1429,7 +1429,6 @@ export default {
// 加载历史消息
async loadHistoryMessages() {
if (this.isLoadingHistory || !this.roomId) return;
this.isLoadingHistory = true;
try {
const response = await getHistory7({
@@ -1437,41 +1436,18 @@ export default {
userType: this.userType,
email: this.userEmail,
});
// console.log("📋 初始历史消息加载响应:", {
// code: response?.code,
// dataExists: !!response?.data,
// dataLength: response?.data?.length || 0,
// isArray: Array.isArray(response?.data),
// });
if (response?.code === 200 && Array.isArray(response.data)) {
// 使用统一的格式化方法
const historyMessages = this.formatHistoryMessages(response.data);
if (historyMessages.length > 0) {
// 有历史消息,按时间顺序排序
this.messages = historyMessages.sort(
(a, b) => new Date(a.time) - new Date(b.time)
);
// console.log(
// "✅ 成功加载",
// historyMessages.length,
// "条初始历史消息"
// );
// 保持对话框打开状态
this.isChatOpen = true;
this.isMinimized = false;
// 只有在初始加载历史消息时才滚动到底部(当消息列表为空时)
// 不自动弹窗,只滚动
await this.$nextTick();
// 添加一个小延时确保所有内容都渲染完成
setTimeout(() => {
this.scrollToBottom(true); // 传入 true 表示强制滚动
this.scrollToBottom(true);
}, 100);
} else {
// 格式化后没有有效消息
this.messages = [
{
type: "system",
@@ -1480,10 +1456,8 @@ export default {
time: new Date().toISOString(),
},
];
// console.log("📋 初始历史消息为空(格式化后无有效消息)");
}
} else {
// 响应无效或无数据
this.messages = [
{
type: "system",
@@ -1492,11 +1466,9 @@ export default {
time: new Date().toISOString(),
},
];
// console.log("📋 初始历史消息为空(响应无效)");
}
} catch (error) {
console.error("加载历史消息失败:", error);
// === 简化历史消息加载失败提示 ===
this.$message.error(
this.$t("chat.loadHistoryFailed") || "加载历史消息失败"
);

View File

@@ -63,32 +63,28 @@ export default {
id:"3",
},
// {//用户算力
// path:"userComputingPower",
// label:`backendSystem.userComputingPower`,
// icon:"el-icon-finished",
// id:"4",
// },
],
activeIndex: "0",
}
},
mounted(){
const savedIndex = localStorage.getItem("activeIndex");
if(savedIndex){
this.activeIndex = savedIndex;
}else{
this.$addStorageEvent(1, "activeIndex", this.activeIndex);
const lang = this.$i18n.locale;
const currentPath = this.$route.path.replace(`/${lang}/`, '');
// 优先根据当前路由匹配菜单项
const matchedMenu = this.menuList.find(item => item.path === currentPath);
if (matchedMenu) {
this.activeIndex = matchedMenu.id;
this.$addStorageEvent(1, "activeIndex", matchedMenu.id);
} else {
// 如果localStorage有值优先用localStorage
const savedIndex = localStorage.getItem("activeIndex");
if(savedIndex){
this.activeIndex = savedIndex;
} else {
this.$addStorageEvent(1, "activeIndex", this.activeIndex);
}
}
},
methods:{
handleClick(item){

View File

@@ -45,7 +45,7 @@ export const ChatWidget_zh = {
readImage:"读取图片失败,请重试",
processingFailed:"图片处理失败,请重试",
Disconnected:"连接已断开",
reconnecting:"正在连...",
reconnecting:"正在连...",
contactList:"联系列表",
search:"搜索最近联系人",
tourist:"游客",
@@ -107,6 +107,7 @@ export const ChatWidget_zh = {
serviceAddressUnavailable:"服务地址不可用,请稍后重试",
connectionFailedService:"无法连接到服务器,请稍后重试",
connectionFailedCustomer:"连接客服系统失败,请检查网络或稍后重试",
logoutSyncNotice:"检测到您已在其他窗口退出登录,当前窗口将自动跳转",
},
@@ -140,7 +141,7 @@ export const ChatWidget_en = {
subscriptionFailed: "Message subscription failed",
break: "Connection lost",
retry: "Retry in seconds",
disconnectWaiting: "Reconnecting...",
disconnectWaiting: "connecting...",
sendFailed: "Send failed, please retry",
noHistory: "No history",
historicalFailure: "Failed to load history",

View File

@@ -20,8 +20,7 @@ export const backendSystem_zh = {
deleteRemind:"确定删除该广播吗?",
deleteSuccess:"删除成功",
editSuccess:"修改成功",
addSuccess:"发布成功",
addSuccess:"发布成功",
cancel:"取 消",
publish:"发 布",
logout:"退出",
@@ -29,6 +28,61 @@ export const backendSystem_zh = {
dialogTitle:"新增广播内容",
pleaseInputContent:"请输入广播内容",
newlineInvalid:"广播内容输入换行符无效",
onlineUserNum:"在线数量",
offlineUserNum:"离线数量",
userPower:"用户算力",
chartTitle:"用户算力及在离线状态图",
noData:"无数据",
startDate:"开始日期",
endDate:"结束日期",
to:"至",
userDetails:"用户详情",
return:"返回",
coin:"币种:",
user:"挖矿账户:",
amount:"交易金额:",
createDate:"收益分配日期",
maxHeight:"最大高度",
shouldOutDate:"实际转账日期",
address:"转账地址",
historyAddress:"历史支付地址:",
userManagementTitle:"注册用户管理",
pleaseInput:"请输入",
email:"邮箱:",
email2:"邮箱",
query:"查询",
coin2:"币种",
amount2:"最小起付金额",
status2:"用户状态",
minerUser2:"挖矿账号",
balance2:"支付地址",
active2:"是否自动提现",
normal:"正常",
delete:"删除",
yes:"是",
no:"否",
Details:"详情",
sendEmail:"发送邮件",
sendSuccess:"发送成功",
sendFail:"发送失败",
sendRemind:"确定发送邮件吗?",
recipient:"收件人",
subject:"邮件主题",
text:"邮件内容",
send:"发送",
emailRemind:"可输入多个邮箱,用逗号隔开",
pleaseInputCorrectEmail:"请输入正确的邮箱地址",
pleaseInputSubject:"请输入邮件主题",
pleaseInputText:"请输入邮件内容",
pleaseInputCorrectEmail2:"请输入正确的邮箱地址,多个邮箱用逗号分隔",
existDuplicateEmail:"存在重复邮箱,请检查",
pleaseInputQueryConditions:"请输入查询条件(挖矿账号、邮箱)",
emailContent:"邮件内容",
leftNavigation:"选择左侧导航栏查看页面",
}
}
@@ -63,7 +117,59 @@ export const backendSystem_en = {
dialogTitle:"Add Broadcast Content",
pleaseInputContent:"Please input broadcast content",
newlineInvalid:"Invalid line break for broadcasting content input",
onlineUserNum:"Online Number",
offlineUserNum:"Offline Number",
userPower:"User Power",
chartTitle:"User Power and Online/Offline Status Chart",
noData:"No Data",
startDate:"Start Date",
endDate:"End Date",
to:"To",
userDetails:"User Details",
return:"Return",
coin:"Coin:",
user:"Mining account:",
amount:"Amount:",
createDate:"Create Date",
maxHeight:"Max Height",
shouldOutDate:"Should Out Date",
address:"Address",
historyAddress:"History Payment Address:",
userManagementTitle:"User Management",
pleaseInput:"Please Input",
email:"Email:",
email2:"Email",
query:"Query",
coin2:"Coin",
amount2:"Minimum Payout Amount",
status2:"User Status",
minerUser2:"Miner User",
balance2:"Payment Address",
active2:"Automatic Withdrawal",
normal:"Normal",
delete:"Delete",
yes:"Yes",
no:"No",
Details:"Details",
sendEmail:"Send Email",
sendSuccess:"Send Success",
sendFail:"Send Fail",
sendRemind:"Are you sure you want to send an email?",
recipient:"Recipient",
subject:"Email Subject",
text:"Email Content",
send:"Send",
emailRemind:"Multiple emails can be entered, separated by commas",
pleaseInputCorrectEmail:"Please enter a valid email address",
pleaseInputSubject:"Please enter an email subject",
pleaseInputText:"Please enter an email content",
pleaseInputCorrectEmail2:"Please enter a valid email address, multiple emails separated by commas",
existDuplicateEmail:"Duplicate email exists, please check",
pleaseInputQueryConditions:"Please enter the query conditions (miner account, email)",
emailContent:"Email Content",
leftNavigation:"Select the left navigation bar to view the page",
}
}

View File

@@ -26,7 +26,7 @@ Vue.use(ElementUI, {
});
Vue.prototype.$axios = axios
// console.log = ()=>{} //全局关闭打印
console.log = ()=>{} //全局关闭打印
// 全局注册混入
Vue.mixin(loadingStateMixin);//loading状态管理
Vue.mixin(networkRecoveryMixin);//网络恢复后数据刷新

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
export default {
'401': '认证失败,无法访问系统资源,请重新登录',
// '401': '认证失败,无法访问系统资源,请重新登录',
'403': '当前操作没有权限',
'404': '访问资源不存在',
'default': '系统未知错误,请反馈给管理员'

View File

@@ -4,7 +4,7 @@ import { Notification, MessageBox, Message } from 'element-ui'
import loadingManager from './loadingManager';
import errorNotificationManager from './errorNotificationManager';
const pendingRequestMap = new Map(); //处理Request aborted 错误
function getRequestKey(config) { //处理Request aborted 错误 生成唯一 key 的函数
@@ -130,7 +130,7 @@ window.addEventListener('online', () => {
// 常见的加载状态
const commonLoadingProps = [
'minerChartLoading', 'reportBlockLoading', 'apiPageLoading',
'MiningLoading', 'miniLoading'
'MiningLoading', 'miniLoading', 'bthLoading', 'editLoading'
];
commonLoadingProps.forEach(prop => {
@@ -138,6 +138,15 @@ window.addEventListener('online', () => {
window.vm[prop] = false;
}
});
// 重置所有以Loading结尾的状态
Object.keys(window.vm).forEach(key => {
if (key.endsWith('Loading')) {
window.vm[key] = false;
}
});
}
// 触发网络重试完成事件
@@ -267,10 +276,10 @@ service.interceptors.response.use(res => {
type: 'warning'
}
).then(() => {
window.vm.$router.push("/login")
window.vm.$router.push(`/${window.vm.$i18n.locale}/login`)
localStorage.removeItem('token')
}).catch(() => {
window.vm.$router.push("/")
window.vm.$router.push(`/${window.vm.$i18n.locale}/`)
localStorage.removeItem('token')
});
@@ -308,10 +317,20 @@ service.interceptors.response.use(res => {
},
error => {
if (error.message && error.message.includes('canceled') || error.message.includes('Request aborted')) {
// 主动取消的请求,直接忽略,不提示
return Promise.reject(error);
// 主动取消的请求,直接忽略,不提示
if (
error.code === 'ERR_CANCELED' ||
(error.message && error.message.includes('canceled')) ||
error.message?.includes('Request aborted')
) {
// 静默处理,不提示,不冒泡
return new Promise(() => {}); // 返回pending Promise阻止控制台报错
}
// 请求异常也要移除 处理Request aborted 错误
if (error.config) {
const requestKey = getRequestKey(error.config);

View File

@@ -39,6 +39,9 @@ export default {
editLoading: false,
byteCount: "",
isOverLimit: false,
total: 0,
pageSizes: [50, 100, 300],
currentPage: 1,
}
},
@@ -54,8 +57,7 @@ export default {
if (token) {
this.fetchList(this.listParams);
}
},
methods: {
async fetchList(params) {
@@ -64,6 +66,7 @@ export default {
const res = await listBroadcast(params)
if (res.code === 200) {
this.tableData = res.rows
this.total = res.total
}
this.setLoading('broadcastLoading', false);
},
@@ -109,6 +112,7 @@ export default {
handleClose() {
this.dialogVisible = false;
this.addParams.content = ""
this.setLoading('bthLoading', false);
},
sureAddBroadcast() {
@@ -136,6 +140,7 @@ export default {
handleEditClose() {
this.editDialogVisible = false;
this.editParams.content = ""
this.setLoading('editLoading', false);
},
handelDelete(row) {
this.deleteBroadcast({ id: row.id });
@@ -184,9 +189,28 @@ export default {
handelTime(time) {
return `${time.split("T")[0]} ${time.split("T")[1]}`
}
},
handleSizeChange(val) {
console.log(`每页 ${val}`);
this.listParams.pageSize = val
this.listParams.pageNum = 1
this.currentPage = 1
this.fetchList(this.listParams);
},
handleCurrentChange(val) {
console.log(`当前页: ${val}`);
this.listParams.pageNum = val
this.fetchList(this.listParams);
}
},
},
}

View File

@@ -1,153 +1,234 @@
<template>
<div v-loading="broadcastLoading">
<div class="main-title-box">
<div class="main-title">{{$t("backendSystem.publishedBroadcast")}}</div>
<el-button class="add-btn" @click="handelAddBroadcast">{{$t("backendSystem.addBroadcast")}} <i class="iconfont icon-youjiantou1 arrow"></i></el-button>
</div>
<el-table
:data="tableData"
border
style="width: 100%; margin-bottom: 18px"
:header-cell-style="{ 'text-align': 'center' }"
:cell-style="{ 'text-align': 'center' }"
>
<el-table-column prop="id" label="ID" width="60" show-overflow-tooltip />
<el-table-column prop="createTime" :label="$t('backendSystem.createTime')" width="160" show-overflow-tooltip>
<template slot-scope="scope">
{{ handelTime(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column prop="content" :label="$t('backendSystem.content')" show-overflow-tooltip/>
<el-table-column prop="createUser" :label="$t('backendSystem.createUser')" width="160" show-overflow-tooltip/>
<el-table-column prop="updateTime" :label="$t('backendSystem.updateTime')" width="160" show-overflow-tooltip>
<template slot-scope="scope">
{{ handelTime(scope.row.updateTime) }}
</template>
</el-table-column>
<el-table-column prop="updateUser" :label="$t('backendSystem.updateUser')" width="160" show-overflow-tooltip/>
<el-table-column :label="$t('backendSystem.operation')" width="160" >
<template slot-scope="scope">
<el-button
size="mini"
@click="handleEdit(scope.row)">{{$t("backendSystem.edit")}}</el-button>
<el-popconfirm
:confirm-button-text='$t(`work.confirm`)'
:cancel-button-text='$t(`work.cancel`)'
icon="el-icon-info"
icon-color="red"
:title="$t(`alerts.deleteRemind`)"
@confirm="handelDelete(scope.row)"
>
<el-button class="elBtn" size="mini" slot="reference">{{ $t(`personal.delete`) }}</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!-- 新增广播弹窗 -->
<el-dialog
:title="$t('backendSystem.dialogTitle')"
:visible.sync="dialogVisible"
width="50%"
:before-close="handleClose"
:close-on-click-modal="false"
>
<el-form :model="addParams" >
<!-- v-if="isOverLimit" -->
<el-form-item >
<el-input resize="none" v-model="addParams.content" type="textarea" :rows="5" @input="val => handleInput(val, 'add')" />
<div style="color: #999; font-size: 12px; margin-top: 4px;display: flex;align-items: center;justify-content: space-between;">
<span v-if="isOverLimit"> {{$t("backendSystem.exceedingInput")}}</span>
<span> {{$t("backendSystem.newlineInvalid")}}</span>
</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose">{{$t("backendSystem.cancel")}}</el-button>
<el-button type="primary" :loading="bthLoading" @click="sureAddBroadcast">{{$t("backendSystem.publish")}}</el-button>
</div>
</el-dialog>
<!-- 修改广播弹窗 -->
<el-dialog
:title="$t('backendSystem.editContent')"
:visible.sync="editDialogVisible"
width="50%"
:before-close="handleEditClose"
:close-on-click-modal="false"
>
<el-form :model="editParams" >
<el-form-item >
<el-input resize="none" v-model="editParams.content" type="textarea" :rows="5" @input="val => handleInput(val, 'edit')" />
<div style="color: #999; font-size: 12px; margin-top: 4px;display: flex;align-items: center;justify-content: space-between;">
<span v-if="isOverLimit"> {{$t("backendSystem.exceedingInput")}}</span>
<span> {{$t("backendSystem.newlineInvalid")}}</span>
</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleEditClose">{{$t("backendSystem.cancel")}}</el-button>
<el-button type="primary" :loading="editLoading" @click="sureEditBroadcast">{{$t("backendSystem.editBroadcast")}}</el-button>
</div>
</el-dialog>
<div v-loading="broadcastLoading">
<div class="main-title-box">
<div class="main-title">{{ $t("backendSystem.publishedBroadcast") }}</div>
<el-button class="add-btn" @click="handelAddBroadcast"
>{{ $t("backendSystem.addBroadcast") }}
<i class="iconfont icon-youjiantou1 arrow"></i
></el-button>
</div>
<el-table
:data="tableData"
border
style="width: 100%; margin-bottom: 18px"
:header-cell-style="{ 'text-align': 'center' }"
:cell-style="{ 'text-align': 'center' }"
height="60vh"
>
<el-table-column prop="id" label="ID" width="60" show-overflow-tooltip />
<el-table-column
prop="createTime"
:label="$t('backendSystem.createTime')"
width="160"
show-overflow-tooltip
>
<template slot-scope="scope">
{{ handelTime(scope.row.createTime) }}
</template>
</el-table-column>
<el-table-column
prop="content"
:label="$t('backendSystem.content')"
show-overflow-tooltip
/>
<el-table-column
prop="createUser"
:label="$t('backendSystem.createUser')"
width="160"
show-overflow-tooltip
/>
<el-table-column
prop="updateTime"
:label="$t('backendSystem.updateTime')"
width="160"
show-overflow-tooltip
>
<template slot-scope="scope">
{{ handelTime(scope.row.updateTime) }}
</template>
</el-table-column>
<el-table-column
prop="updateUser"
:label="$t('backendSystem.updateUser')"
width="160"
show-overflow-tooltip
/>
<el-table-column :label="$t('backendSystem.operation')" width="160">
<template slot-scope="scope">
<el-button size="mini" @click="handleEdit(scope.row)">{{
$t("backendSystem.edit")
}}</el-button>
<el-popconfirm
:confirm-button-text="$t(`work.confirm`)"
:cancel-button-text="$t(`work.cancel`)"
icon="el-icon-info"
icon-color="red"
:title="$t(`alerts.deleteRemind`)"
@confirm="handelDelete(scope.row)"
>
<el-button class="elBtn" size="mini" slot="reference">{{
$t(`personal.delete`)
}}</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-row>
<el-col :span="24" style="display: flex; justify-content: center">
<el-pagination
style="margin: 0 auto; margin-top: 10px"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="currentPage"
:page-sizes="pageSizes"
:page-size="listParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination>
</el-col>
</el-row>
<!-- 新增广播弹窗 -->
<el-dialog
:title="$t('backendSystem.dialogTitle')"
:visible.sync="dialogVisible"
width="50%"
:before-close="handleClose"
:close-on-click-modal="false"
>
<el-form :model="addParams">
<!-- v-if="isOverLimit" -->
<el-form-item>
<el-input
resize="none"
v-model="addParams.content"
type="textarea"
:rows="5"
@input="(val) => handleInput(val, 'add')"
/>
<div
style="
color: #999;
font-size: 12px;
margin-top: 4px;
display: flex;
align-items: center;
justify-content: space-between;
"
>
<span v-if="isOverLimit">
{{ $t("backendSystem.exceedingInput") }}</span
>
<span> {{ $t("backendSystem.newlineInvalid") }}</span>
</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose">{{
$t("backendSystem.cancel")
}}</el-button>
<el-button
type="primary"
:loading="bthLoading"
@click="sureAddBroadcast"
>{{ $t("backendSystem.publish") }}</el-button
>
</div>
</el-dialog>
<!-- 修改广播弹窗 -->
<el-dialog
:title="$t('backendSystem.editContent')"
:visible.sync="editDialogVisible"
width="50%"
:before-close="handleEditClose"
:close-on-click-modal="false"
>
<el-form :model="editParams">
<el-form-item>
<el-input
resize="none"
v-model="editParams.content"
type="textarea"
:rows="5"
@input="(val) => handleInput(val, 'edit')"
/>
<div
style="
color: #999;
font-size: 12px;
margin-top: 4px;
display: flex;
align-items: center;
justify-content: space-between;
"
>
<span v-if="isOverLimit">
{{ $t("backendSystem.exceedingInput") }}</span
>
<span> {{ $t("backendSystem.newlineInvalid") }}</span>
</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleEditClose">{{
$t("backendSystem.cancel")
}}</el-button>
<el-button
type="primary"
:loading="editLoading"
@click="sureEditBroadcast"
>{{ $t("backendSystem.editBroadcast") }}</el-button
>
</div>
</el-dialog>
</div>
</template>
<script>
import Index from './index'
import Index from "./index";
export default {
mixins:[Index],
}
mixins: [Index],
};
</script>
<style scoped lang="scss">
.main-title-box{
.main-title-box {
display: flex;
align-items: center;
margin-bottom: 20px;
.add-btn{
background: #661FFB;
.add-btn {
background: #661ffb;
color: #fff;
border:none;
border: none;
margin-left: 28px;
border-radius: 20px;
padding: 10px 20px;
transition: all 0.3s ease;
.arrow{
.arrow {
margin-left: 10px;
}
}
.add-btn:hover{
transform: scale(1.05);
.add-btn:hover {
transform: scale(1.05);
}
}
.main-title{
.main-title {
font-size: 24px;
font-weight: bold;
color: #333;
}
.elBtn{
.elBtn {
background: #e60751;
color: #fff;
border:none;
border: none;
margin-left: 18px;
}
</style>

View File

@@ -433,7 +433,7 @@ export default {
},
},
async created() {
created() {
try {
let userEmail = localStorage.getItem("userEmail");
this.userEmail = JSON.parse(userEmail);
@@ -443,11 +443,11 @@ export default {
});
// 获取聊天室列表
await this.fetchRoomList();
this.fetchRoomList();
// 在组件创建时加载手动创建的聊天室
this.loadManualCreatedRooms();
console.log(this.userEmail, "初始化的时候");
console.log("mounted userEmail=", userEmail, "parsed=", this.userEmail);
// 初始化 WebSocket 连接
this.initWebSocket();
} catch (error) {
@@ -455,6 +455,8 @@ export default {
}
},
async mounted() {
// 获取聊天室列表
await this.fetchRoomList();
@@ -577,6 +579,9 @@ export default {
return;
}
console.log("走这里了嘛 家电节爱哦都觉得久啊是哦大点击");
// 防止重复初始化
if (this.stompClient && this.stompClient.state !== "DISCONNECTED") {
console.log("WebSocket正在连接中跳过初始化");
@@ -588,15 +593,9 @@ export default {
if (this.stompClient) {
this.forceDisconnectAll();
}
let apiUrl = process.env.VUE_APP_BASE_API;
let baseUrl=""
// 将 https 替换为 wss
if (apiUrl.startsWith("https://")) {
baseUrl= apiUrl.replace("https://", "wss://");
}
if (apiUrl.startsWith("http://")) {
baseUrl=apiUrl.replace("http://", "ws://");
}
console.log("开始初始化WebSocket连接...");
const baseUrl = process.env.VUE_APP_BASE_API.replace("https", "wss");
const wsUrl = `${baseUrl}chat/ws`;
this.stompClient = Stomp.client(wsUrl);
@@ -650,11 +649,12 @@ export default {
this.subscribeToMessages();
this.updateLastActivityTime();
// === 启动心跳检测 ===
this.startHeartbeat();
// === 启动连接状态检查 ===
this.startConnectionCheck();
// === 暂时禁用心跳和连接检查,避免误判 ===
// this.startHeartbeat();
// this.startConnectionCheck();
// 只依赖 STOMP 内置心跳和订阅成功状态
console.log("✅ 连接成功,只使用 STOMP 内置心跳机制");
// === 注意:不在这里启动验证,而是在订阅成功后 ===
console.log("⚡ 客服连接成功,等待订阅完成后验证");
@@ -715,11 +715,10 @@ export default {
console.log("📢 客服订阅成功,立即标记连接已验证");
this.markConnectionVerified();
// 确保连接状态正确
if (this.connectionStatus !== "connected") {
console.log("📡 修正客服连接状态为connected");
this.connectionStatus = "connected";
}
// 强制确保连接状态正确
this.isWebSocketConnected = true;
this.connectionStatus = "connected";
console.log("✅ 强制设置连接状态为已连接");
} else {
console.error("❌ 客服订阅失败返回空subscription");
// 如果订阅失败,启动验证机制等待超时重连
@@ -1734,6 +1733,10 @@ export default {
this.markConnectionVerified();
this.updateLastActivityTime(); // 收到消息也是一种活动
this.lastHeartbeatTime = Date.now(); // 更新心跳时间
// 强制确保连接状态正确(收到消息说明连接肯定是好的)
this.isWebSocketConnected = true;
this.connectionStatus = "connected";
const msg = JSON.parse(message.body);
console.log("客服收到的消息", msg);
@@ -3665,14 +3668,45 @@ export default {
localStorage.setItem(key, String(count));
},
/**
* 监听storage事件实现多窗口未读同步
* 监听storage事件实现多窗口未读同步和登录状态同步
*/
handleStorageChange(e) {
// 监听未读消息同步
if (e.key && e.key.startsWith("cs_unread_")) {
const roomId = e.key.replace("cs_unread_", "");
const count = parseInt(e.newValue, 10) || 0;
const contact = this.contacts.find((c) => c.roomId == roomId);
if (contact) contact.unread = count;
return;
}
// 监听登录状态同步 - 当userEmail被清除时表示用户已退出登录
if (e.key === "userEmail" && e.oldValue && (!e.newValue || e.newValue === "null")) {
this.handleLogoutSync();
}
},
/**
* 处理多窗口登录状态同步
*/
handleLogoutSync() {
try {
// 断开WebSocket连接
this.forceDisconnectAll();
// 清除本地数据
this.userEmail = "";
this.currentContactId = null;
this.contacts = [];
this.messages = {};
this.inputMessage = "";
this.isWebSocketConnected = false;
this.connectionStatus = "disconnected";
// 跳转到首页
this.$router.replace("/");
} catch (error) {
this.$router.replace("/");
}
},

View File

@@ -918,7 +918,7 @@ export default {
});
// this.getBroadcastList({pageNum:1,pageSize:100})
this.getBroadcastList()
this.getBroadcastList({lang:this.$i18n.locale})
this.startScroll();
this.$nextTick(() => {
@@ -1732,6 +1732,10 @@ scrollRight() {
if (this.broadcastList.length <= 1) return;
this.scrollTimer = setInterval(this.nextScroll, 3000);
},
stopScroll() {
if (this.scrollTimer) clearInterval(this.scrollTimer);
this.scrollTimer = null;
},
nextScroll() {
if (this.broadcastList.length <= 1) return;
this.isTransition = true;

View File

@@ -421,7 +421,8 @@
<div class="describe-row">
<div class="broadcast-scroll-wrap">
<div class="broadcast-scroll-list" :style="scrollStyle">
<div class="broadcast-scroll-list" @mouseenter="stopScroll"
@mouseleave="startScroll" :style="scrollStyle">
<div
v-for="item in broadcastList"
:key="item.id"
@@ -430,7 +431,7 @@
<i class="iconfont icon-tishishuoming"></i>
<span class="describeTitle">{{ $t(`home.describeTitle`) }}</span>
<span> {{ item.content }}</span>
<span :title="item.content"> {{ item.content }}</span>
<span
class="view"
v-show="item.id == 'b1'"

View File

@@ -270,6 +270,16 @@ export default {
},
},
watch: {
'$route'(to) {
// 路由变化时自动同步语言和radio
const match = to.path.match(/^\/(zh|en)(\/|$)/);
if (match) {
this.radio = match[1];
this.lang = match[1];
this.$i18n.locale = match[1];
localStorage.setItem("lang", match[1]);
}
},
"$i18n.locale": function () {
this.translate();
},
@@ -290,10 +300,29 @@ export default {
// }
},
mounted() {
this.lang = this.$i18n.locale; // 初始化语言值
this.radio = localStorage.getItem("lang")
? localStorage.getItem("lang")
: "en";
// this.lang = this.$i18n.locale; // 初始化语言值
// this.radio = localStorage.getItem("lang")
// ? localStorage.getItem("lang")
// : "en";
// 获取当前路由路径
const path = this.$route.path;
// 匹配 /zh/ 或 /en/ 作为语言前缀
const match = path.match(/^\/(zh|en)(\/|$)/);
if (match) {
this.radio = match[1];
this.lang = match[1];
this.$i18n.locale = match[1];
localStorage.setItem("lang", match[1]);
} else {
// fallback 到 localStorage 或 "en"
this.radio = localStorage.getItem("lang") || "en";
this.lang = this.radio;
this.$i18n.locale = this.radio;
}
},
methods: {
translate() {

View File

@@ -333,15 +333,44 @@ export default {
}
},
watch: {
'$route'(to) {
// 路由变化时自动同步语言和radio
const match = to.path.match(/^\/(zh|en)(\/|$)/);
if (match) {
this.radio = match[1];
this.lang = match[1];
this.$i18n.locale = match[1];
localStorage.setItem("lang", match[1]);
}
},
"$i18n.locale": function () {
this.translate();
},
},
mounted() {
this.lang = this.$i18n.locale;
this.radio = localStorage.getItem("lang")
? localStorage.getItem("lang")
: "en";
// this.lang = this.$i18n.locale;
// this.radio = localStorage.getItem("lang")
// ? localStorage.getItem("lang")
// : "en";
// 获取当前路由路径
const path = this.$route.path;
// 匹配 /zh/ 或 /en/ 作为语言前缀
const match = path.match(/^\/(zh|en)(\/|$)/);
if (match) {
this.radio = match[1];
this.lang = match[1];
this.$i18n.locale = match[1];
localStorage.setItem("lang", match[1]);
} else {
// fallback 到 localStorage 或 "en"
this.radio = localStorage.getItem("lang") || "en";
this.lang = this.radio;
this.$i18n.locale = this.radio;
}
for (const key in this.registerForm) {
this.registerForm[key] = "";
}

View File

@@ -1,5 +1,6 @@
import { getUserDetails,getUserLineChart,getUserOnlineStatus } from '../../api/userManagement'
import * as echarts from "echarts";
import { Debounce, throttle } from "../../utils/publicMethods";
export default {
name: 'UserDetails',
data() {
@@ -34,30 +35,341 @@ export default {
userDetailsParams:{
coin: '',
minerUser: '',
startDate:"",
endDate:"",
},
labelPosition: 'top',
noDataTip: false,
lineChartParams:{
user: '',
minerUser: '',
endDate:"",
startDate:"",
coin:"",
},
onlineStatusParams:{
user: '',
coin: '',
datePoint:"",
}
minerUser: '',
coin: '',
endDate:"",
startDate:"",
},
onlineStatusData:[
{
"date":"2024-11-28T11:00:00",
"offlineNum": 148,
"onlineNum": 15
},
{
"date":"2025-06-27 17:00:00",
"offlineNum": 110,
"onlineNum": 156
},
{
"date":"2025-06-27 17:30:00",
"offlineNum": 90,
"onlineNum": 152
},
{
"date":"2025-06-27 18:00:00",
"offlineNum": 70,
"onlineNum": 152
},
{
"date":"2025-06-27 18:00:00",
"offlineNum": 70,
"onlineNum": 152
},
{
"date":"2025-06-27 18:00:00",
"offlineNum": 70,
"onlineNum": 152
},
{
"date":"2025-06-27 18:00:00",
"offlineNum": 70,
"onlineNum": 152
},
],
lineChatTimes:[],
option: {
tooltip: {
trigger: "axis",
//解决tooltip显示不全问题1
confine: true,
// formatter: function (params) {
// var res
// res = params[0].axisValueLabel;
// for (let i = 0; i <= params.length - 1; i++) {
// if (params[i].seriesName == "Currency Price" || params[i].seriesName == "币价") {
// res += `</br>${params[i].marker} ${params[i].seriesName} &nbsp&nbsp&nbsp&nbsp <span style="font-weight: bold">${params[i].value} USD</span>`
// } else {
// res += `</br>${params[i].marker} ${params[i].seriesName} &nbsp&nbsp&nbsp&nbsp <span style="font-weight: bold">${params[i].value}</span>`
// }
// }
// return res;
// },
},
legend: {
right: "8%",
},
// grid: {//解决Y轴显示不全
// left: "10%",//10%
// containLabel: true
// },
grid: {
left: "8%",
right: "8%",
top: "10%",
bottom: "15%", // 增加底部空间为旋转的X轴标签和滑动条留出空间
},
xAxis: {
// type: "time",
boundaryGap: true, // 让柱子不超出X轴
axisLabel: {
interval: 'auto', // 自动间隔显示标签,避免重叠
rotate: 45, // 旋转45度避免标签重叠
formatter: function(value) {
// 格式化显示,只显示月-日 时:分
if (value.includes(' ')) {
const [date, time] = value.split(' ');
const [year, month, day] = date.split('-');
const [hour, minute] = time.split(':');
return `${month}-${day} ${hour}:${minute}`;
}
return value;
}
},
// axisTick: {
// //去除刻度
// show: false,
// },
// axisLine: {
// //去除轴线
// show: false,
// },
data: []
},
yAxis: [
{
// position: "left",
type: "value",
name: "GH/s",
nameTextStyle: {
padding: [0, 0, 0, -40],
},
// min: `dataMin`,
// max: `dataMax`,
axisLabel: {
formatter: function (value) {
// let data
// if (value > 10000000) {
// data = `${(value / 10000000)} KW`
// } else if (value > 1000000) {
// data = `${(value / 1000000)} M`
// } else if (value / 10000) {
// data = `${(value / 10000)} W`
// }
return value
}
}
},
{
position: "right",
// type: "log",
// splitNumber: "5",
show: true,
// min: 0,
// max: this.maxValue,
splitLine: {//不显示右侧Y轴横线
show: false
},
// name: "",
nameTextStyle: {
padding: [0, 0, 0, 40],
}
},
],
dataZoom: [
{
type: "inside",
start: 50, // 默认显示后30%的数据,避免图表过于拥挤
end: 100,
maxSpan: 100,
minSpan: 2, // 最小显示5%的数据
animation: false,
},
{
type: "inside",
start: 50,
end: 100,
height: 20, // 滑动条高度
bottom: 0, // 滑动条位置
showDetail: false, // 不显示详细数值
},
],
series: [
{//在线数量
name: "Number of users online",
type: "bar",
data: [],
yAxisIndex: 1,
itemStyle: {
color: '#239342', // 这里设置柱子的主色为绿色
},
barWidth: '40%', // 使用百分比宽度,保持适当的柱子宽度
barGap: '10%', // 同类型柱子间的间隔
barCategoryGap: '20%', // 类目间的间隔
},
{
name: "Number of offline users",
type: "bar",
// smooth: false, //线条是否圆滑
// symbol: "circle",
// symbolSize: 5,
// showSymbol: false,
itemStyle: {
color: "#FE2E74",
borderColor: "rgba(221,220,107,0.1)",
},
barWidth: '40%', // 使用百分比宽度,保持适当的柱子宽度
barGap: '10%', // 同类型柱子间的间隔
barCategoryGap: '20%', // 类目间的间隔
data: [],
yAxisIndex: 1,
},
{
name: "User computing power",
type: "line",
smooth: false, //线条是否圆滑
symbol: "circle",
symbolSize: 5,
showSymbol: false,
itemStyle: {
color: "#5721E4",
borderColor: "rgba(221,220,107,0.1)",
borderWidth: 12,
},
lineStyle: {
//线条样式
color: "#5721E4",
width: "2",
},
// areaStyle: {
// color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
// {
// offset: 0,
// color: 'rgb(210,195,234)'
// },
// {
// offset: 1,
// color: 'rgb(255, 255, 255)'
// }
// ])
// },
zlevel: 1, z: 1,
data: [],
yAxisIndex: 0,
},
],
},
lineChartLoading:false,
lineChartData:[
{
"minerUser": "miner",
"pv": "8.00080",
"date": "2024-11-28T11:00:00",
"unit":"GH/s"
},
{
"minerUser": "miner",
"pv": "6.00060",
"date": "2024-11-28T11:30:00",
"unit":"GH/s"
},
{
"minerUser": "miner",
"pv": "0.05000",
"date": "2024-11-28T12:00:00",
"unit":"GH/s"
},
{
"minerUser": "miner",
"pv": "2.0000",
"date": "2024-11-28T12:30:00",
"unit":"GH/s"
},
{
"minerUser": "miner",
"pv": "0.3000",
"date": "2024-11-28T13:00:00",
"unit":"GH/s"
},
{
"minerUser": "miner",
"pv": "0.9000",
"date": "2024-11-28T13:30:00",
"unit":"GH/s"
},
{
"minerUser": "miner",
"pv": "15.0000",
"date": "2024-11-28T14:00:00",
"unit":"GH/s"
},
],
chartShow:true,
tableData:[],
historyBalance:[],
}
},
mounted() {
console.log('userDetails mounted', this.$route.path, this.$route.query)
let token
try{
token =JSON.parse(localStorage.getItem('token'))
}catch(e){
console.log(e);
}
if (!token) {
this.$router.push({ path: `/${lang}/login` });
}
const params= this.$route.query || JSON.parse(localStorage.getItem("userDetailsParams"));
this.lineChartParams.user=params.user
this.lineChartParams.minerUser=params.minerUser
this.lineChartParams.coin=params.coin
this.onlineStatusParams.user=params.user
this.onlineStatusParams.minerUser=params.minerUser
this.onlineStatusParams.coin=params.coin
this.userDetailsParams.coin=params.coin
this.userDetailsParams.minerUser=params.minerUser
@@ -66,42 +378,132 @@ export default {
localStorage.setItem("userDetailsParams", JSON.stringify(params));
this.fetchUserDetails(this.userDetailsParams);
this.fetchUserLineChart(this.lineChartParams);
this.fetchUserOnlineStatus(this.onlineStatusParams);
// 等待两个图表相关接口都完成后再调用inCharts
this.fetchChartData();
}
},
methods: {
//初始化图表
inCharts() {
if (this.myChart == null) {
this.myChart = echarts.init(document.getElementById("lineChart"));
}
this.option.series[0].name = this.$t(`backendSystem.onlineUserNum`)
this.option.series[1].name = this.$t(`backendSystem.offlineUserNum`)
this.option.series[2].name = this.$t(`backendSystem.userPower`)
this.myChart.setOption(this.option);
// 回调函数,在渲染完成后执行
this.myChart.on('finished', () => {
// 图表渲染完成
console.log('图表渲染完成');
});
window.addEventListener('resize', throttle(() => {
if (this.myChart) this.myChart.resize();
}, 200));
},
async fetchUserDetails(params) {
this.userDetailsLoading = true
// 这里写你的获取详情逻辑
this.setLoading('userDetailsLoading', true);
// 获取用户详情逻辑
const res = await getUserDetails(params)
console.log(res)
if(res && res.code == 200){
if (!res.data) {
// this.$message.error('未获取到用户信息');
this.noDataTip = true
this.userData.coin = this.userDetailsParams.coin
this.userData.user = this.userDetailsParams.minerUser
// this.userData.coin = this.userDetailsParams.coin
// this.userData.user = this.userDetailsParams.minerUser
}else{
this.userData = res.data
this.userData.shouldOutDate=`${this.userData.shouldOutDate.split("T")[0]} ${this.userData.shouldOutDate.split("T")[1]}`
this.userData.createDate=`${this.userData.createDate.split("T")[0]} ${this.userData.createDate.split("T")[1]}`
// this.userData.shouldOutDate=`${this.userData.shouldOutDate.split("T")[0]} ${this.userData.shouldOutDate.split("T")[1]}`
// this.userData.createDate=`${this.userData.createDate.split("T")[0]} ${this.userData.createDate.split("T")[1]}`
this.historyBalance=res.data.historyBalance
this.tableData = res.data.walletInInfo.sort((a, b) => new Date(b.createDate) - new Date(a.createDate));
this.noDataTip = false
}
}
this.userDetailsLoading = false
this.setLoading('userDetailsLoading', false);
},
async fetchUserLineChart(params) {
const res = await getUserLineChart(params)
console.log(res)
let xData = []
let pvData = []
if(res && res.code == 200){
if (!Array.isArray(res.data) || res.data.length === 0 || !res.data[0].unit) {
this.chartShow = false
return;
}
this.chartShow = true
this.lineChartData = res.data
this.lineChartData.forEach(item => {
item.date = item.date.split("T")[0] + " " + item.date.split("T")[1].split(".")[0]
xData.push(item.date)
pvData.push(Number(item.pv).toFixed(6))
})
this.option.xAxis.data = xData
this.option.series[2].data = pvData
this.option.yAxis[0].name = this.lineChartData[0].unit
// this.inCharts()
}
},
async fetchUserOnlineStatus(params) {
const res = await getUserOnlineStatus(params)
console.log(res)
// let xData = []
let offlineNum = []
let onlineNum = []
if(res && res.code == 200){
if (!Array.isArray(res.data) || res.data.length === 0 ) {
this.chartShow = false
return;
}
this.chartShow = true
this.onlineStatusData = res.data
this.onlineStatusData.forEach(item => {
offlineNum.push(item.offlineNum)
onlineNum.push(item.onlineNum)
})
this.option.series[0].data = onlineNum
this.option.series[1].data = offlineNum
}
},
/**
* 获取图表数据 - 等待两个接口都完成后再渲染图表
*/
async fetchChartData() {
try {
this.setLoading('lineChartLoading', true);
// 并行调用两个接口,等待都完成
await Promise.all([
this.fetchUserLineChart(this.lineChartParams),
this.fetchUserOnlineStatus(this.onlineStatusParams)
]);
// 两个接口都完成后,检查是否有数据再渲染图表
if (this.chartShow) {
this.inCharts();
}
} catch (error) {
console.error('获取图表数据失败:', error);
} finally {
this.setLoading('lineChartLoading', false);
}
},
goBack(){
const lang = this.$i18n.locale;
@@ -109,5 +511,31 @@ export default {
path: `/${lang}/userManagement`,
})
},
handleLineChatTimesChange(val){
console.log(val,'val');
if(val){
this.lineChartParams.startDate=val[0]
this.lineChartParams.endDate=val[1]
this.onlineStatusParams.startDate=val[0]
this.onlineStatusParams.endDate=val[1]
this.userDetailsParams.startDate=val[0]
this.userDetailsParams.endDate=val[1]
}else{
this.lineChartParams.startDate=""
this.lineChartParams.endDate=""
this.onlineStatusParams.startDate=""
this.onlineStatusParams.endDate=""
this.userDetailsParams.startDate=""
this.userDetailsParams.endDate=""
}
this.fetchUserDetails(this.userDetailsParams);
// 等待两个图表相关接口都完成后再调用inCharts
this.fetchChartData();
},
handelTime(time){
return time.split("T")[0] + " " + time.split("T")[1]
}
}
}

View File

@@ -1,87 +1,216 @@
<template>
<div v-loading="userDetailsLoading">
<div class="main-title">用户详情 <span @click="goBack" style="color: #409EFF;cursor: pointer;font-size: 16px;">返回</span> </div>
<section class="user-details-box">
<section class="user-details-form">
<el-form
:label-position="labelPosition"
:inline="true"
:model="userData"
class="demo-form-inline"
<div class="main-title">
{{$t('backendSystem.userDetails')}}
<span
@click="goBack"
style="color: #409eff; cursor: pointer; font-size: 16px;margin-left: 10px;"
>{{$t('backendSystem.return')}}</span
>
<el-row>
<el-col :span="8">
<el-form-item style="width: 80%;" label="币种:" label-width="100px" prop="coin">
<el-input disabled v-model="userData.coin"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item style="width: 80%;" label="挖矿账户:" label-width="100px" prop="user">
<el-input disabled v-model="userData.user"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item style="width: 80%;" label="交易金额:" label-width="100px" prop="amount">
<el-input disabled v-model="userData.amount"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item
label="收益分配日期:"
label-width="100px"
prop="createDate"
style="width: 80%;"
>
<el-input disabled v-model="userData.createDate"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item style="width: 80%;" label="最大高度:" label-width="100px" prop="maxHeight">
<el-input disabled v-model="userData.maxHeight"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item style="width: 80%;" label="实际转账日期:" label-width="100px" prop="shouldOutDate">
<el-input disabled v-model="userData.shouldOutDate"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item style="width: 55%;" label="转账地址:" label-width="100px" prop="address">
<el-input
disabled v-model="userData.address"
></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item style="width: 55%; max-height: 300px;overflow-y: auto;" label="历史支付地址:" label-width="100px" prop="address">
<p class="history-balance-item" v-for="item in userData.historyBalance" :key="item.balance">{{item.balance}}</p>
</el-form-item>
</el-col>
</el-row>
</el-form>
</section>
<section class="chartBox">
<div class="lineChartBox">用户挖矿曲线图</div>
<div class="barChartBox">柱状图</div>
</section>
</section>
</div>
<section class="user-details-box">
<!-- <section class="user-details-form">
<el-form
:label-position="labelPosition"
:inline="true"
:model="userData"
class="demo-form-inline"
>
<el-row>
<el-col :span="8">
<el-form-item
style="width: 80%"
:label="$t('backendSystem.coin')"
label-width="100px"
prop="coin"
>
<el-input disabled v-model="userData.coin"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item
style="width: 80%"
:label="$t('backendSystem.user')"
label-width="100px"
prop="user"
>
<el-input disabled v-model="userData.user"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item
style="width: 80%"
:label="$t('backendSystem.amount')"
label-width="100px"
prop="amount"
>
<el-input disabled v-model="userData.amount"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="8">
<el-form-item
:label="$t('backendSystem.createDate')"
label-width="100px"
prop="createDate"
style="width: 80%"
>
<el-input disabled v-model="userData.createDate"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item
style="width: 80%"
:label="$t('backendSystem.maxHeight')"
label-width="100px"
prop="maxHeight"
>
<el-input disabled v-model="userData.maxHeight"></el-input>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item
style="width: 80%"
:label="$t('backendSystem.shouldOutDate')"
label-width="100px"
prop="shouldOutDate"
>
<el-input disabled v-model="userData.shouldOutDate"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item
style="width: 55%"
:label="$t('backendSystem.address')"
label-width="100px"
prop="address"
>
<el-input disabled v-model="userData.address"></el-input>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-form-item
style="width: 55%; max-height: 300px; overflow-y: auto"
:label="$t('backendSystem.historyAddress')"
label-width="100px"
prop="address"
>
<p
class="history-balance-item"
v-for="item in userData.historyBalance"
:key="item.balance"
>
{{ item.balance }}
</p>
</el-form-item>
</el-col>
</el-row>
</el-form>
</section> -->
<el-date-picker
v-model="lineChatTimes"
type="daterange"
:range-separator="$t('backendSystem.to')"
:start-placeholder="$t('backendSystem.startDate')"
:end-placeholder="$t('backendSystem.endDate')"
@change="handleLineChatTimesChange"
value-format="yyyy-MM-dd HH:mm:ss"
size="small"
style="margin-bottom: 18px;"
>
</el-date-picker>
<el-table
:data="tableData"
border
:header-cell-style="{ 'text-align': 'center' }"
:cell-style="{ 'text-align': 'center' }"
height="42vh"
>
<el-table-column prop="createDate" :label="$t('backendSystem.createDate')" width="160" >
<template slot-scope="scope">
<span>{{ handelTime(scope.row.createDate ) }}</span>
</template>
</el-table-column>
<el-table-column prop="shouldOutDate" :label="$t('backendSystem.shouldOutDate')" width="180" show-overflow-tooltip >
<template slot-scope="scope">
<span>{{ handelTime(scope.row.shouldOutDate ) }}</span>
</template>
</el-table-column>
<el-table-column prop="coin" :label="$t('backendSystem.coin2')" width="100" show-overflow-tooltip>
</el-table-column>
<el-table-column prop="amount" :label="$t('backendSystem.amount2')" width="200" show-overflow-tooltip/>
<el-table-column prop="user" :label="$t('backendSystem.minerUser2')" width="180" show-overflow-tooltip/>
<el-table-column prop="address" :label="$t('backendSystem.address')" show-overflow-tooltip />
<el-table-column prop="maxHeight" :label="$t('backendSystem.maxHeight')" width="180" show-overflow-tooltip />
</el-table>
<section class="chartBox">
<div class="lineChartBox">
<div class="lineChartBox-header">
<div class="lineChartBox-title">{{$t('backendSystem.chartTitle')}}</div>
<!-- <el-date-picker
v-model="lineChatTimes"
type="daterange"
:range-separator="$t('backendSystem.to')"
:start-placeholder="$t('backendSystem.startDate')"
:end-placeholder="$t('backendSystem.endDate')"
@change="handleLineChatTimesChange"
value-format="yyyy-MM-dd HH:mm:ss"
size="small"
:disabled="!chartShow"
>
</el-date-picker> -->
</div>
<div
class="lineChartBox-content"
style="width: 100%; height: 500px; margin-top: 35px;"
>
<div v-if="!chartShow" style="text-align: center;margin-top: 200px;color: #ccc;">{{$t('backendSystem.noData')}}</div>
<div v-else v-loading="lineChartLoading" id="lineChart" style="width: 100%; height: 100%"></div>
</div>
</div>
</section>
<el-row>
<el-col :span="24">
<div
style="width: 100%; max-height: 300px; overflow-y: auto;"
>
<h3 style="margin-bottom: 20px;">{{$t('backendSystem.historyAddress')}}</h3>
<p
class="history-balance-item"
v-for="item in historyBalance"
:key="item.balance"
>
{{ item.balance }}
</p>
</div>
</el-col>
</el-row>
</section>
</div>
</template>
@@ -100,7 +229,7 @@ export default {
color: #333;
margin-bottom: 18px;
}
.user-details-box{
.user-details-box {
width: 100%;
margin: 0 auto;
box-sizing: border-box;
@@ -124,55 +253,76 @@ export default {
// background: palegoldenrod;
}
.no-data-tip{
.no-data-tip {
width: 80%;
margin: 0 auto;
font-size: 16px;
color: #999;
text-align: center;
margin-top: 20px;
}
.history-balance-item{
margin: 0;
padding: 0;
background:#E7DFF3 ;
padding: 0px 20px;
.history-balance-item {
display: inline-block; /* 让宽度随内容自适应 */
background: #e7dff3;
padding: 8px 20px;
border-radius: 20px;
margin-bottom: 10px;
color: rgba(0,0,0,0.6);
color: rgba(0, 0, 0, 0.6);
/* width: auto; 其实可以省略inline-block默认就是auto */
/* 防止flex布局下被拉伸 */
flex: none;
align-self: flex-start;
margin-left: 10px;
}
::v-deep .el-form-item__label {
// color: #409EFF; /* 例如设置为 Element UI 主色 */
font-weight: bold; /* 加粗 */
font-size: 16px; /* 字号 */
letter-spacing: 1px; /* 字间距 */
font-weight: bold; /* 加粗 */
font-size: 16px; /* 字号 */
letter-spacing: 1px; /* 字间距 */
/* 你可以根据需要添加更多样式 */
}
.chartBox{
.chartBox {
width: 80vw;
box-sizing: border-box;
// background: palegoldenrod;
height: 800px;
padding: 0;
margin: 30px 0;
}
.lineChartBox {
width: 97%;
height: 80%;
box-sizing: border-box;
box-shadow: 0px 0px 1px 1px #ccc;
padding: 20px;
border-radius: 10px;
}
.lineChartBox-title {
font-size: 16px;
font-weight: bold;
color: rgba(0, 0, 0, 0.6);
margin-bottom: 20px;
}
.lineChartBox-header{
display: flex;
align-content: center;
justify-content: space-around;
justify-content: space-between;
align-items: center;
margin-bottom: 18px;
padding:1% 5%;
}
.lineChartBox{
width: 49%;
height: 100%;
background: palegoldenrod;
</style>
<style>
.el-date-editor .el-range-separator {
padding: 0 !important;
color: #333 !important;
}
.barChartBox{
width: 49%;
height: 100%;
background: pink;
}
</style>
</style>

View File

@@ -24,17 +24,17 @@ export default {
user: [
{
type: 'email',
message: '请输入正确的邮箱地址',
message: this.$t('backendSystem.pleaseInputCorrectEmail'),
trigger: ['blur', 'change']
}
]
},
emailRules: {
subject: [
{ required: true, message: '请输入邮件主题', trigger: 'blur' }
{ required: true, message: this.$t('backendSystem.pleaseInputSubject'), trigger: 'blur' }
],
text: [
{ required: true, message: '请输入邮件内容', trigger: 'blur' }
{ required: true, message: this.$t('backendSystem.pleaseInputText'), trigger: 'blur' }
],
to: [
{
@@ -56,7 +56,7 @@ export default {
// 检查格式
for (let email of emails) {
if (!emailReg.test(email)) {
callback(new Error('请输入正确的邮箱地址,多个邮箱用逗号分隔'));
callback(new Error(this.$t('backendSystem.pleaseInputCorrectEmail2')));
return;
}
}
@@ -65,7 +65,7 @@ export default {
for (let email of emails) {
const lower = email.toLowerCase();
if (lowerSet.has(lower)) {
callback(new Error('存在重复邮箱,请检查'));
callback(new Error(this.$t('backendSystem.existDuplicateEmail')));
return;
}
lowerSet.add(lower);
@@ -83,6 +83,9 @@ export default {
to:"",
},
sendEmailLoading: false,
total: 0,
pageSizes: [50, 100, 300],
currentPage: 1,
}
@@ -94,11 +97,11 @@ export default {
}catch(e){
console.log(e);
}
if (token) {
if (!token) {
this.$router.push({ path: `/${lang}/login` });
this.fetchUserList(this.userListParams);
}
this.fetchUserList(this.userListParams);
this.currencyList = JSON.parse(localStorage.getItem("currencyList"))
window.addEventListener("setItem", () => {
this.currencyList = JSON.parse(localStorage.getItem("currencyList"))
@@ -110,9 +113,12 @@ export default {
this.setLoading('userManagementLoading', true);
const data = await getUserList(params);
console.log(data,'data');
if (data && data.code == 200) {
this.tableData = data.rows;
this.total = data.total;
}
this.setLoading('userManagementLoading', false);
@@ -124,7 +130,7 @@ export default {
if (data && data.code == 200) {
this.$message.success('发送成功');
this.$message.success(this.$t('backendSystem.sendSuccess'));
this.dialogVisible = false;
for (const key in this.senParams) {
this.senParams[key] = "";
@@ -146,7 +152,11 @@ export default {
},
handelImg(coin) {
return this.currencyList.find(item => item.value === coin)?.imgUrl || '';
if(this.currencyList &&this.currencyList.length > 0 && coin){
return this.currencyList.find(item => item.value === coin)?.imgUrl || '';
}else{
return '';
}
},
handelQuery() {
this.$refs.formRef.validate((valid) => {
@@ -158,7 +168,7 @@ export default {
}
if (!this.userListParams.minerUser && !this.userListParams.user) {
this.$message.error('请输入查询条件(挖矿账号、邮箱)');
this.$message.error(this.$t('backendSystem.pleaseInputQueryConditions'));
return;
}
this.fetchUserList(this.userListParams);
@@ -218,6 +228,23 @@ export default {
// 保存ID到localStorage
localStorage.setItem("userDetailsParams",JSON.stringify(obj));
}
},
handleSizeChange(val) {
console.log(`每页 ${val}`);
this.userListParams.pageSize = val
this.userListParams.pageNum = 1
this.currentPage = 1
this.fetchUserList(this.userListParams);
},
handleCurrentChange(val) {
console.log(`当前页: ${val}`);
this.userListParams.pageNum = val
this.fetchUserList(this.userListParams);
},
}
}

View File

@@ -1,9 +1,9 @@
<template>
<div v-loading="userManagementLoading">
<div class="main-title">注册用户管理</div>
<div class="main-title">{{$t('backendSystem.userManagementTitle')}}</div>
<el-form :inline="true" :model="userListParams" class="demo-form-inline" :rules="rules" ref="formRef">
<el-form-item label="币种:" prop="coin">
<el-form-item :label="$t('backendSystem.coin')" prop="coin">
<el-select
class="input"
@@ -28,21 +28,21 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item label="挖矿账号:" style="margin-left: 5vw" prop="minerUser">
<el-input v-model="userListParams.minerUser" placeholder="挖矿账号" clearable @clear="handleInputClear"></el-input>
<el-form-item :label="$t('backendSystem.user')" style="margin-left: 5vw" prop="minerUser">
<el-input v-model="userListParams.minerUser" :placeholder="$t('backendSystem.pleaseInput')" clearable @clear="handleInputClear"></el-input>
</el-form-item>
<el-form-item label="邮箱:" style="margin-left: 5vw" prop="user">
<el-form-item :label="$t('backendSystem.email')" style="margin-left: 5vw" prop="user">
<el-input
v-model="userListParams.user"
type="email"
placeholder="邮箱"
:placeholder="$t('backendSystem.pleaseInput')"
clearable
@clear="handleInputClear"
></el-input>
</el-form-item>
<el-form-item style="margin-left: 1vw">
<el-button type="primary" @click="handelQuery">查询</el-button>
<el-button type="primary" @click="handelQuery">{{$t('backendSystem.query')}}</el-button>
</el-form-item>
</el-form>
<el-table
@@ -51,10 +51,10 @@
style="width: 100%; margin-bottom: 18px"
:header-cell-style="{ 'text-align': 'center' }"
:cell-style="{ 'text-align': 'center' }"
height="65vh"
height="60vh"
>
<el-table-column prop="id" label="ID" width="60" />
<el-table-column prop="coin" label="币种" width="100" show-overflow-tooltip>
<el-table-column prop="coin" :label="$t('backendSystem.coin2')" width="100" show-overflow-tooltip>
<template slot-scope="scope" >
<div style="display: flex; align-items: center;justify-content: center;">
<img :src="handelImg(scope.row.coin)" style="width: 20px" />
@@ -62,59 +62,76 @@
</div>
</template>
</el-table-column>
<el-table-column prop="user" label="用户邮箱" width="200" show-overflow-tooltip/>
<el-table-column prop="amount" label="最小起付金额" width="100" show-overflow-tooltip/>
<el-table-column prop="status" label="用户状态" width="80">
<el-table-column prop="user" :label="$t('backendSystem.email2')" width="200" show-overflow-tooltip/>
<el-table-column prop="amount" :label="$t('backendSystem.amount2')" width="150" show-overflow-tooltip/>
<el-table-column prop="status" :label="$t('backendSystem.status2')" width="100">
<template slot-scope="scope">
<el-tag :type="scope.row.status === '1' ? 'success' : 'danger'">{{
scope.row.status == "0" ? "正常" : "删除"
<el-tag :type="scope.row.status === '1' ? 'success' : 'danger'">{{
scope.row.status == "0" ? $t('backendSystem.normal') : $t('backendSystem.delete')
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="minerUser" label="挖矿账号" width="180" show-overflow-tooltip/>
<el-table-column prop="balance" label="支付地址" show-overflow-tooltip />
<el-table-column prop="active" label="是否自动提现" width="80" show-overflow-tooltip>
<el-table-column prop="minerUser" :label="$t('backendSystem.minerUser2')" width="180" show-overflow-tooltip/>
<el-table-column prop="balance" :label="$t('backendSystem.balance2')" show-overflow-tooltip />
<el-table-column prop="active" :label="$t('backendSystem.active2')" width="100" show-overflow-tooltip>
<template slot-scope="scope">
<el-tag :type="scope.row.active === '1' ? 'success' : 'danger'">{{
scope.row.active == "0" ? "是" : "否"
scope.row.active == "0" ? $t('backendSystem.yes') : $t('backendSystem.no')
}}</el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('backendSystem.operation')" width="180">
<el-table-column :label="$t('backendSystem.operation')" width="200">
<template slot-scope="scope">
<el-button size="mini" @click="handleDetails(scope.row)" type="primary" plain>详情</el-button>
<el-button size="mini" @click="handleDetails(scope.row)" type="primary" plain>{{$t('backendSystem.Details')}}</el-button>
<el-button size="mini" @click="sendEmail(scope.row)" style="color: #651fff; border: 1px solid #651fff"
>发送邮件</el-button
>{{$t('backendSystem.sendEmail')}}</el-button
>
</template>
</el-table-column>
</el-table>
<el-row>
<el-col :span="24" style="display: flex; justify-content: center;">
<el-pagination
style="margin:0 auto;margin-top: 10px;"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="currentPage"
:page-sizes="pageSizes"
:page-size="userListParams.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</el-col>
</el-row>
<!-- 发送邮箱弹窗-->
<el-dialog
title="邮件内容"
:title="$t('backendSystem.emailContent')"
:visible.sync="dialogVisible"
width="50%"
:before-close="handleClose"
:close-on-click-modal="false"
>
<el-form :model="senParams" ref="formRef" :rules="emailRules">
<el-form-item label="收件人" prop="to">
<el-form-item :label="$t('backendSystem.recipient')" prop="to">
<el-input maxlength="500" resize="none" v-model="senParams.to" type="textarea" :rows="2" />
<div style="color: #999; font-size: 12px; margin-top: 4px;">可输入多个邮箱用逗号隔开</div>
<div style="color: #999; font-size: 12px; margin-top: 4px;">{{$t('backendSystem.emailRemind')}}</div>
</el-form-item>
<el-form-item label="邮件主题" prop="subject">
<el-form-item :label="$t('backendSystem.subject')" prop="subject">
<el-input resize="none" maxlength="300" show-word-limit v-model="senParams.subject" type="textarea" :rows="3" />
</el-form-item>
<el-form-item label="邮件内容" prop="text">
<el-form-item :label="$t('backendSystem.text')" prop="text">
<el-input resize="none" maxlength="600" show-word-limit v-model="senParams.text" type="textarea" :rows="8" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="handleClose">{{$t("backendSystem.cancel")}}</el-button>
<el-button type="primary" :loading="sendEmailLoading" @click="sureSendEmail">发送</el-button>
<el-button type="primary" :loading="sendEmailLoading" @click="sureSendEmail">{{$t("backendSystem.send")}}</el-button>
</div>
</el-dialog>

View File

@@ -248,7 +248,7 @@
:cell-style="{ 'text-align': 'center' }"
:data="from1"
style="width: 100%; text-transform: none;"
max-height="600"
max-height="550"
stripe
>
<el-table-column prop="id" :label="$t(`work.WorkID`)" > </el-table-column>
@@ -290,7 +290,7 @@
</el-table-column>
</el-table>
<!-- 分页 -->
<el-row type="flex" justify="center" style="margin-top: 15px;">
<el-row type="flex" justify="center" style="margin-top: 20px;margin-bottom: 10px;">
<el-col :span="10">
<el-pagination
@size-change="handleSizeChange"