2 changed files with 939 additions and 0 deletions
@ -0,0 +1,845 @@ |
|||
<template> |
|||
<div> |
|||
<!-- 账号切换菜单主体 --> |
|||
<div |
|||
class="account-switch-menu" |
|||
v-if="visible" |
|||
@click.stop |
|||
ref="menu" |
|||
v-loading="loading"> |
|||
<!-- 当前账号 --> |
|||
<div class="menu-header"> |
|||
<div class="current-label">当前账号</div> |
|||
<div class="current-account"> |
|||
<img |
|||
:src="currentUser.headImageThumb || currentUser.headImage || defaultAvatar" |
|||
class="user-avatar" |
|||
@error="handleAvatarError"> |
|||
<div class="account-info"> |
|||
<div class="nick-name"> |
|||
{{ currentUser.nickName }} |
|||
<el-tag type="success" size="mini" class="role-tag">客服</el-tag> |
|||
</div> |
|||
<div class="account-name">@{{ currentUser.userName }}</div> |
|||
</div> |
|||
<i class="el-icon-check current-icon"></i> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 其他账号列表 --> |
|||
<div class="other-accounts" v-if="allAccounts.length > 0"> |
|||
<div class="divider"></div> |
|||
<div class="other-label"> |
|||
可切换账号 |
|||
<span class="account-count">{{ allAccounts.length }}</span> |
|||
</div> |
|||
<div |
|||
class="account-item" |
|||
v-for="account in allAccounts" |
|||
:key="account.id" |
|||
@click="handleSwitch(account)"> |
|||
<div class="chat-left"> |
|||
<img |
|||
:src="account.headImageThumb || account.headImage || defaultAvatar" |
|||
class="user-avatar" |
|||
@error="handleAvatarError"> |
|||
<!-- 未读消息角标 --> |
|||
<div |
|||
v-show="account.unreadCount > 0" |
|||
class="unread-text"> |
|||
{{ account.unreadCount > 99 ? '99+' : account.unreadCount }} |
|||
</div> |
|||
</div> |
|||
<div class="account-info"> |
|||
<div class="nick-name"> |
|||
{{ account.nickName }} |
|||
<el-tag type="success" size="mini" class="role-tag">客服</el-tag> |
|||
</div> |
|||
<div class="account-name">@{{ account.userName }}</div> |
|||
<div class="account-id">ID: {{ account.id }}</div> |
|||
</div> |
|||
<div class="account-actions"> |
|||
<el-button |
|||
type="primary" |
|||
size="small" |
|||
@click.stop="handleSwitch(account)"> |
|||
切换 |
|||
</el-button> |
|||
<i |
|||
class="el-icon-close delete-icon" |
|||
@click.stop="handleRemove(account)" |
|||
title="移除可切换账号"> |
|||
</i> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 空状态 --> |
|||
<div class="empty-state" v-else-if="!loading"> |
|||
<i class="el-icon-info"></i> |
|||
<p>暂无其他可切换账号</p> |
|||
</div> |
|||
|
|||
<!-- 加载中 --> |
|||
<div class="loading-state" v-if="loading"> |
|||
<i class="el-icon-loading"></i> |
|||
<p>加载中...</p> |
|||
</div> |
|||
|
|||
<div class="divider"></div> |
|||
|
|||
<!-- 底部操作 --> |
|||
<div class="menu-footer"> |
|||
<div class="menu-item add-account" @click="handleAddAccount"> |
|||
<i class="el-icon-plus"></i> |
|||
<span>添加账号</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 添加账号登录对话框 --> |
|||
<el-dialog |
|||
title="添加账号" |
|||
:visible.sync="addAccountDialogVisible" |
|||
width="400px" |
|||
custom-class="add-account-dialog" |
|||
:close-on-click-modal="false" |
|||
:modal-append-to-body="true" |
|||
:append-to-body="true" |
|||
@click.stop="" |
|||
> |
|||
<el-form :model="loginForm" :rules="loginRules" ref="loginForm" label-width="80px" @submit.native.prevent> |
|||
<el-form-item label="账号" prop="userName"> |
|||
<el-input |
|||
v-model="loginForm.userName" |
|||
placeholder="请输入账号" |
|||
prefix-icon="el-icon-user" |
|||
@keyup.enter.native="handleAddAccountLogin"> |
|||
</el-input> |
|||
</el-form-item> |
|||
|
|||
<el-form-item label="密码" prop="password"> |
|||
<el-input |
|||
v-model="loginForm.password" |
|||
type="password" |
|||
placeholder="请输入密码" |
|||
prefix-icon="el-icon-lock" |
|||
show-password |
|||
@keyup.enter.native="handleAddAccountLogin"> |
|||
</el-input> |
|||
</el-form-item> |
|||
</el-form> |
|||
<span slot="footer"> |
|||
<el-button @click="addAccountDialogVisible = false">取消</el-button> |
|||
<el-button type="primary" @click="handleAddAccountLogin" :loading="adding">添加</el-button> |
|||
</span> |
|||
</el-dialog> |
|||
</div> |
|||
</template> |
|||
|
|||
<script> |
|||
const ACCOUNTS_CACHE_KEY = 'switchable_accounts_cache'; // 本地缓存key |
|||
|
|||
export default { |
|||
name: 'AccountSwitchMenu', |
|||
|
|||
props: { |
|||
visible: { |
|||
type: Boolean, |
|||
default: false |
|||
}, |
|||
currentUser: { |
|||
type: Object, |
|||
required: true |
|||
}, |
|||
position: { |
|||
type: Object, |
|||
default: () => ({ x: 0, y: 0 }) |
|||
} |
|||
}, |
|||
|
|||
data() { |
|||
return { |
|||
allAccounts: [], |
|||
loading: false, |
|||
adding: false, |
|||
addAccountDialogVisible: false, |
|||
loginForm: { |
|||
userName: '', |
|||
password: '' |
|||
}, |
|||
loginRules: { |
|||
userName: [ |
|||
{ required: true, message: '请输入账号', trigger: 'blur' } |
|||
], |
|||
password: [ |
|||
{ required: true, message: '请输入密码', trigger: 'blur' } |
|||
] |
|||
}, |
|||
defaultAvatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png', |
|||
unreadTimer: null, |
|||
wsListenerAdded: false |
|||
}; |
|||
}, |
|||
|
|||
watch: { |
|||
visible: { |
|||
handler(val) { |
|||
if (val) { |
|||
this.loadAccountList(); |
|||
this.$nextTick(() => { |
|||
this.positionMenu(); |
|||
}); |
|||
document.addEventListener('click', this.handleClickOutside); |
|||
this.addMessageListener(); |
|||
this.startUnreadTimer(); |
|||
} else { |
|||
document.removeEventListener('click', this.handleClickOutside); |
|||
this.stopUnreadTimer(); |
|||
} |
|||
}, |
|||
immediate: true |
|||
} |
|||
}, |
|||
|
|||
mounted() { |
|||
this.addMessageListener(); |
|||
this.startUnreadTimer(); |
|||
}, |
|||
|
|||
beforeDestroy() { |
|||
document.removeEventListener('click', this.handleClickOutside); |
|||
this.stopUnreadTimer(); |
|||
this.removeMessageListener(); |
|||
}, |
|||
|
|||
methods: { |
|||
// 获取本地缓存的账号列表 |
|||
getCachedAccounts() { |
|||
try { |
|||
const cached = localStorage.getItem(ACCOUNTS_CACHE_KEY); |
|||
return cached ? JSON.parse(cached) : []; |
|||
} catch (e) { |
|||
console.error('读取缓存失败:', e); |
|||
return []; |
|||
} |
|||
}, |
|||
|
|||
// 保存账号列表到本地缓存 |
|||
saveCachedAccounts(accounts) { |
|||
try { |
|||
// 只保存必要字段,减少存储空间 |
|||
const cacheData = accounts.map(a => ({ |
|||
id: a.id, |
|||
userName: a.userName, |
|||
nickName: a.nickName, |
|||
headImage: a.headImage, |
|||
headImageThumb: a.headImageThumb |
|||
})); |
|||
localStorage.setItem(ACCOUNTS_CACHE_KEY, JSON.stringify(cacheData)); |
|||
} catch (e) { |
|||
console.error('保存缓存失败:', e); |
|||
} |
|||
}, |
|||
|
|||
// 添加单个账号到缓存 |
|||
addAccountToCache(account) { |
|||
const accounts = this.getCachedAccounts(); |
|||
// 检查是否已存在 |
|||
const exists = accounts.some(a => a.id === account.id); |
|||
if (!exists) { |
|||
accounts.push({ |
|||
id: account.id, |
|||
userName: account.userName, |
|||
nickName: account.nickName, |
|||
headImage: account.headImage, |
|||
headImageThumb: account.headImageThumb |
|||
}); |
|||
this.saveCachedAccounts(accounts); |
|||
} |
|||
}, |
|||
|
|||
// 从缓存移除账号 |
|||
removeAccountFromCache(accountId) { |
|||
let accounts = this.getCachedAccounts(); |
|||
accounts = accounts.filter(a => a.id !== accountId); |
|||
this.saveCachedAccounts(accounts); |
|||
}, |
|||
|
|||
async loadAccountList() { |
|||
this.loading = true; |
|||
try { |
|||
// 1. 先从缓存读取 |
|||
let accounts = this.getCachedAccounts(); |
|||
|
|||
// 2. 过滤掉当前账号 |
|||
accounts = accounts.filter(a => a.id !== this.currentUser.id); |
|||
|
|||
console.log('从缓存加载账号列表:', accounts); |
|||
|
|||
// 3. 尝试从后端获取最新列表(可选,用于同步) |
|||
try { |
|||
const res = await this.$http({ |
|||
url: '/user/getSwitchableAccounts', |
|||
method: 'post' |
|||
}); |
|||
|
|||
const data = res.data || res || {}; |
|||
const serverAccounts = data.switchableUsers || []; |
|||
|
|||
// 合并缓存和后端数据 |
|||
if (serverAccounts.length > 0) { |
|||
// 用后端数据更新缓存 |
|||
const mergedAccounts = this.mergeAccounts(accounts, serverAccounts); |
|||
this.saveCachedAccounts(mergedAccounts); |
|||
accounts = mergedAccounts.filter(a => a.id !== this.currentUser.id); |
|||
} |
|||
} catch (error) { |
|||
console.log('后端接口获取失败,使用缓存数据:', error); |
|||
} |
|||
|
|||
// 4. 获取未读消息数 |
|||
if (accounts.length > 0) { |
|||
accounts = await this.fetchUnreadCounts(accounts); |
|||
} |
|||
|
|||
this.allAccounts = accounts; |
|||
|
|||
} catch (error) { |
|||
console.error('加载账号列表失败:', error); |
|||
// 最终降级:只显示缓存数据 |
|||
const cached = this.getCachedAccounts(); |
|||
this.allAccounts = cached.filter(a => a.id !== this.currentUser.id); |
|||
} finally { |
|||
this.loading = false; |
|||
} |
|||
}, |
|||
|
|||
// 合并账号列表 |
|||
mergeAccounts(cached, server) { |
|||
const map = new Map(); |
|||
// 先添加缓存数据 |
|||
cached.forEach(a => map.set(a.id, a)); |
|||
// 用后端数据覆盖/补充 |
|||
server.forEach(a => map.set(a.id, a)); |
|||
return Array.from(map.values()); |
|||
}, |
|||
|
|||
// 刷新未读消息数 |
|||
async refreshUnreadCounts() { |
|||
if (this.allAccounts.length === 0) return; |
|||
|
|||
try { |
|||
const accounts = await this.fetchUnreadCounts(this.allAccounts); |
|||
this.allAccounts = accounts; |
|||
} catch (error) { |
|||
console.error('刷新未读消息数失败:', error); |
|||
} |
|||
}, |
|||
|
|||
// 获取未读消息数 |
|||
async fetchUnreadCounts(accounts) { |
|||
try { |
|||
const userIds = accounts.map(a => a.id); |
|||
|
|||
const res = await this.$http({ |
|||
url: '/message/private/unreadCounts', |
|||
method: 'post', |
|||
data: { |
|||
userIds: userIds |
|||
} |
|||
}); |
|||
|
|||
const unreadMap = res.data || res || {}; |
|||
|
|||
return accounts.map(account => ({ |
|||
...account, |
|||
unreadCount: unreadMap[account.id] || 0 |
|||
})); |
|||
|
|||
} catch (error) { |
|||
console.error('获取未读消息数失败:', error); |
|||
return accounts.map(account => ({ |
|||
...account, |
|||
unreadCount: 0 |
|||
})); |
|||
} |
|||
}, |
|||
|
|||
addMessageListener() { |
|||
if (this.wsListenerAdded) return; |
|||
this.$eventBus.$on('onPrivateMessage', this.handleNewMessage); |
|||
this.$eventBus.$on('onMessageReaded', this.handleMessageReaded); |
|||
this.wsListenerAdded = true; |
|||
}, |
|||
|
|||
removeMessageListener() { |
|||
this.$eventBus.$off('onPrivateMessage', this.handleNewMessage); |
|||
this.$eventBus.$off('onMessageReaded', this.handleMessageReaded); |
|||
this.wsListenerAdded = false; |
|||
}, |
|||
|
|||
handleNewMessage(msg) { |
|||
const account = this.allAccounts.find(a => a.id === msg.recvId); |
|||
if (account) { |
|||
account.unreadCount = (account.unreadCount || 0) + 1; |
|||
this.$forceUpdate(); |
|||
} |
|||
}, |
|||
|
|||
handleMessageReaded(data) { |
|||
this.refreshUnreadCounts(); |
|||
}, |
|||
|
|||
startUnreadTimer() { |
|||
this.stopUnreadTimer(); |
|||
this.unreadTimer = setInterval(() => { |
|||
if (this.visible && this.allAccounts.length > 0) { |
|||
this.refreshUnreadCounts(); |
|||
} |
|||
}, 30000); |
|||
}, |
|||
|
|||
stopUnreadTimer() { |
|||
if (this.unreadTimer) { |
|||
clearInterval(this.unreadTimer); |
|||
this.unreadTimer = null; |
|||
} |
|||
}, |
|||
|
|||
positionMenu() { |
|||
const menu = this.$refs.menu || this.$el; |
|||
if (!menu || typeof menu.getBoundingClientRect !== 'function') { |
|||
setTimeout(() => this.positionMenu(), 10); |
|||
return; |
|||
} |
|||
|
|||
try { |
|||
const rect = menu.getBoundingClientRect(); |
|||
const windowWidth = window.innerWidth; |
|||
const windowHeight = window.innerHeight; |
|||
|
|||
let left = this.position.x; |
|||
let top = this.position.y; |
|||
|
|||
if (left + rect.width > windowWidth - 10) { |
|||
left = windowWidth - rect.width - 10; |
|||
} |
|||
|
|||
if (top + rect.height > windowHeight - 10) { |
|||
top = windowHeight - rect.height - 10; |
|||
} |
|||
|
|||
left = Math.max(10, left); |
|||
top = Math.max(10, top); |
|||
|
|||
menu.style.left = left + 'px'; |
|||
menu.style.top = top + 'px'; |
|||
} catch (e) { |
|||
console.error('定位菜单失败', e); |
|||
} |
|||
}, |
|||
|
|||
handleClickOutside(event) { |
|||
if (!this.visible) return; |
|||
if (this.addAccountDialogVisible) return; |
|||
|
|||
const menu = this.$refs.menu || this.$el; |
|||
if (menu && !menu.contains(event.target)) { |
|||
const trigger = document.querySelector('.user-head-image'); |
|||
if (trigger && trigger.contains(event.target)) { |
|||
return; |
|||
} |
|||
this.$emit('close'); |
|||
} |
|||
}, |
|||
|
|||
handleAddAccount() { |
|||
this.loginForm = { |
|||
userName: '', |
|||
password: '' |
|||
}; |
|||
this.addAccountDialogVisible = true; |
|||
|
|||
this.$nextTick(() => { |
|||
if (this.$refs.loginForm) { |
|||
this.$refs.loginForm.clearValidate(); |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
async handleAddAccountLogin() { |
|||
this.$refs.loginForm.validate(async (valid) => { |
|||
if (!valid) return; |
|||
|
|||
this.adding = true; |
|||
try { |
|||
const res = await this.$http({ |
|||
url: '/user/addAccounts', |
|||
method: 'post', |
|||
data: { |
|||
userName: this.loginForm.userName, |
|||
password: this.loginForm.password, |
|||
terminal: this.getTerminalType() |
|||
} |
|||
}); |
|||
|
|||
// 获取添加的账号信息 |
|||
const accountData = res.data || res || {}; |
|||
|
|||
// 添加到缓存 |
|||
if (accountData.user) { |
|||
this.addAccountToCache(accountData.user); |
|||
} else if (accountData.id) { |
|||
this.addAccountToCache(accountData); |
|||
} |
|||
|
|||
this.$message.success('添加账号成功'); |
|||
this.addAccountDialogVisible = false; |
|||
|
|||
await this.loadAccountList(); |
|||
this.$emit('update'); |
|||
|
|||
} catch (error) { |
|||
console.error('添加失败:', error); |
|||
if (error.response && error.response.data) { |
|||
this.$message.error(error.response.data.message || '添加失败'); |
|||
} else { |
|||
this.$message.error('添加失败'); |
|||
} |
|||
} finally { |
|||
this.adding = false; |
|||
} |
|||
}); |
|||
}, |
|||
|
|||
handleSwitch(account) { |
|||
// 切换前,确保目标账号在缓存中 |
|||
this.addAccountToCache(account); |
|||
// 确保当前账号也在缓存中 |
|||
this.addAccountToCache(this.currentUser); |
|||
|
|||
this.$emit('switch', account); |
|||
}, |
|||
|
|||
getTerminalType() { |
|||
const userAgent = navigator.userAgent; |
|||
if (/mobile/i.test(userAgent)) { |
|||
return 2; |
|||
} |
|||
if (/tablet/i.test(userAgent)) { |
|||
return 3; |
|||
} |
|||
return 1; |
|||
}, |
|||
|
|||
async handleRemove(account) { |
|||
try { |
|||
await this.$confirm(`确定要移除账号【${account.nickName}】吗?`, '移除账号', { |
|||
confirmButtonText: '确定移除', |
|||
cancelButtonText: '取消', |
|||
type: 'warning' |
|||
}); |
|||
|
|||
// 从缓存移除 |
|||
this.removeAccountFromCache(account.id); |
|||
|
|||
// 尝试调用后端接口 |
|||
try { |
|||
await this.$http({ |
|||
url: '/user/removeSwitchableAccount', |
|||
method: 'post', |
|||
data: { |
|||
targetUserId: account.id |
|||
} |
|||
}); |
|||
} catch (error) { |
|||
console.log('后端移除失败,已从本地缓存移除'); |
|||
} |
|||
|
|||
this.$message.success('账号已移除'); |
|||
await this.loadAccountList(); |
|||
this.$emit('update'); |
|||
} catch (error) { |
|||
// 用户取消 |
|||
} |
|||
}, |
|||
|
|||
handleAvatarError(e) { |
|||
e.target.src = this.defaultAvatar; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
|
|||
<style lang="scss"> |
|||
|
|||
.add-account-dialog { |
|||
border-radius: 10px !important; |
|||
} |
|||
.account-switch-menu { |
|||
position: fixed; |
|||
z-index: 9999; |
|||
background: #fff; |
|||
border-radius: 12px; |
|||
box-shadow: 0 6px 30px rgba(0, 0, 0, 0.2); |
|||
width: 380px; |
|||
max-width: 90vw; |
|||
padding: 12px 0; |
|||
user-select: none; |
|||
|
|||
.menu-header { |
|||
padding: 8px 16px; |
|||
|
|||
.current-label { |
|||
font-size: 12px; |
|||
color: #999; |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
.current-account { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 10px 12px; |
|||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
|||
border-radius: 12px; |
|||
|
|||
.user-avatar { |
|||
width: 44px; |
|||
height: 44px; |
|||
border-radius: 50%; |
|||
object-fit: cover; |
|||
border: 2px solid rgba(255, 255, 255, 0.3); |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.account-info { |
|||
flex: 1; |
|||
margin-left: 14px; |
|||
|
|||
.nick-name { |
|||
font-size: 16px; |
|||
font-weight: 500; |
|||
color: #fff; |
|||
display: flex; |
|||
align-items: center; |
|||
|
|||
.role-tag { |
|||
margin-left: 8px; |
|||
background: rgba(255, 255, 255, 0.2); |
|||
border: none; |
|||
color: #fff; |
|||
} |
|||
} |
|||
|
|||
.account-name { |
|||
font-size: 13px; |
|||
color: rgba(255, 255, 255, 0.85); |
|||
margin-top: 3px; |
|||
} |
|||
} |
|||
|
|||
.current-icon { |
|||
color: #fff; |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
} |
|||
} |
|||
} |
|||
|
|||
.divider { |
|||
height: 1px; |
|||
background: #f0f0f0; |
|||
margin: 12px 0; |
|||
} |
|||
|
|||
.other-accounts { |
|||
padding: 0 4px; |
|||
max-height: 320px; |
|||
overflow-y: auto; |
|||
|
|||
&::-webkit-scrollbar { |
|||
width: 4px; |
|||
} |
|||
|
|||
&::-webkit-scrollbar-thumb { |
|||
background: #ddd; |
|||
border-radius: 2px; |
|||
} |
|||
|
|||
.other-label { |
|||
font-size: 12px; |
|||
color: #999; |
|||
margin-bottom: 10px; |
|||
padding-left: 12px; |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: space-between; |
|||
|
|||
.account-count { |
|||
background: #f0f0f0; |
|||
padding: 2px 8px; |
|||
border-radius: 10px; |
|||
font-size: 11px; |
|||
} |
|||
} |
|||
|
|||
.account-item { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 10px 16px; |
|||
margin: 2px 0; |
|||
border-radius: 10px; |
|||
cursor: pointer; |
|||
transition: all 0.2s; |
|||
|
|||
&:hover { |
|||
background: #f5f7fa; |
|||
|
|||
.delete-icon { |
|||
opacity: 1; |
|||
} |
|||
} |
|||
|
|||
.chat-left { |
|||
position: relative; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
flex-shrink: 0; |
|||
|
|||
.user-avatar { |
|||
width: 40px; |
|||
height: 40px; |
|||
border-radius: 50%; |
|||
object-fit: cover; |
|||
} |
|||
|
|||
.unread-text { |
|||
position: absolute; |
|||
background-color: #f56c6c; |
|||
right: -4px; |
|||
top: -8px; |
|||
color: white; |
|||
border-radius: 30px; |
|||
padding: 1px 5px; |
|||
font-size: 10px; |
|||
text-align: center; |
|||
white-space: nowrap; |
|||
border: 1px solid #f1e5e5; |
|||
min-width: 16px; |
|||
height: 18px; |
|||
line-height: 16px; |
|||
} |
|||
} |
|||
|
|||
.account-info { |
|||
flex: 1; |
|||
margin-left: 14px; |
|||
min-width: 0; |
|||
|
|||
.nick-name { |
|||
font-size: 14px; |
|||
font-weight: 500; |
|||
color: #333; |
|||
display: flex; |
|||
align-items: center; |
|||
|
|||
.role-tag { |
|||
margin-left: 6px; |
|||
transform: scale(0.85); |
|||
} |
|||
} |
|||
|
|||
.account-name { |
|||
font-size: 12px; |
|||
color: #999; |
|||
margin-top: 2px; |
|||
} |
|||
|
|||
.account-id { |
|||
font-size: 11px; |
|||
color: #bbb; |
|||
margin-top: 2px; |
|||
} |
|||
} |
|||
|
|||
.account-actions { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8px; |
|||
flex-shrink: 0; |
|||
|
|||
.delete-icon { |
|||
opacity: 0; |
|||
color: #999; |
|||
font-size: 16px; |
|||
padding: 6px; |
|||
cursor: pointer; |
|||
transition: all 0.2s; |
|||
border-radius: 50%; |
|||
|
|||
&:hover { |
|||
color: #f56c6c; |
|||
background: rgba(245, 108, 108, 0.1); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
.empty-state, |
|||
.loading-state { |
|||
text-align: center; |
|||
padding: 30px 20px; |
|||
color: #999; |
|||
|
|||
i { |
|||
font-size: 36px; |
|||
margin-bottom: 8px; |
|||
} |
|||
|
|||
p { |
|||
margin: 0; |
|||
font-size: 13px; |
|||
} |
|||
} |
|||
|
|||
.menu-footer { |
|||
padding: 4px 8px 0; |
|||
border-radius: 10px; |
|||
.menu-item { |
|||
display: flex; |
|||
align-items: center; |
|||
padding: 12px 16px; |
|||
border-radius: 10px; |
|||
cursor: pointer; |
|||
transition: background 0.2s; |
|||
|
|||
i { |
|||
font-size: 18px; |
|||
margin-right: 14px; |
|||
width: 20px; |
|||
text-align: center; |
|||
} |
|||
|
|||
span { |
|||
font-size: 14px; |
|||
} |
|||
|
|||
&:hover { |
|||
background: #f5f7fa; |
|||
} |
|||
|
|||
&.add-account { |
|||
color: #667eea; |
|||
|
|||
&:hover { |
|||
background: rgba(102, 126, 234, 0.08); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
</style> |
|||
@ -0,0 +1,94 @@ |
|||
// utils/accountManager.js
|
|||
const ACCOUNT_LIST_KEY = 'IM_ACCOUNT_LIST'; |
|||
const MAX_ACCOUNT_COUNT = 5; |
|||
|
|||
class AccountManager { |
|||
// 获取所有保存的账号
|
|||
static getAccountList() { |
|||
try { |
|||
const list = localStorage.getItem(ACCOUNT_LIST_KEY); |
|||
return list ? JSON.parse(list) : []; |
|||
} catch (e) { |
|||
console.error('获取账号列表失败', e); |
|||
return []; |
|||
} |
|||
} |
|||
|
|||
// 保存账号列表
|
|||
static saveAccountList(accounts) { |
|||
try { |
|||
localStorage.setItem(ACCOUNT_LIST_KEY, JSON.stringify(accounts)); |
|||
} catch (e) { |
|||
console.error('保存账号列表失败', e); |
|||
} |
|||
} |
|||
|
|||
// 保存或更新账号信息
|
|||
static saveAccount(accountInfo) { |
|||
if (!accountInfo || !accountInfo.account) return; |
|||
|
|||
let accounts = this.getAccountList(); |
|||
const existingIndex = accounts.findIndex(item => item.account === accountInfo.account); |
|||
|
|||
const newAccount = { |
|||
account: accountInfo.account, |
|||
nickName: accountInfo.nickName || accountInfo.account, |
|||
headImage: accountInfo.headImage || '', |
|||
headImageThumb: accountInfo.headImageThumb || '', |
|||
lastLoginTime: new Date().getTime(), |
|||
userId: accountInfo.userId || accountInfo.id |
|||
}; |
|||
|
|||
if (existingIndex >= 0) { |
|||
accounts[existingIndex] = newAccount; |
|||
} else { |
|||
accounts.unshift(newAccount); |
|||
} |
|||
|
|||
if (accounts.length > MAX_ACCOUNT_COUNT) { |
|||
accounts = accounts.slice(0, MAX_ACCOUNT_COUNT); |
|||
} |
|||
|
|||
this.saveAccountList(accounts); |
|||
} |
|||
|
|||
// 删除账号
|
|||
static removeAccount(account) { |
|||
let accounts = this.getAccountList(); |
|||
accounts = accounts.filter(item => item.account !== account); |
|||
this.saveAccountList(accounts); |
|||
} |
|||
|
|||
// 获取其他账号
|
|||
static getOtherAccounts(currentAccount) { |
|||
const accounts = this.getAccountList(); |
|||
return accounts.filter(item => item.account !== currentAccount); |
|||
} |
|||
|
|||
// 设置要切换的账号
|
|||
static setSwitchAccount(account) { |
|||
sessionStorage.setItem('switch_account', account); |
|||
} |
|||
|
|||
// 清除切换账号标记
|
|||
static clearSwitchAccount() { |
|||
sessionStorage.removeItem('switch_account'); |
|||
} |
|||
|
|||
// 格式化时间
|
|||
static formatTime(timestamp) { |
|||
if (!timestamp) return ''; |
|||
const date = new Date(timestamp); |
|||
const now = new Date(); |
|||
const diff = now - date; |
|||
|
|||
if (diff < 60000) return '刚刚'; |
|||
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'; |
|||
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'; |
|||
if (diff < 604800000) return Math.floor(diff / 86400000) + '天前'; |
|||
|
|||
return `${date.getMonth() + 1}月${date.getDate()}日`; |
|||
} |
|||
} |
|||
|
|||
export default AccountManager; |
|||
Loading…
Reference in new issue