Files
DJKB/my-vue-app/src/components/UserDropdown.vue
lbw_9527443 f93236ab36 feat: 初始化Vue3项目并添加核心功能模块
新增项目基础结构,包括Vue3、Pinia、Element Plus等核心依赖
添加路由配置和用户认证状态管理
实现销售数据看板、客户画像、团队管理等核心功能模块
集成图表库和API请求工具,完成基础样式配置
2025-08-12 14:34:44 +08:00

785 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="header-ringht user-dropdown" style="display: flex; align-items: center; gap: 10px; position: relative; cursor: pointer;" @click="toggleDropdown">
<img
src="https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png"
alt="用户头像"
class="avatar"
style="width: 35px; height: 35px;"
/>
<span style="color: black;">你好{{ userStore.userInfo?.username || '管理员' }}</span>
<svg width="12" height="12" viewBox="0 0 12 12" style="margin-left: 5px; transition: transform 0.3s;" :style="{ transform: showDropdown ? 'rotate(180deg)' : 'rotate(0deg)' }">
<path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<!-- 下拉菜单 -->
<div v-if="showDropdown" class="dropdown-menu" @click.stop>
<div class="dropdown-item" @click="handleSetSecurity">
<svg width="16" height="16" viewBox="0 0 16 16" style="margin-right: 8px;">
<path d="M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zM6 7V3a2 2 0 1 1 4 0v4h.5A1.5 1.5 0 0 1 12 8.5v5a1.5 1.5 0 0 1-1.5 1.5h-5A1.5 1.5 0 0 1 4 13.5v-5A1.5 1.5 0 0 1 5.5 7H6z" fill="currentColor"/>
</svg>
设置密保
</div>
<div class="dropdown-item" @click="handleChangePassword">
<svg width="16" height="16" viewBox="0 0 16 16" style="margin-right: 8px;">
<path d="M6.5 1A1.5 1.5 0 0 0 5 2.5V3H1.5A1.5 1.5 0 0 0 0 4.5v8A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-8A1.5 1.5 0 0 0 14.5 3H11v-.5A1.5 1.5 0 0 0 9.5 1h-3zM11 3V2.5a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5V3h4z" fill="currentColor"/>
</svg>
修改密码
</div>
<div class="dropdown-item logout-item" @click="handleLogout">
<svg width="16" height="16" viewBox="0 0 16 16" style="margin-right: 8px;">
<path d="M6 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H6zM5 3a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V3z" fill="currentColor"/>
<path d="M11.5 8.5a.5.5 0 0 0 0-1H9a.5.5 0 0 0 0 1h2.5z" fill="currentColor"/>
<path d="M10.146 7.146a.5.5 0 0 1 .708.708l-1.5 1.5a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 0 1 .708-.708L8.5 8.293l.646-.647z" fill="currentColor"/>
</svg>
退出登录
</div>
</div>
</div>
<!-- 密保设置弹窗 -->
<div v-if="showSecurityModal" class="security-modal-overlay">
<div class="security-modal">
<div class="security-modal-header">
<h2>设置密保问题</h2>
<p>请设置密保问题用于账户安全验证</p>
</div>
<div class="security-modal-body">
<div class="form-group">
<label for="security-question">密保问题</label>
<input
id="security-question"
v-model="securityForm.question"
type="text"
placeholder="请输入密保问题,例如:你的城市是?"
required
:disabled="securityLoading"
/>
</div>
<div class="form-group">
<label for="security-answer">密保答案</label>
<input
id="security-answer"
v-model="securityForm.answer"
type="text"
placeholder="请输入密保答案"
required
:disabled="securityLoading"
/>
</div>
</div>
<div class="security-modal-footer">
<button
type="button"
class="btn-cancel"
@click="cancelSecuritySetup"
:disabled="securityLoading"
>
取消
</button>
<button
type="button"
class="btn-confirm"
@click="handleSecuritySubmit"
:disabled="securityLoading"
>
<span v-if="securityLoading" class="loading-spinner"></span>
{{ securityLoading ? '设置中...' : '确认设置' }}
</button>
</div>
</div>
</div>
<!-- 修改密码弹窗 -->
<div v-if="showPasswordModal" class="password-modal-overlay">
<div class="password-modal">
<div class="password-modal-header">
<h2>修改密码</h2>
<p>请输入旧密码和新密码</p>
</div>
<div class="password-modal-body">
<div class="form-group">
<label for="old-password">旧密码</label>
<input
id="old-password"
v-model="passwordForm.oldPassword"
type="password"
placeholder="请输入旧密码"
required
:disabled="passwordLoading"
/>
</div>
<div class="form-group">
<label for="new-password">新密码</label>
<input
id="new-password"
v-model="passwordForm.newPassword"
type="password"
placeholder="请输入新密码"
required
:disabled="passwordLoading"
/>
</div>
</div>
<div class="password-modal-footer">
<button
type="button"
class="btn-cancel"
@click="cancelPasswordChange"
:disabled="passwordLoading"
>
取消
</button>
<button
type="button"
class="btn-confirm"
@click="handlePasswordSubmit"
:disabled="passwordLoading"
>
<span v-if="passwordLoading" class="loading-spinner"></span>
{{ passwordLoading ? '修改中...' : '确认修改' }}
</button>
</div>
</div>
</div>
<!-- 退出登录确认弹窗 -->
<div v-if="showLogoutModal" class="logout-modal-overlay" @click="cancelLogout">
<div class="logout-modal" @click.stop>
<div class="modal-header">
<h3>退出确认</h3>
</div>
<div class="modal-body">
<p>确定要退出登录吗</p>
</div>
<div class="modal-footer">
<button class="btn btn-cancel" @click="cancelLogout">取消</button>
<button class="btn btn-confirm" @click="confirmLogout">确认</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import http from '@/utils/https'
// 路由实例
const router = useRouter()
// 用户store
const userStore = useUserStore()
// STATE
const showDropdown = ref(false) // 下拉菜单显示状态
const showLogoutModal = ref(false) // 退出登录弹窗显示状态
const showSecurityModal = ref(false) // 密保设置弹窗显示状态
const securityForm = ref({
question: '',
answer: ''
}) // 密保表单数据
const securityLoading = ref(false) // 密保设置加载状态
const showPasswordModal = ref(false) // 修改密码弹窗显示状态
const passwordForm = ref({
oldPassword: '',
newPassword: ''
}) // 修改密码表单数据
const passwordLoading = ref(false) // 修改密码加载状态
// 切换下拉菜单显示状态
const toggleDropdown = () => {
showDropdown.value = !showDropdown.value
}
// 点击外部关闭下拉菜单
const handleClickOutside = (event) => {
const dropdown = event.target.closest('.user-dropdown')
if (!dropdown) {
showDropdown.value = false
}
}
// 监听点击事件
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
// 设置密保
const handleSetSecurity = () => {
console.log('设置密保')
showDropdown.value = false
showSecurityModal.value = true
}
// 密保设置处理函数
const handleSecuritySubmit = async () => {
if (!securityForm.value.question || !securityForm.value.answer) {
alert('请填写完整的密保问题和答案')
return
}
securityLoading.value = true
try {
const response = await http.post('/api/v1/set_security_question', {
question: securityForm.value.question,
answer: securityForm.value.answer
})
if (response.code === 200 || response.success) {
alert('密保设置成功')
showSecurityModal.value = false
// 清空表单
securityForm.value.question = ''
securityForm.value.answer = ''
} else {
alert(response.message || '密保设置失败')
}
} catch (error) {
console.error('密保设置失败:', error)
alert('密保设置失败,请重试')
} finally {
securityLoading.value = false
}
}
// 取消密保设置
const cancelSecuritySetup = () => {
showSecurityModal.value = false
// 清空表单
securityForm.value.question = ''
securityForm.value.answer = ''
}
// 修改密码
const handleChangePassword = () => {
console.log('修改密码')
showDropdown.value = false
showPasswordModal.value = true
}
// 修改密码处理函数
const handlePasswordSubmit = async () => {
if (!passwordForm.value.oldPassword || !passwordForm.value.newPassword) {
alert('请填写完整的旧密码和新密码')
return
}
passwordLoading.value = true
try {
const response = await http.post('/api/v1/change_password', {
old_password: passwordForm.value.oldPassword,
new_password: passwordForm.value.newPassword
})
if (response.code === 200 || response.success) {
alert('密码修改成功')
showPasswordModal.value = false
// 清空表单
passwordForm.value.oldPassword = ''
passwordForm.value.newPassword = ''
} else {
alert(response.message || '密码修改失败')
}
} catch (error) {
console.error('密码修改失败:', error)
alert('密码修改失败,请重试')
} finally {
passwordLoading.value = false
}
}
// 取消修改密码
const cancelPasswordChange = () => {
showPasswordModal.value = false
// 清空表单
passwordForm.value.oldPassword = ''
passwordForm.value.newPassword = ''
}
// 退出登录
const handleLogout = () => {
showDropdown.value = false
showLogoutModal.value = true
}
// 确认退出登录
const confirmLogout = () => {
console.log('用户确认退出登录')
// 清除用户信息(如果有的话)
// localStorage.removeItem('token')
// sessionStorage.clear()
// 关闭弹窗
showLogoutModal.value = false
// 跳转到登录页面
router.push('/login')
}
// 取消退出登录
const cancelLogout = () => {
console.log('用户取消退出登录')
showLogoutModal.value = false
}
</script>
<style scoped>
/* 用户下拉菜单样式 */
.user-dropdown {
position: relative;
.avatar {
border-radius: 50%;
object-fit: cover;
border: 2px solid #e2e8f0;
transition: border-color 0.3s;
&:hover {
border-color: #667eea;
}
}
}
.dropdown-menu {
position: absolute;
top: 100%;
right: 0;
background: white;
border-radius: 8px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
min-width: 160px;
z-index: 1000;
border: 1px solid #e2e8f0;
overflow: hidden;
margin-top: 8px;
.dropdown-item {
display: flex;
align-items: center;
padding: 12px 16px;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
color: #374151;
border-bottom: 1px solid #f1f5f9;
&:last-child {
border-bottom: none;
}
&:hover {
background: #f8fafc;
color: #667eea;
svg {
color: #667eea;
}
}
&.logout-item {
&:hover {
background: #fef2f2;
color: #dc2626;
svg {
color: #dc2626;
}
}
}
}
}
/* 退出登录弹窗样式 */
.logout-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
backdrop-filter: blur(4px);
}
.logout-modal {
background: white;
border-radius: 12px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
min-width: 400px;
max-width: 500px;
animation: modalSlideIn 0.3s ease-out;
}
@keyframes modalSlideIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.modal-header {
padding: 24px 24px 16px;
border-bottom: 1px solid #f1f5f9;
h3 {
font-size: 18px;
font-weight: 600;
color: #1e293b;
margin: 0;
}
}
.modal-body {
padding: 24px;
p {
font-size: 16px;
color: #64748b;
margin: 0;
line-height: 1.5;
}
}
.modal-footer {
padding: 16px 24px 24px 24px;
display: flex;
gap: 12px;
justify-content: flex-end;
.btn {
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
min-width: 80px;
&.btn-cancel {
background: #f8fafc;
color: #64748b;
border: 1px solid #e2e8f0;
&:hover {
background: #f1f5f9;
transform: translateY(-1px);
}
}
&.btn-confirm {
background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%);
color: white;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.3);
}
}
}
}
/* 密保设置弹窗样式 */
.security-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
backdrop-filter: blur(4px);
}
.security-modal {
background: white;
border-radius: 12px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
min-width: 400px;
max-width: 500px;
animation: modalSlideIn 0.3s ease-out;
}
.security-modal-header {
padding: 24px 24px 16px;
border-bottom: 1px solid #f1f5f9;
}
.security-modal-header h2 {
font-size: 20px;
font-weight: 600;
color: #1e293b;
margin: 0 0 8px 0;
}
.security-modal-header p {
font-size: 14px;
color: #64748b;
margin: 0;
}
.security-modal-body {
padding: 24px;
}
.security-modal-body .form-group {
margin-bottom: 20px;
}
.security-modal-body .form-group:last-child {
margin-bottom: 0;
}
.security-modal-body label {
display: block;
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 8px;
}
.security-modal-body input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
transition: all 0.2s;
box-sizing: border-box;
}
.security-modal-body input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.security-modal-body input:disabled {
background-color: #f8fafc;
cursor: not-allowed;
}
.security-modal-footer {
padding: 16px 24px 24px 24px;
display: flex;
gap: 12px;
justify-content: flex-end;
}
.security-modal-footer .btn-cancel,
.security-modal-footer .btn-confirm {
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
min-width: 80px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.security-modal-footer .btn-cancel {
background: #f8fafc;
color: #64748b;
border: 1px solid #e2e8f0;
}
.security-modal-footer .btn-cancel:hover:not(:disabled) {
background: #f1f5f9;
transform: translateY(-1px);
}
.security-modal-footer .btn-confirm {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.security-modal-footer .btn-confirm:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.security-modal-footer .btn-cancel:disabled,
.security-modal-footer .btn-confirm:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.loading-spinner {
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 修改密码弹窗样式 */
.password-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
backdrop-filter: blur(4px);
}
.password-modal {
background: white;
border-radius: 12px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
min-width: 400px;
max-width: 500px;
animation: modalSlideIn 0.3s ease-out;
}
.password-modal-header {
padding: 24px 24px 16px;
border-bottom: 1px solid #f1f5f9;
}
.password-modal-header h2 {
font-size: 20px;
font-weight: 600;
color: #1e293b;
margin: 0 0 8px 0;
}
.password-modal-header p {
font-size: 14px;
color: #64748b;
margin: 0;
}
.password-modal-body {
padding: 24px;
}
.password-modal-body .form-group {
margin-bottom: 20px;
}
.password-modal-body .form-group:last-child {
margin-bottom: 0;
}
.password-modal-body label {
display: block;
font-size: 14px;
font-weight: 500;
color: #374151;
margin-bottom: 8px;
}
.password-modal-body input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
transition: all 0.2s;
box-sizing: border-box;
}
.password-modal-body input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.password-modal-body input:disabled {
background-color: #f8fafc;
cursor: not-allowed;
}
.password-modal-footer {
padding: 16px 24px 24px 24px;
display: flex;
gap: 12px;
justify-content: flex-end;
}
.password-modal-footer .btn-cancel,
.password-modal-footer .btn-confirm {
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: none;
min-width: 80px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.password-modal-footer .btn-cancel {
background: #f8fafc;
color: #64748b;
border: 1px solid #e2e8f0;
}
.password-modal-footer .btn-cancel:hover:not(:disabled) {
background: #f1f5f9;
transform: translateY(-1px);
}
.password-modal-footer .btn-confirm {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.password-modal-footer .btn-confirm:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.password-modal-footer .btn-cancel:disabled,
.password-modal-footer .btn-confirm:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
</style>