Files
system-file/web-vue/src/components/ProfileDialog.vue
2026-04-02 23:35:48 +08:00

275 lines
6.8 KiB
Vue

<template>
<el-dialog
v-model="visible"
title="个人信息"
width="520px"
class="custom-dialog"
:close-on-click-modal="false"
>
<div class="profile-container">
<!-- 上方头像区域 -->
<div class="profile-top">
<div class="avatar-wrapper">
<el-avatar :size="88" :src="avatarUrl" class="profile-avatar">
{{ !avatarUrl ? ((userStore.nickname || userStore.username)?.[0]?.toUpperCase() || '?') : '' }}
</el-avatar>
<div class="avatar-overlay" @click="triggerAvatarUpload">
<el-icon :size="20"><Camera /></el-icon>
<span>更换</span>
</div>
<input ref="avatarInputRef" type="file" accept="image/*" style="display:none" @change="handleAvatarChange" />
</div>
<div class="username-text">@{{ userStore.nickname || userStore.username }}</div>
</div>
<!-- 分割线 -->
<el-divider />
<!-- 下方表单区域 -->
<div class="profile-bottom">
<el-form label-position="right" label-width="70px" class="profile-form" :model="form" :rules="rules" ref="formRef">
<el-form-item label="账号">
<el-input :model-value="userStore.username" disabled :prefix-icon="User" />
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="form.nickname" placeholder="设置你的昵称" maxlength="20" show-word-limit :prefix-icon="UserFilled" />
</el-form-item>
<el-form-item label="电话" prop="phone">
<el-input v-model="form.phone" placeholder="请输入电话号码" maxlength="11" :prefix-icon="Phone" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱地址" maxlength="50" :prefix-icon="Message" />
</el-form-item>
<el-form-item label="签名" prop="signature" class="signature-item">
<el-input
v-model="form.signature"
type="textarea"
:rows="3"
placeholder="写点什么介绍自己吧..."
maxlength="100"
show-word-limit
resize="none"
/>
</el-form-item>
</el-form>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false">
<el-icon><Close /></el-icon>
<span style="margin-left: 4px">取消</span>
</el-button>
<el-button type="primary" @click="handleSave" :loading="saving">
<el-icon><Check /></el-icon>
<span style="margin-left: 4px">保存</span>
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { computed, ref, reactive, watch } from 'vue'
import { useUserStore } from '@/store/user'
import { ElMessage } from 'element-plus'
import { Camera, User, UserFilled, Close, Check, Phone, Message } from '@element-plus/icons-vue'
import request from '@/api/request'
const props = defineProps({ modelValue: Boolean })
const emit = defineEmits(['update:modelValue'])
const userStore = useUserStore()
const visible = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
})
const avatarUrl = ref('')
const avatarInputRef = ref(null)
const formRef = ref(null)
const saving = ref(false)
const form = reactive({
nickname: '',
phone: '',
email: '',
signature: ''
})
const rules = {
nickname: [{ max: 20, message: '昵称最多20个字符', trigger: 'blur' }],
phone: [
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
email: [
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }
],
signature: [{ max: 100, message: '签名最多100个字符', trigger: 'blur' }]
}
watch(visible, (v) => {
if (v) {
avatarUrl.value = userStore.avatar || ''
form.nickname = userStore.nickname || ''
form.phone = userStore.phone || ''
form.email = userStore.email || ''
form.signature = userStore.signature || ''
}
})
const triggerAvatarUpload = () => {
avatarInputRef.value?.click()
}
const handleAvatarChange = async (e) => {
const file = e.target.files?.[0]
if (!file) return
e.target.value = ''
if (file.size > 2 * 1024 * 1024) {
ElMessage.warning('图片大小不能超过2MB')
return
}
try {
const formData = new FormData()
formData.append('avatar', file)
const data = await request.post('/users/avatar', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
const newUrl = data?.url || ''
avatarUrl.value = newUrl
userStore.setUser({ avatar: newUrl })
ElMessage.success('头像更新成功')
} catch {
ElMessage.error('头像上传失败')
}
}
const handleSave = async () => {
try {
await formRef.value.validate()
} catch {
return
}
try {
await request.put('/users/profile', {
nickname: form.nickname,
phone: form.phone,
email: form.email,
signature: form.signature
})
userStore.setUser({
nickname: form.nickname,
phone: form.phone,
email: form.email,
signature: form.signature
})
ElMessage.success('保存成功')
visible.value = false
} catch {
ElMessage.error('保存失败')
}
}
</script>
<style scoped>
.profile-container {
display: flex;
flex-direction: column;
align-items: center;
}
.profile-top {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.avatar-wrapper {
position: relative;
cursor: pointer;
}
.profile-avatar {
background: linear-gradient(135deg, #409eff, #66b1ff);
font-size: 32px;
font-weight: 600;
border: 3px solid #e4e7ed;
transition: border-color 0.3s;
}
.avatar-wrapper:hover .profile-avatar {
border-color: #409eff;
}
.avatar-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 28px;
background: rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
border-radius: 0 0 44px 44px;
color: #fff;
font-size: 12px;
opacity: 0;
transition: opacity 0.3s;
}
.avatar-wrapper:hover .avatar-overlay {
opacity: 1;
}
.username-text {
font-size: 14px;
color: #606266;
font-weight: 500;
}
.profile-top :deep(.el-divider) {
margin: 4px 0 0 0;
}
.profile-bottom {
width: 100%;
}
.profile-form {
width: 100%;
}
.profile-form :deep(.el-form-item) {
margin-bottom: 16px;
}
.profile-form :deep(.el-form-item__label) {
font-weight: 500;
color: #606266;
padding-right: 12px;
}
.signature-item :deep(.el-form-item__content) {
display: block;
}
.profile-form :deep(.el-input.is-disabled .el-input__inner) {
background: #f5f7fa;
color: #909399;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>