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