config(eslint): 更新Vue组件代码块顺序规范
- 修改vue/block-order规则为template→script→style顺序 - 与项目新规范保持一致,提升代码可读性 - 确保ESLint规则与团队开发规范同步
This commit is contained in:
parent
d356434c5a
commit
696c8b1417
@ -19,6 +19,9 @@ export default antfu(
|
||||
'vue/no-unused-refs': 'off', // 暂时关闭,等待vue-lint的分支合并
|
||||
'vue/no-reserved-component-names': 'off',
|
||||
'vue/component-definition-name-casing': 'off',
|
||||
'vue/block-order': ['error', {
|
||||
order: ['template', 'script', 'style'],
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
588
src/views/personal-center/index.vue
Normal file
588
src/views/personal-center/index.vue
Normal file
@ -0,0 +1,588 @@
|
||||
<template>
|
||||
<div class="p-1 bg-gray-50 h-screen flex flex-col">
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-100 flex-1 overflow-auto">
|
||||
<div class="p-6">
|
||||
<!-- 页面标题 -->
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-900">
|
||||
个人中心
|
||||
</h1>
|
||||
<p class="text-gray-600 mt-1">
|
||||
管理您的个人信息和账户设置
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- 左侧个人信息卡片 -->
|
||||
<div class="lg:col-span-1">
|
||||
<n-card title="个人信息" class="h-fit">
|
||||
<n-spin :show="loading">
|
||||
<div class="text-center mb-6">
|
||||
<!-- 头像 -->
|
||||
<div class="relative inline-block">
|
||||
<n-avatar
|
||||
:size="100"
|
||||
:src="personalData.avatar"
|
||||
round
|
||||
class="border-4 border-blue-100 cursor-pointer hover:border-blue-300 transition-colors"
|
||||
@click="handleAvatarClick"
|
||||
>
|
||||
<template #placeholder>
|
||||
<icon-park-outline-user class="text-4xl" />
|
||||
</template>
|
||||
</n-avatar>
|
||||
<!-- 头像上传图标 -->
|
||||
<div
|
||||
class="absolute bottom-0 right-0 w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center cursor-pointer hover:bg-blue-600 transition-colors shadow-md"
|
||||
@click="handleAvatarClick"
|
||||
>
|
||||
<icon-park-outline-camera class="text-white text-sm" />
|
||||
</div>
|
||||
<!-- 上传中遮罩 -->
|
||||
<div
|
||||
v-if="uploading"
|
||||
class="absolute inset-0 bg-black bg-opacity-50 rounded-full flex items-center justify-center"
|
||||
>
|
||||
<n-spin size="small" stroke="white" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的文件输入 -->
|
||||
<input
|
||||
ref="avatarUploadRef"
|
||||
type="file"
|
||||
accept="image/jpeg,image/jpg,image/png,image/gif"
|
||||
class="hidden"
|
||||
@change="handleAvatarChange"
|
||||
>
|
||||
|
||||
<!-- 用户名 -->
|
||||
<h3 class="text-xl font-semibold mt-4 text-gray-900">
|
||||
{{ personalData.userName || '未设置' }}
|
||||
</h3>
|
||||
|
||||
<!-- 登录名 -->
|
||||
<p class="text-gray-500 text-sm">
|
||||
@{{ personalData.loginName }}
|
||||
</p>
|
||||
|
||||
<!-- 头像上传提示 -->
|
||||
<p class="text-xs text-gray-400 mt-2">
|
||||
点击头像上传新头像
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 详细信息 -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span class="text-gray-600">邮箱</span>
|
||||
<span class="text-gray-900">{{ personalData.email || '未设置' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span class="text-gray-600">手机号</span>
|
||||
<span class="text-gray-900">{{ personalData.phone || '未设置' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span class="text-gray-600">性别</span>
|
||||
<span class="text-gray-900">{{ getGenderText(personalData.sex || '3') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center py-2 border-b border-gray-100">
|
||||
<span class="text-gray-600">状态</span>
|
||||
<n-tag :type="personalData.userStatus === '0' ? 'success' : 'error'" size="small">
|
||||
{{ getStatusText(personalData.userStatus || '0') }}
|
||||
</n-tag>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center py-2">
|
||||
<span class="text-gray-600">注册时间</span>
|
||||
<span class="text-gray-900 text-sm">{{ formatDate(personalData.createTime || '') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</n-spin>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作区域 -->
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
<!-- 基本资料编辑 -->
|
||||
<n-card title="基本资料编辑">
|
||||
<template #header-extra>
|
||||
<n-space>
|
||||
<n-button :loading="loading" @click="handleBasicReset">
|
||||
<template #icon>
|
||||
<icon-park-outline-refresh />
|
||||
</template>
|
||||
重置
|
||||
</n-button>
|
||||
<n-button type="primary" :loading="loading" @click="handleBasicSave">
|
||||
<template #icon>
|
||||
<icon-park-outline-save />
|
||||
</template>
|
||||
保存
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
|
||||
<n-form
|
||||
ref="basicFormRef"
|
||||
:model="basicForm"
|
||||
:rules="basicRules"
|
||||
label-placement="left"
|
||||
label-width="100px"
|
||||
require-mark-placement="right-hanging"
|
||||
>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<n-form-item label="用户名" path="userName">
|
||||
<n-input v-model:value="basicForm.userName" placeholder="请输入用户名" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="性别" path="sex">
|
||||
<n-select
|
||||
v-model:value="basicForm.sex"
|
||||
:options="genderOptions"
|
||||
placeholder="请选择性别"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="邮箱地址" path="email">
|
||||
<n-input v-model:value="basicForm.email" placeholder="请输入邮箱地址" />
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="手机号码" path="phone">
|
||||
<n-input v-model:value="basicForm.phone" placeholder="请输入手机号码" />
|
||||
</n-form-item>
|
||||
</div>
|
||||
</n-form>
|
||||
</n-card>
|
||||
|
||||
<!-- 密码管理 -->
|
||||
<n-card title="密码管理">
|
||||
<template #header-extra>
|
||||
<n-button type="warning" @click="openPasswordDialog">
|
||||
<template #icon>
|
||||
<icon-park-outline-lock />
|
||||
</template>
|
||||
修改密码
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||
<div class="flex items-start">
|
||||
<icon-park-outline-info class="text-yellow-600 text-lg mt-0.5 mr-3" />
|
||||
<div>
|
||||
<h4 class="text-yellow-800 font-medium">
|
||||
密码安全提示
|
||||
</h4>
|
||||
<p class="text-yellow-700 text-sm mt-1">
|
||||
为了您的账户安全,建议定期更换密码。新密码应包含字母、数字,长度不少于6位。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改密码弹框 -->
|
||||
<NovaDialog
|
||||
ref="passwordDialogRef"
|
||||
title="修改密码"
|
||||
:width="500"
|
||||
height="auto"
|
||||
confirm-text="确认修改"
|
||||
cancel-text="取消"
|
||||
@nova-confirm="handlePasswordSubmit"
|
||||
@nova-cancel="handlePasswordCancel"
|
||||
>
|
||||
<template #content>
|
||||
<div class="px-3 py-2">
|
||||
<n-form
|
||||
ref="passwordFormRef"
|
||||
:model="passwordForm"
|
||||
:rules="passwordRules"
|
||||
label-placement="top"
|
||||
require-mark-placement="right-hanging"
|
||||
>
|
||||
<n-form-item label="当前密码" path="password">
|
||||
<n-input
|
||||
v-model:value="passwordForm.password"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
placeholder="请输入当前密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="新密码" path="newPassword">
|
||||
<n-input
|
||||
v-model:value="passwordForm.newPassword"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
placeholder="请输入新密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="确认新密码" path="confirmPassword">
|
||||
<n-input
|
||||
v-model:value="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
</template>
|
||||
</NovaDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { coiMsgError, coiMsgSuccess } from '@/utils/coi'
|
||||
import { getPersonalData, updateBasicData, updatePassword, uploadAvatar } from '@/service/api/personal'
|
||||
import type { PersonalDataVo, UpdatePasswordBo, UpdatePersonalBo } from '@/service/api/personal'
|
||||
import NovaDialog from '@/components/common/NovaDialog.vue'
|
||||
import { serviceConfig } from '@/../service.config'
|
||||
|
||||
// 获取用户信息
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 页面数据
|
||||
const loading = ref(false)
|
||||
const uploading = ref(false)
|
||||
const personalData = ref<PersonalDataVo>({
|
||||
userId: 0,
|
||||
userName: '',
|
||||
loginName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
sex: '1',
|
||||
avatar: '',
|
||||
userStatus: '0',
|
||||
createTime: '',
|
||||
})
|
||||
|
||||
// 表单引用
|
||||
const basicFormRef = ref()
|
||||
const passwordFormRef = ref()
|
||||
const avatarUploadRef = ref()
|
||||
|
||||
// 编辑基本信息表单
|
||||
const basicForm = ref<UpdatePersonalBo>({
|
||||
userName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
sex: '1',
|
||||
avatar: '',
|
||||
})
|
||||
|
||||
// 修改密码表单
|
||||
const passwordForm = ref<UpdatePasswordBo>({
|
||||
password: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
})
|
||||
|
||||
// 弹框控制
|
||||
const passwordDialogRef = ref()
|
||||
|
||||
// 表单验证规则
|
||||
const basicRules = {
|
||||
userName: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
],
|
||||
email: [
|
||||
{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' },
|
||||
],
|
||||
phone: [
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
const passwordRules = {
|
||||
password: [
|
||||
{ required: true, message: '请输入当前密码', trigger: 'blur' },
|
||||
],
|
||||
newPassword: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' },
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '请确认新密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (_rule: any, value: string) => {
|
||||
if (value !== passwordForm.value.newPassword) {
|
||||
return new Error('两次输入密码不一致')
|
||||
}
|
||||
return true
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
// 性别选项
|
||||
const genderOptions = [
|
||||
{ label: '男', value: '1' },
|
||||
{ label: '女', value: '2' },
|
||||
{ label: '未知', value: '3' },
|
||||
]
|
||||
|
||||
// 状态选项
|
||||
const statusOptions = [
|
||||
{ label: '启用', value: '0' },
|
||||
{ label: '停用', value: '1' },
|
||||
]
|
||||
|
||||
// 获取性别显示文字
|
||||
function getGenderText(sex: string) {
|
||||
const option = genderOptions.find(item => item.value === sex)
|
||||
return option?.label || '未知'
|
||||
}
|
||||
|
||||
// 获取状态显示文字
|
||||
function getStatusText(status: string) {
|
||||
const option = statusOptions.find(item => item.value === status)
|
||||
return option?.label || '未知'
|
||||
}
|
||||
|
||||
// 获取个人资料
|
||||
async function fetchPersonalData() {
|
||||
try {
|
||||
loading.value = true
|
||||
const result = await getPersonalData()
|
||||
if (result.isSuccess) {
|
||||
personalData.value = result.data
|
||||
// 同步到编辑表单
|
||||
basicForm.value = {
|
||||
userName: result.data.userName,
|
||||
email: result.data.email,
|
||||
phone: result.data.phone,
|
||||
sex: result.data.sex,
|
||||
avatar: result.data.avatar,
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
coiMsgError('获取个人资料失败')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 头像上传处理
|
||||
function handleAvatarClick() {
|
||||
avatarUploadRef.value?.click()
|
||||
}
|
||||
|
||||
// 头像上传文件选择
|
||||
async function handleAvatarChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (target.files && target.files[0]) {
|
||||
const file = target.files[0]
|
||||
|
||||
// 验证文件类型
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
coiMsgError('只支持JPG、PNG、GIF格式的图片')
|
||||
return
|
||||
}
|
||||
|
||||
// 验证文件大小(5MB)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
coiMsgError('图片大小不能超过5MB')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
uploading.value = true
|
||||
const result = await uploadAvatar(file, 5)
|
||||
if (result.isSuccess) {
|
||||
// 使用配置文件中的服务地址构建头像访问URL
|
||||
const baseUrl = serviceConfig[import.meta.env.MODE].url
|
||||
const avatarUrl = `${baseUrl}${result.data.fileUploadPath}`
|
||||
|
||||
basicForm.value.avatar = avatarUrl
|
||||
personalData.value.avatar = avatarUrl
|
||||
coiMsgSuccess('头像上传成功')
|
||||
// 单独保存头像
|
||||
await handleAvatarSave()
|
||||
}
|
||||
else {
|
||||
coiMsgError('头像上传失败')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
coiMsgError('头像上传失败')
|
||||
}
|
||||
finally {
|
||||
uploading.value = false
|
||||
// 清空文件选择
|
||||
target.value = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 单独保存头像
|
||||
async function handleAvatarSave() {
|
||||
try {
|
||||
const result = await updateBasicData({ avatar: basicForm.value.avatar })
|
||||
if (result.isSuccess) {
|
||||
coiMsgSuccess('头像保存成功')
|
||||
// 更新全局用户状态中的头像
|
||||
authStore.updateUserInfo({ avatar: basicForm.value.avatar })
|
||||
await fetchPersonalData()
|
||||
}
|
||||
else {
|
||||
coiMsgError('头像保存失败')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
coiMsgError('头像保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 保存基本信息
|
||||
async function handleBasicSave() {
|
||||
try {
|
||||
await basicFormRef.value?.validate()
|
||||
loading.value = true
|
||||
const result = await updateBasicData(basicForm.value)
|
||||
if (result.isSuccess) {
|
||||
coiMsgSuccess('保存成功')
|
||||
// 更新全局用户状态
|
||||
authStore.updateUserInfo({
|
||||
userName: basicForm.value.userName,
|
||||
avatar: basicForm.value.avatar,
|
||||
})
|
||||
await fetchPersonalData()
|
||||
}
|
||||
else {
|
||||
coiMsgError('保存失败')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
coiMsgError('保存失败')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置基本信息表单
|
||||
function handleBasicReset() {
|
||||
basicForm.value = {
|
||||
userName: personalData.value.userName,
|
||||
email: personalData.value.email,
|
||||
phone: personalData.value.phone,
|
||||
sex: personalData.value.sex,
|
||||
avatar: personalData.value.avatar,
|
||||
}
|
||||
}
|
||||
|
||||
// 打开修改密码弹框
|
||||
function openPasswordDialog() {
|
||||
passwordForm.value = {
|
||||
password: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
}
|
||||
passwordDialogRef.value?.novaOpen()
|
||||
}
|
||||
|
||||
// 提交密码修改
|
||||
async function handlePasswordSubmit() {
|
||||
try {
|
||||
await passwordFormRef.value?.validate()
|
||||
loading.value = true
|
||||
const result = await updatePassword(passwordForm.value)
|
||||
if (result.isSuccess) {
|
||||
coiMsgSuccess('密码修改成功')
|
||||
passwordDialogRef.value?.novaClose()
|
||||
}
|
||||
else {
|
||||
coiMsgError('密码修改失败')
|
||||
}
|
||||
}
|
||||
catch {
|
||||
coiMsgError('密码修改失败')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消密码修改
|
||||
function handlePasswordCancel() {
|
||||
passwordDialogRef.value?.novaClose()
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr: string) {
|
||||
if (!dateStr)
|
||||
return '-'
|
||||
return new Date(dateStr).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 页面初始化
|
||||
onMounted(() => {
|
||||
fetchPersonalData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dark .bg-gray-50 {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
|
||||
.dark .bg-white {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
.dark .text-gray-900 {
|
||||
color: #f9fafb;
|
||||
}
|
||||
|
||||
.dark .text-gray-600 {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.dark .text-gray-700 {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.dark .border-gray-100 {
|
||||
border-color: #4b5563;
|
||||
}
|
||||
|
||||
.dark .shadow-sm {
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dark .bg-yellow-50 {
|
||||
background-color: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
.dark .border-yellow-200 {
|
||||
border-color: rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.dark .text-yellow-800 {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.dark .text-yellow-700 {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.dark .text-yellow-600 {
|
||||
color: #d97706;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue
Block a user