feat: user-bar 弹出修改
This commit is contained in:
@@ -45,65 +45,78 @@
|
|||||||
|
|
||||||
<div class="user-panel__divider"></div>
|
<div class="user-panel__divider"></div>
|
||||||
|
|
||||||
<!-- 菜单区:编辑个人资料 + 在线状态(hover 触发展开) -->
|
<!-- 设置区:编辑个人资料 + 在线状态 -->
|
||||||
<el-menu mode="vertical" class="user-panel__menu" menu-trigger="hover">
|
<div class="user-panel__section">
|
||||||
<el-menu-item index="edit" @click="handleEditProfile">
|
<div class="user-panel__item" @click="handleEditProfile">
|
||||||
<el-icon :size="18"><EditPen /></el-icon>
|
<el-icon :size="18"><EditPen /></el-icon>
|
||||||
<span>编辑个人资料</span>
|
<span>编辑个人资料</span>
|
||||||
</el-menu-item>
|
</div>
|
||||||
|
|
||||||
<el-sub-menu index="status" popper-class="status-submenu-popper" :popper-offset="12">
|
<!-- 在线状态(hover 右侧弹出子菜单) -->
|
||||||
<template #title>
|
<el-popover
|
||||||
<el-icon :size="18">
|
trigger="hover"
|
||||||
<span
|
placement="right-start"
|
||||||
class="user-panel__status-dot"
|
:show-arrow="false"
|
||||||
:style="{ backgroundColor: currentStatus.color }"
|
:offset="16"
|
||||||
></span>
|
:show-after="100"
|
||||||
</el-icon>
|
:hide-after="200"
|
||||||
<span>{{ currentStatus.label }}</span>
|
:teleported="false"
|
||||||
|
popper-class="status-submenu-popper"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<div class="user-panel__item">
|
||||||
|
<el-icon :size="18">
|
||||||
|
<span
|
||||||
|
class="user-panel__status-dot"
|
||||||
|
:style="{ backgroundColor: currentStatus.color }"
|
||||||
|
></span>
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ currentStatus.label }}</span>
|
||||||
|
<el-icon :size="14" class="user-panel__item-arrow"><ArrowRight /></el-icon>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-menu-item
|
<div class="status-submenu">
|
||||||
v-for="status in statusOptions"
|
<div class="status-submenu__header">在线状态</div>
|
||||||
:key="status.key"
|
<div
|
||||||
:index="'status-' + status.key"
|
v-for="status in statusOptions"
|
||||||
@click="selectStatus(status)"
|
:key="status.key"
|
||||||
>
|
class="status-submenu__item"
|
||||||
<el-icon :size="18">
|
:class="{ 'is-active': currentStatus.key === status.key }"
|
||||||
<span
|
@click="selectStatus(status)"
|
||||||
class="user-panel__status-dot"
|
|
||||||
:style="{ backgroundColor: status.color }"
|
|
||||||
></span>
|
|
||||||
</el-icon>
|
|
||||||
<span>{{ status.label }}</span>
|
|
||||||
<el-icon
|
|
||||||
v-if="currentStatus.key === status.key"
|
|
||||||
:size="14"
|
|
||||||
class="user-panel__check"
|
|
||||||
>
|
>
|
||||||
<Check />
|
<el-icon :size="18">
|
||||||
</el-icon>
|
<span
|
||||||
</el-menu-item>
|
class="user-panel__status-dot"
|
||||||
</el-sub-menu>
|
:style="{ backgroundColor: status.color }"
|
||||||
</el-menu>
|
></span>
|
||||||
|
</el-icon>
|
||||||
|
<span>{{ status.label }}</span>
|
||||||
|
<el-icon
|
||||||
|
v-if="currentStatus.key === status.key"
|
||||||
|
:size="14"
|
||||||
|
class="user-panel__check"
|
||||||
|
>
|
||||||
|
<Check />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="user-panel__divider"></div>
|
<div class="user-panel__divider"></div>
|
||||||
|
|
||||||
<!-- 菜单区:复制账号ID + 退出登录 -->
|
<!-- 操作区:复制账号ID + 退出登录 -->
|
||||||
<el-menu mode="vertical" class="user-panel__menu">
|
<div class="user-panel__section">
|
||||||
<el-menu-item index="copy-id" @click="handleCopyUserId">
|
<div class="user-panel__item" @click="handleCopyUserId">
|
||||||
<el-icon :size="18"><DocumentCopy /></el-icon>
|
<el-icon :size="18"><DocumentCopy /></el-icon>
|
||||||
<span>复制账号ID</span>
|
<span>复制账号ID</span>
|
||||||
</el-menu-item>
|
</div>
|
||||||
<el-menu-item
|
<div class="user-panel__item user-panel__item--danger" @click="handleLogout">
|
||||||
index="logout"
|
|
||||||
class="user-panel__menu-item--danger"
|
|
||||||
@click="handleLogout"
|
|
||||||
>
|
|
||||||
<el-icon :size="18"><SwitchButton /></el-icon>
|
<el-icon :size="18"><SwitchButton /></el-icon>
|
||||||
<span>退出登录</span>
|
<span>退出登录</span>
|
||||||
</el-menu-item>
|
</div>
|
||||||
</el-menu>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
|
|
||||||
@@ -118,14 +131,13 @@
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Setting, EditPen, Check, DocumentCopy, SwitchButton } from '@element-plus/icons-vue'
|
import { Setting, EditPen, ArrowRight, Check, DocumentCopy, SwitchButton } from '@element-plus/icons-vue'
|
||||||
import { useAccountStore } from '@/stores'
|
import { useAccountStore } from '@/stores'
|
||||||
import { UserStatus, type UserStatusType } from '@/api/modules/user'
|
import { UserStatus, type UserStatusType } from '@/api/modules/user'
|
||||||
|
|
||||||
/** 在线状态选项 */
|
/** 在线状态选项 */
|
||||||
interface StatusOption {
|
interface StatusOption {
|
||||||
key: string
|
key: string
|
||||||
/** 对应后端 UserStatus 值 */
|
|
||||||
value: UserStatusType
|
value: UserStatusType
|
||||||
label: string
|
label: string
|
||||||
color: string
|
color: string
|
||||||
@@ -138,45 +150,26 @@ const STATUS_OPTIONS: StatusOption[] = [
|
|||||||
{ key: 'invisible', value: UserStatus.INVISIBLE, label: '隐身', color: 'var(--text-tertiary)' }
|
{ key: 'invisible', value: UserStatus.INVISIBLE, label: '隐身', color: 'var(--text-tertiary)' }
|
||||||
]
|
]
|
||||||
|
|
||||||
/** 后端状态值 → StatusOption 映射 */
|
|
||||||
const statusMap = new Map<UserStatusType, StatusOption>(STATUS_OPTIONS.map((s) => [s.value, s]))
|
const statusMap = new Map<UserStatusType, StatusOption>(STATUS_OPTIONS.map((s) => [s.value, s]))
|
||||||
|
|
||||||
const accountStore = useAccountStore()
|
const accountStore = useAccountStore()
|
||||||
const { accountInfo } = storeToRefs(accountStore)
|
const { accountInfo } = storeToRefs(accountStore)
|
||||||
|
|
||||||
/** 在线状态选项列表 */
|
|
||||||
const statusOptions = STATUS_OPTIONS
|
const statusOptions = STATUS_OPTIONS
|
||||||
|
|
||||||
/** 当前在线状态(从 store 读取,默认在线) */
|
|
||||||
const currentStatus = computed<StatusOption>(() => {
|
const currentStatus = computed<StatusOption>(() => {
|
||||||
const backendStatus = accountInfo.value?.status
|
const backendStatus = accountInfo.value?.status
|
||||||
return backendStatus ? (statusMap.get(backendStatus) ?? STATUS_OPTIONS[0]!) : STATUS_OPTIONS[0]!
|
return backendStatus ? (statusMap.get(backendStatus) ?? STATUS_OPTIONS[0]!) : STATUS_OPTIONS[0]!
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 用户头像 */
|
const userAvatar = computed(() => accountInfo.value?.avatar || '')
|
||||||
const userAvatar = computed(() => {
|
const displayName = computed(() => accountInfo.value?.nickName || '未登录')
|
||||||
return accountInfo.value?.avatar || ''
|
const signatureText = computed(() => accountInfo.value?.signature || '这个人很懒,什么都没写...')
|
||||||
})
|
|
||||||
|
|
||||||
/** 显示昵称 */
|
|
||||||
const displayName = computed(() => {
|
|
||||||
return accountInfo.value?.nickName || '未登录'
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 个性签名 */
|
|
||||||
const signatureText = computed(() => {
|
|
||||||
return accountInfo.value?.signature || '这个人很懒,什么都没写...'
|
|
||||||
})
|
|
||||||
|
|
||||||
/** 编辑个人资料(预留入口) */
|
|
||||||
const handleEditProfile = () => {
|
const handleEditProfile = () => {
|
||||||
ElMessage.info('编辑个人资料功能开发中')
|
ElMessage.info('编辑个人资料功能开发中')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 选择一个在线状态,调用后端接口持久化
|
|
||||||
* @param option - 选中的状态选项
|
|
||||||
*/
|
|
||||||
const selectStatus = async (option: StatusOption) => {
|
const selectStatus = async (option: StatusOption) => {
|
||||||
try {
|
try {
|
||||||
await accountStore.updateUserStatus(option.value)
|
await accountStore.updateUserStatus(option.value)
|
||||||
@@ -185,25 +178,27 @@ const selectStatus = async (option: StatusOption) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const handleCopyUserId = () => {
|
||||||
* 复制账号ID到剪贴板
|
|
||||||
*/
|
|
||||||
const handleCopyUserId = async () => {
|
|
||||||
const userId = accountInfo.value?.userId
|
const userId = accountInfo.value?.userId
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
ElMessage.warning('账号ID获取失败,请刷新后重试')
|
ElMessage.warning('账号ID获取失败,请刷新后重试')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(userId)
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.value = userId
|
||||||
|
textarea.style.position = 'fixed'
|
||||||
|
textarea.style.opacity = '0'
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(textarea)
|
||||||
ElMessage.success('账号ID已复制到剪贴板')
|
ElMessage.success('账号ID已复制到剪贴板')
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('复制失败,请手动复制')
|
ElMessage.error('复制失败,请手动复制')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 退出登录 */
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
accountStore.logout()
|
accountStore.logout()
|
||||||
ElMessage.success('已退出登录')
|
ElMessage.success('已退出登录')
|
||||||
@@ -318,7 +313,6 @@ const handleLogout = () => {
|
|||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 状态圆点(复用,放在 el-icon 内) ---- */
|
|
||||||
.user-panel__status-dot {
|
.user-panel__status-dot {
|
||||||
display: block;
|
display: block;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@@ -326,7 +320,6 @@ const handleLogout = () => {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 顶部头像区 ---- */
|
|
||||||
.user-panel__header {
|
.user-panel__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -363,7 +356,6 @@ const handleLogout = () => {
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 签名区 ---- */
|
|
||||||
.user-panel__signature {
|
.user-panel__signature {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -372,96 +364,57 @@ const handleLogout = () => {
|
|||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 分隔线 ---- */
|
|
||||||
.user-panel__divider {
|
.user-panel__divider {
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background-color: var(--border-light);
|
background-color: var(--border-light);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- 选中勾 ---- */
|
|
||||||
.user-panel__check {
|
.user-panel__check {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ========== el-menu 样式覆盖 ========== */
|
/* ========== 菜单项(纯 div,无 el-menu) ========== */
|
||||||
.user-panel__menu {
|
.user-panel__section {
|
||||||
border-right: none !important;
|
display: flex;
|
||||||
background: transparent !important;
|
flex-direction: column;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* 普通菜单项 */
|
.user-panel__item {
|
||||||
:deep(.el-menu-item) {
|
display: flex;
|
||||||
display: flex;
|
align-items: center;
|
||||||
align-items: center;
|
gap: 10px;
|
||||||
gap: 10px;
|
padding: 10px 8px;
|
||||||
height: auto;
|
border-radius: var(--radius-sm);
|
||||||
min-height: 0;
|
cursor: pointer;
|
||||||
padding: 10px 8px;
|
font-size: 14px;
|
||||||
border-radius: var(--radius-sm);
|
color: var(--text-primary);
|
||||||
font-size: 14px;
|
transition: background-color 0.12s ease;
|
||||||
line-height: 1.5;
|
|
||||||
color: var(--text-primary);
|
|
||||||
background: transparent;
|
|
||||||
transition: background-color 0.12s ease;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--bg-surface-hover) !important;
|
|
||||||
color: var(--text-primary) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除默认选中高亮(底部 border + 品牌色文字) */
|
|
||||||
&.is-active {
|
|
||||||
color: var(--text-primary) !important;
|
|
||||||
background: transparent !important;
|
|
||||||
border-bottom: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 子菜单标题 */
|
|
||||||
:deep(.el-sub-menu__title) {
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
height: auto;
|
|
||||||
min-height: 0;
|
|
||||||
padding: 10px 8px;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: var(--text-primary);
|
|
||||||
background: transparent;
|
|
||||||
transition: background-color 0.12s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--bg-surface-hover) !important;
|
|
||||||
color: var(--text-primary) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 子菜单展开态 */
|
|
||||||
:deep(.el-sub-menu.is-opened > .el-sub-menu__title) {
|
|
||||||
background-color: var(--bg-surface-hover);
|
background-color: var(--bg-surface-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 退出登录危险项 */
|
.user-panel__item--danger {
|
||||||
.user-panel__menu-item--danger {
|
color: var(--color-danger);
|
||||||
color: var(--color-danger) !important;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--color-danger-light) !important;
|
background-color: var(--color-danger-light);
|
||||||
color: var(--color-danger) !important;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-icon) {
|
.user-panel__item-arrow {
|
||||||
color: var(--color-danger);
|
margin-left: auto;
|
||||||
}
|
color: var(--text-tertiary);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- 非 scoped 样式:popover 和子菜单 popper -->
|
<!-- 非 scoped 样式 -->
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
/* 主弹出面板 */
|
/* 主弹出面板 */
|
||||||
.user-popover {
|
.user-popover {
|
||||||
@@ -478,34 +431,37 @@ const handleLogout = () => {
|
|||||||
box-shadow: var(--shadow-lg) !important;
|
box-shadow: var(--shadow-lg) !important;
|
||||||
border: 1px solid var(--border-default) !important;
|
border: 1px solid var(--border-default) !important;
|
||||||
padding: 4px 8px !important;
|
padding: 4px 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.el-menu--popup {
|
/* 状态子菜单内部 */
|
||||||
border-right: none !important;
|
.status-submenu {
|
||||||
}
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
.el-menu-item {
|
.status-submenu__header {
|
||||||
display: flex;
|
padding: 8px 8px 6px;
|
||||||
align-items: center;
|
font-size: 11px;
|
||||||
gap: 10px;
|
font-weight: 700;
|
||||||
height: auto;
|
text-transform: uppercase;
|
||||||
min-height: 0;
|
letter-spacing: 0.5px;
|
||||||
padding: 10px 8px;
|
color: var(--text-tertiary);
|
||||||
border-radius: var(--radius-sm);
|
}
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: var(--text-primary);
|
|
||||||
background: transparent;
|
|
||||||
transition: background-color 0.12s ease;
|
|
||||||
|
|
||||||
&:hover {
|
.status-submenu__item {
|
||||||
background-color: var(--bg-surface-hover) !important;
|
display: flex;
|
||||||
color: var(--text-primary) !important;
|
align-items: center;
|
||||||
}
|
gap: 10px;
|
||||||
|
padding: 10px 8px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: background-color 0.12s ease;
|
||||||
|
|
||||||
&.is-active {
|
&:hover {
|
||||||
color: var(--text-primary) !important;
|
background-color: var(--bg-surface-hover);
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user