config(eslint): 更新Vue组件代码块顺序规范

- 修改vue/block-order规则为template→script→style顺序
- 与项目新规范保持一致,提升代码可读性
- 确保ESLint规则与团队开发规范同步
This commit is contained in:
Leo 2025-07-07 00:16:29 +08:00
parent d356434c5a
commit 696c8b1417
2 changed files with 591 additions and 0 deletions

View File

@ -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'],
}],
},
},
},

View 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>