|
|
|
@ -1,36 +1,84 @@ |
|
|
|
<template> |
|
|
|
<el-dialog v-dialogDrag class="setting" title="设置" :visible.sync="visible" width="420px" :before-close="onClose"> |
|
|
|
<el-form :model="userInfo" label-width="80px" :rules="rules" ref="settingForm" size="small"> |
|
|
|
<el-form-item label="头像" style="margin-bottom: 0 !important;"> |
|
|
|
<file-upload class="avatar-uploader" :action="imageAction" :showLoading="true" :maxSize="maxSize" |
|
|
|
:isPermanent="true" @success="onUploadSuccess" |
|
|
|
:fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp']"> |
|
|
|
<img v-if="userInfo.headImage" :src="userInfo.headImage" class="avatar"> |
|
|
|
<i v-else class="el-icon-plus avatar-uploader-icon"></i> |
|
|
|
</file-upload> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="用户名"> |
|
|
|
<el-input disabled v-model="userInfo.userName" autocomplete="off" size="small"></el-input> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item prop="nickName" label="昵称"> |
|
|
|
<el-input v-model="userInfo.nickName" autocomplete="off" size="small" maxlength="20"></el-input> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="性别"> |
|
|
|
<el-radio-group v-model="userInfo.sex"> |
|
|
|
<el-radio :label="0">男</el-radio> |
|
|
|
<el-radio :label="1">女</el-radio> |
|
|
|
</el-radio-group> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="个性签名"> |
|
|
|
<el-input type="textarea" v-model="userInfo.signature" :rows="3" maxlength="64"></el-input> |
|
|
|
</el-form-item> |
|
|
|
</el-form> |
|
|
|
|
|
|
|
<span slot="footer" class="dialog-footer"> |
|
|
|
<el-button @click="onClose()">取 消</el-button> |
|
|
|
<el-button type="primary" @click="onSubmit()">确 定</el-button> |
|
|
|
</span> |
|
|
|
</el-dialog> |
|
|
|
<div> |
|
|
|
<!-- 设置弹窗 --> |
|
|
|
<el-dialog v-dialogDrag class="setting" title="设置" :visible.sync="visible" width="420px" :before-close="onClose"> |
|
|
|
<el-form :model="userInfo" label-width="80px" :rules="rules" ref="settingForm" size="small"> |
|
|
|
<el-form-item label="头像" style="margin-bottom: 0 !important;"> |
|
|
|
<file-upload class="avatar-uploader" :action="imageAction" :showLoading="true" :maxSize="maxSize" |
|
|
|
:isPermanent="true" @success="onUploadSuccess" |
|
|
|
:fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp']"> |
|
|
|
<img v-if="userInfo.headImage" :src="userInfo.headImage" class="avatar"> |
|
|
|
<i v-else class="el-icon-plus avatar-uploader-icon"></i> |
|
|
|
</file-upload> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="用户名"> |
|
|
|
<el-input disabled v-model="userInfo.userName" autocomplete="off" size="small"></el-input> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item prop="nickName" label="昵称"> |
|
|
|
<el-input v-model="userInfo.nickName" autocomplete="off" size="small" maxlength="20"></el-input> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="性别"> |
|
|
|
<el-radio-group v-model="userInfo.sex"> |
|
|
|
<el-radio :label="0">男</el-radio> |
|
|
|
<el-radio :label="1">女</el-radio> |
|
|
|
</el-radio-group> |
|
|
|
</el-form-item> |
|
|
|
<el-form-item label="个性签名"> |
|
|
|
<el-input type="textarea" v-model="userInfo.signature" :rows="3" maxlength="64"></el-input> |
|
|
|
</el-form-item> |
|
|
|
</el-form> |
|
|
|
|
|
|
|
<span slot="footer" class="dialog-footer"> |
|
|
|
<el-button @click="onSwitchAccount" style="float: left;">切换账号</el-button> |
|
|
|
<el-button @click="onClose()">取 消</el-button> |
|
|
|
<el-button type="primary" @click="onSubmit()">确 定</el-button> |
|
|
|
</span> |
|
|
|
</el-dialog> |
|
|
|
|
|
|
|
<!-- 切换账号弹窗 --> |
|
|
|
<el-dialog title="切换账号" :visible.sync="switchDialogVisible" width="500px" append-to-body :close-on-click-modal="false"> |
|
|
|
<el-input |
|
|
|
v-model="searchKeyword" |
|
|
|
placeholder="请输入客服昵称搜索" |
|
|
|
@keyup.enter.native="filterCustomerList" |
|
|
|
@clear="clearSearch" |
|
|
|
clearable |
|
|
|
style="margin-bottom: 20px"> |
|
|
|
<el-button slot="append" icon="el-icon-search" @click="filterCustomerList">搜索</el-button> |
|
|
|
</el-input> |
|
|
|
|
|
|
|
<div class="user-list" v-loading="userListLoading"> |
|
|
|
<div v-for="user in filteredUserList" :key="user.id" class="user-item" @click="switchToAccount(user)"> |
|
|
|
<img :src="user.headImageThumb || defaultAvatar" class="user-avatar" @error="handleAvatarError"> |
|
|
|
<div class="user-info"> |
|
|
|
<div class="user-name"> |
|
|
|
<span>{{ user.nickName }}</span> |
|
|
|
<el-tag type="success" size="mini" style="margin-left: 8px">客服</el-tag> |
|
|
|
<el-tag v-if="user.id === userStore.userInfo.id" type="warning" size="mini" style="margin-left: 8px">当前</el-tag> |
|
|
|
</div> |
|
|
|
<div class="user-username">用户名: {{ user.userName }}</div> |
|
|
|
<div class="user-id">ID: {{ user.id }}</div> |
|
|
|
</div> |
|
|
|
<el-button |
|
|
|
:type="user.id === userStore.userInfo.id ? 'info' : 'primary'" |
|
|
|
size="small" |
|
|
|
:disabled="user.id === userStore.userInfo.id" |
|
|
|
@click.stop="switchToAccount(user)"> |
|
|
|
{{ user.id === userStore.userInfo.id ? '当前账号' : '切换' }} |
|
|
|
</el-button> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div v-if="filteredUserList.length === 0 && !userListLoading" class="empty-text"> |
|
|
|
<i class="el-icon-info"></i> |
|
|
|
<p>{{ searchKeyword ? '未找到相关客服' : '暂无其他客服账号' }}</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
<span slot="footer" class="dialog-footer"> |
|
|
|
<el-button @click="switchDialogVisible = false">取 消</el-button> |
|
|
|
</span> |
|
|
|
</el-dialog> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script> |
|
|
|
@ -52,13 +100,37 @@ export default { |
|
|
|
message: '请输入昵称', |
|
|
|
trigger: 'blur' |
|
|
|
}] |
|
|
|
}, |
|
|
|
// 切换账号相关 |
|
|
|
switchDialogVisible: false, |
|
|
|
searchKeyword: '', |
|
|
|
allCustomerList: [], // 存储所有客服列表 |
|
|
|
userListLoading: false, |
|
|
|
defaultAvatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png' |
|
|
|
} |
|
|
|
}, |
|
|
|
computed: { |
|
|
|
imageAction() { |
|
|
|
return `/image/upload?thumbSize=20`; |
|
|
|
}, |
|
|
|
// 过滤后的客服列表 |
|
|
|
filteredUserList() { |
|
|
|
if (!this.searchKeyword.trim()) { |
|
|
|
return this.allCustomerList; |
|
|
|
} |
|
|
|
const keyword = this.searchKeyword.toLowerCase(); |
|
|
|
return this.allCustomerList.filter(user => |
|
|
|
user.nickName?.toLowerCase().includes(keyword) || |
|
|
|
user.userName?.toLowerCase().includes(keyword) || |
|
|
|
user.id?.toString().includes(keyword) |
|
|
|
); |
|
|
|
} |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
onClose() { |
|
|
|
this.$emit("close"); |
|
|
|
}, |
|
|
|
|
|
|
|
onSubmit() { |
|
|
|
this.$refs['settingForm'].validate((valid) => { |
|
|
|
if (!valid) { |
|
|
|
@ -75,6 +147,179 @@ export default { |
|
|
|
}) |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
// 切换账号 |
|
|
|
onSwitchAccount() { |
|
|
|
// 检查权限:只有客服才能切换账号(isCustomer == 2) |
|
|
|
// const currentUser = this.userStore.userInfo; |
|
|
|
// console.log(currentthis.userStoreUser); |
|
|
|
// if (currentUser.isCustomer !== 2) { |
|
|
|
// this.$message.warning('只有客服账号才能切换账号'); |
|
|
|
// return; |
|
|
|
// } |
|
|
|
this.showSwitchAccountDialog(); |
|
|
|
}, |
|
|
|
|
|
|
|
// 显示切换账号弹窗 |
|
|
|
showSwitchAccountDialog() { |
|
|
|
this.switchDialogVisible = true; |
|
|
|
this.searchKeyword = ''; |
|
|
|
this.loadCustomerList(); |
|
|
|
}, |
|
|
|
|
|
|
|
// 加载客服列表 |
|
|
|
async loadCustomerList() { |
|
|
|
this.userListLoading = true; |
|
|
|
try { |
|
|
|
const res = await this.$http({ |
|
|
|
url: '/user/getEnableChangeCustomer', |
|
|
|
method: 'post' |
|
|
|
}); |
|
|
|
|
|
|
|
let apiCustomers = res.data || res || []; |
|
|
|
const currentUser = this.userStore.userInfo; |
|
|
|
|
|
|
|
// ============================================== |
|
|
|
// 1. 从本地缓存读取所有历史账号 |
|
|
|
// ============================================== |
|
|
|
let cachedCustomers = JSON.parse(localStorage.getItem('allCustomerAccounts') || '[]'); |
|
|
|
|
|
|
|
// ============================================== |
|
|
|
// 2. 合并接口返回 + 本地缓存,去重 |
|
|
|
// ============================================== |
|
|
|
let allCustomers = [...apiCustomers, ...cachedCustomers]; |
|
|
|
// 按 id 去重 |
|
|
|
allCustomers = Array.from(new Map(allCustomers.map(item => [item.id, item])).values()); |
|
|
|
|
|
|
|
// ============================================== |
|
|
|
// 3. 把当前账号加入列表(如果不存在) |
|
|
|
// ============================================== |
|
|
|
const currentExists = allCustomers.some(item => item.id === currentUser.id); |
|
|
|
if (!currentExists) { |
|
|
|
const fullCurrentUser = { |
|
|
|
id: currentUser.id, |
|
|
|
nickName: currentUser.nickName, |
|
|
|
userName: currentUser.userName, |
|
|
|
headImageThumb: currentUser.headImageThumb, |
|
|
|
headImage: currentUser.headImage |
|
|
|
}; |
|
|
|
allCustomers.unshift(fullCurrentUser); |
|
|
|
} |
|
|
|
|
|
|
|
// ============================================== |
|
|
|
// 4. 把完整列表写回本地缓存(永久保存) |
|
|
|
// ============================================== |
|
|
|
localStorage.setItem('allCustomerAccounts', JSON.stringify(allCustomers)); |
|
|
|
|
|
|
|
// ============================================== |
|
|
|
// 5. 最终列表:当前账号置顶,其他按顺序 |
|
|
|
// ============================================== |
|
|
|
this.allCustomerList = allCustomers.sort((a, b) => { |
|
|
|
if (a.id === currentUser.id) return -1; |
|
|
|
if (b.id === currentUser.id) return 1; |
|
|
|
return 0; |
|
|
|
}); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
console.error('加载客服列表失败:', error); |
|
|
|
this.$message.error('加载客服列表失败'); |
|
|
|
} finally { |
|
|
|
this.userListLoading = false; |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 过滤客服列表(前端搜索) |
|
|
|
filterCustomerList() { |
|
|
|
// 前端过滤,不需要调用接口 |
|
|
|
// computed 属性会自动处理过滤 |
|
|
|
if (this.searchKeyword.trim() && this.filteredUserList.length === 0) { |
|
|
|
this.$message.info('未找到相关客服'); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
// 清除搜索 |
|
|
|
clearSearch() { |
|
|
|
this.searchKeyword = ''; |
|
|
|
}, |
|
|
|
|
|
|
|
// 执行切换账号(刷新页面版本,最稳定!) |
|
|
|
async switchToAccount(targetUser) { |
|
|
|
if (targetUser.id === this.userStore.userInfo.id) { |
|
|
|
this.$message.warning('已是当前账号'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
try { |
|
|
|
await this.$confirm(`确定要切换到客服账号【${targetUser.nickName}】吗?`, '提示', { |
|
|
|
confirmButtonText: '确定', |
|
|
|
cancelButtonText: '取消', |
|
|
|
type: 'warning' |
|
|
|
}); |
|
|
|
|
|
|
|
const loading = this.$loading({ |
|
|
|
lock: true, |
|
|
|
text: '正在切换账号...', |
|
|
|
spinner: 'el-icon-loading' |
|
|
|
}); |
|
|
|
|
|
|
|
// 调用切换接口 |
|
|
|
const res = await this.$http({ |
|
|
|
url: '/user/switchAccount', |
|
|
|
method: 'post', |
|
|
|
data: { |
|
|
|
targetUserId: targetUser.id, |
|
|
|
terminal: this.getTerminalType() |
|
|
|
} |
|
|
|
}); |
|
|
|
|
|
|
|
const loginData = res; |
|
|
|
|
|
|
|
// 保存新token |
|
|
|
if (loginData.accessToken) { |
|
|
|
localStorage.setItem('accessToken', loginData.accessToken); |
|
|
|
} |
|
|
|
if (loginData.refreshToken) { |
|
|
|
localStorage.setItem('refreshToken', loginData.refreshToken); |
|
|
|
} |
|
|
|
|
|
|
|
// 保存新用户信息 |
|
|
|
if (loginData.user) { |
|
|
|
localStorage.setItem('userInfo', JSON.stringify(loginData.user)); |
|
|
|
} |
|
|
|
|
|
|
|
loading.close(); |
|
|
|
|
|
|
|
this.$message.success(`已切换到客服账号:${targetUser.nickName}`); |
|
|
|
|
|
|
|
// 刷新页面 → 所有数据强制重置为新账号 |
|
|
|
setTimeout(() => { |
|
|
|
window.location.reload(); |
|
|
|
}, 10); |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
if (error !== 'cancel') { |
|
|
|
console.error('切换账号失败:', error); |
|
|
|
this.$message.error('切换账号失败'); |
|
|
|
} |
|
|
|
} |
|
|
|
}, |
|
|
|
// 获取终端类型 |
|
|
|
getTerminalType() { |
|
|
|
const userAgent = navigator.userAgent; |
|
|
|
if (/mobile/i.test(userAgent)) { |
|
|
|
return 2; // APP端 |
|
|
|
} |
|
|
|
if (/tablet/i.test(userAgent)) { |
|
|
|
return 3; // 平板端 |
|
|
|
} |
|
|
|
return 1; // WEB端 |
|
|
|
}, |
|
|
|
|
|
|
|
// 头像加载错误处理 |
|
|
|
handleAvatarError(e) { |
|
|
|
e.target.src = this.defaultAvatar; |
|
|
|
}, |
|
|
|
|
|
|
|
onUploadSuccess(data, file) { |
|
|
|
this.userInfo.headImage = data.originUrl; |
|
|
|
this.userInfo.headImageThumb = data.thumbUrl; |
|
|
|
@ -85,19 +330,15 @@ export default { |
|
|
|
type: Boolean |
|
|
|
} |
|
|
|
}, |
|
|
|
computed: { |
|
|
|
imageAction() { |
|
|
|
return `/image/upload?thumbSize=20`; |
|
|
|
} |
|
|
|
}, |
|
|
|
watch: { |
|
|
|
visible: function () { |
|
|
|
// 深拷贝 |
|
|
|
let mine = this.userStore.userInfo; |
|
|
|
this.userInfo = JSON.parse(JSON.stringify(mine)); |
|
|
|
visible: function (newVal) { |
|
|
|
if (newVal) { |
|
|
|
// 现在这里取到的永远是最新账号! |
|
|
|
this.userInfo = JSON.parse(JSON.stringify(this.userStore.userInfo)); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
</script> |
|
|
|
|
|
|
|
<style lang="scss" scoped> |
|
|
|
@ -137,4 +378,76 @@ export default { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.user-list { |
|
|
|
max-height: 400px; |
|
|
|
overflow-y: auto; |
|
|
|
|
|
|
|
.user-item { |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
padding: 12px; |
|
|
|
border-bottom: 1px solid #f0f0f0; |
|
|
|
cursor: pointer; |
|
|
|
transition: background-color 0.3s; |
|
|
|
|
|
|
|
&:hover { |
|
|
|
background-color: #f5f7fa; |
|
|
|
} |
|
|
|
|
|
|
|
.user-avatar { |
|
|
|
width: 40px; |
|
|
|
height: 40px; |
|
|
|
border-radius: 50%; |
|
|
|
margin-right: 12px; |
|
|
|
object-fit: cover; |
|
|
|
flex-shrink: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.user-info { |
|
|
|
flex: 1; |
|
|
|
min-width: 0; |
|
|
|
|
|
|
|
.user-name { |
|
|
|
font-weight: 500; |
|
|
|
margin-bottom: 4px; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
flex-wrap: wrap; |
|
|
|
|
|
|
|
span { |
|
|
|
overflow: hidden; |
|
|
|
text-overflow: ellipsis; |
|
|
|
white-space: nowrap; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.user-username { |
|
|
|
font-size: 12px; |
|
|
|
color: #909399; |
|
|
|
margin-bottom: 2px; |
|
|
|
} |
|
|
|
|
|
|
|
.user-id { |
|
|
|
font-size: 11px; |
|
|
|
color: #c0c4cc; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
.empty-text { |
|
|
|
text-align: center; |
|
|
|
padding: 40px; |
|
|
|
color: #909399; |
|
|
|
|
|
|
|
i { |
|
|
|
font-size: 48px; |
|
|
|
margin-bottom: 10px; |
|
|
|
} |
|
|
|
|
|
|
|
p { |
|
|
|
margin: 0; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
</style> |