feat: 完善个人中心

This commit is contained in:
chansee97 2025-09-04 18:07:00 +08:00
parent c168e92135
commit 41d4e9a0d5
6 changed files with 356 additions and 79 deletions

View File

@ -48,3 +48,14 @@ export function updateUser(id: number, data: Partial<Entity.User>) {
export function deleteUser(id: number) {
return request.Delete<Api.Response<boolean>>(`/user/${id}`)
}
/**
*
* Patch /user/password
*/
export interface UserPasswordParams {
oldPassword: string
newPassword: string
}
export function updateUserPassword(data: UserPasswordParams) {
return request.Patch<Api.Response<boolean>>(`/user/password`, data)
}

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
//
const settings = ref({
systemUpdateEmail: true,
securityAlertEmail: true,
})
</script>
<template>
<div class="space-y-4">
<!-- 邮件通知 -->
<n-card title="邮件通知" size="small">
<n-list>
<n-list-item>
<n-thing>
<template #header>
系统更新通知
</template>
<template #description>
系统有重要更新时发送邮件通知
</template>
</n-thing>
<template #suffix>
<n-switch v-model:value="settings.systemUpdateEmail" />
</template>
</n-list-item>
<n-list-item>
<n-thing>
<template #header>
安全警报
</template>
<template #description>
检测到安全威胁时立即发送邮件
</template>
</n-thing>
<template #suffix>
<n-switch v-model:value="settings.securityAlertEmail" />
</template>
</n-list-item>
</n-list>
</n-card>
</div>
</template>

View File

@ -61,7 +61,6 @@ async function updateUserInfo() {
:model="formData"
label-placement="left"
label-width="80px"
class="max-w-400px"
>
<n-grid :cols="1">
<n-grid-item>

View File

@ -0,0 +1,47 @@
<script setup lang="ts">
//
const preferences = ref({
aiAssistant: false,
predictiveLoading: false,
})
</script>
<template>
<div class="space-y-4">
<!-- 实验性功能 -->
<n-card title="实验性功能" size="small">
<n-alert type="warning" style="margin-bottom: 16px;">
以下功能仍在测试阶段可能会影响系统稳定性
</n-alert>
<n-list>
<n-list-item>
<n-thing>
<template #header>
AI 助手
</template>
<template #description>
启用智能助手功能提供操作建议
</template>
</n-thing>
<template #suffix>
<n-switch v-model:value="preferences.aiAssistant" />
</template>
</n-list-item>
<n-list-item>
<n-thing>
<template #header>
预测性加载
</template>
<template #description>
根据使用习惯预加载可能需要的内容
</template>
</n-thing>
<template #suffix>
<n-switch v-model:value="preferences.predictiveLoading" />
</template>
</n-list-item>
</n-list>
</n-card>
</div>
</template>

View File

@ -0,0 +1,162 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { FormInst, FormRules } from 'naive-ui'
import { useMessage } from 'naive-ui'
import { updateUserPassword } from '@/api/system/user'
const message = useMessage()
const formRef = ref<FormInst | null>(null)
const loading = ref(false)
//
const passwordForm = ref({
oldPassword: '',
newPassword: '',
confirmPassword: '',
})
//
const loginNotificationEnabled = ref(true)
//
const passwordRules: FormRules = {
oldPassword: [
{ 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, value) => {
return value === passwordForm.value.newPassword
},
message: '两次输入的密码不一致',
trigger: 'blur',
},
],
}
//
async function handleUpdatePassword() {
try {
await formRef.value?.validate()
loading.value = true
await updateUserPassword({
oldPassword: passwordForm.value.oldPassword,
newPassword: passwordForm.value.newPassword,
})
message.success('密码更新成功')
resetForm()
}
finally {
loading.value = false
}
}
//
function resetForm() {
passwordForm.value = {
oldPassword: '',
newPassword: '',
confirmPassword: '',
}
formRef.value?.restoreValidation()
}
</script>
<template>
<div class="space-y-4">
<!-- 密码更新 -->
<n-card title="密码更新" size="small">
<n-form
ref="formRef"
:model="passwordForm"
:rules="passwordRules"
label-placement="left"
label-width="120px"
require-mark-placement="right-hanging"
>
<n-form-item label="当前密码" path="oldPassword">
<n-input
v-model:value="passwordForm.oldPassword"
type="password"
placeholder="请输入当前密码"
show-password-on="click"
/>
</n-form-item>
<n-form-item label="新密码" path="newPassword">
<n-input
v-model:value="passwordForm.newPassword"
type="password"
placeholder="请输入新密码"
show-password-on="click"
/>
</n-form-item>
<n-form-item label="确认新密码" path="confirmPassword">
<n-input
v-model:value="passwordForm.confirmPassword"
type="password"
placeholder="请再次输入新密码"
show-password-on="click"
/>
</n-form-item>
<n-form-item label=" ">
<n-space>
<n-button
type="primary"
:loading="loading"
@click="handleUpdatePassword"
>
更新密码
</n-button>
<n-button @click="resetForm">
重置
</n-button>
</n-space>
</n-form-item>
</n-form>
</n-card>
<!-- 账户安全 -->
<n-card title="账户安全" size="small">
<n-list>
<n-list-item>
<n-thing>
<template #header>
登录设备管理
</template>
<template #description>
管理已登录的设备可以远程注销可疑设备
</template>
</n-thing>
<template #suffix>
<n-button size="small" secondary>
查看设备
</n-button>
</template>
</n-list-item>
<n-list-item>
<n-thing>
<template #header>
登录通知
</template>
<template #description>
新设备登录时发送邮件通知
</template>
</n-thing>
<template #suffix>
<n-switch v-model:value="loginNotificationEnabled" />
</template>
</n-list-item>
</n-list>
</n-card>
</div>
</template>

View File

@ -1,13 +1,65 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { computed, h, onMounted, ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useAuthStore } from '@/store'
import PersonalInfo from './components/PersonalInfo.vue'
import SecuritySettings from './components/SecuritySettings.vue'
import NotificationSettings from './components/NotificationSettings.vue'
import PreferenceSettings from './components/PreferenceSettings.vue'
import IconUser from '~icons/icon-park-outline/user'
import IconLock from '~icons/icon-park-outline/lock'
import IconRemind from '~icons/icon-park-outline/remind'
import IconSetting from '~icons/icon-park-outline/setting-one'
const authStore = useAuthStore()
const { userInfo } = storeToRefs(authStore)
//
const activeTab = ref('profile')
//
const activeMenu = ref('profile')
// -
const pageConfig = [
{
key: 'profile',
label: '个人信息',
title: '个人信息',
icon: IconUser,
component: PersonalInfo,
},
{
key: 'security',
label: '安全设置',
title: '安全设置',
icon: IconLock,
component: SecuritySettings,
},
{
key: 'notification',
label: '通知设置',
title: '通知设置',
icon: IconRemind,
component: NotificationSettings,
},
{
key: 'preference',
label: '偏好设置',
title: '偏好设置',
icon: IconSetting,
component: PreferenceSettings,
},
]
// -
const menuOptions = pageConfig.map(({ key, label, icon }) => ({
key,
label,
icon: () => h(icon),
}))
// -
const currentPageConfig = computed(() => {
return pageConfig.find(config => config.key === activeMenu.value)
})
//
function getGreeting() {
@ -34,83 +86,46 @@ onMounted(async () => {
</script>
<template>
<n-space vertical>
<!-- 欢迎标题 -->
<n-card>
<n-flex align="center" justify="space-between">
<div>
<n-h2>
<n-card>
<n-flex :wrap="false" style="height: 100%;">
<!-- 左侧区域 -->
<div class="w-[220px] border-r border-[var(--n-border-color)] flex flex-col">
<!-- 左上头像和招呼 -->
<n-flex
align="center" justify="center" vertical
class="p-3 border-b border-[var(--n-border-color)]"
:size="8"
>
<n-avatar
round
:size="80"
:src="userInfo?.avatar || `https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=${userInfo!.username}`"
/>
<n-h3 class="m-0">
{{ getGreeting() }}{{ userInfo?.nickName || userInfo.username }}
</n-h2>
<n-p class="text-sm opacity-90 m-0">
</n-h3>
<n-text>
欢迎使用个人中心
</n-p>
</div>
<n-avatar
round
:size="64"
:src="userInfo?.avatar || `https://api.dicebear.com/9.x/adventurer-neutral/svg?seed=${userInfo!.username}`"
</n-text>
</n-flex>
<!-- 左下菜单 -->
<n-menu
v-model:value="activeMenu"
class="flex-1"
:options="menuOptions"
:icon-size="16"
/>
</n-flex>
</n-card>
</div>
<!-- 主要内容区域 -->
<n-card>
<n-tabs
v-model:value="activeTab"
type="line"
placement="left"
tab-style="min-height: 30px;"
pane-style="padding-left: 1rem;"
>
<n-tab-pane name="profile">
<template #tab>
<icon-park-outline-user class="mr-2" />
个人信息
</template>
<n-h5>
个人信息
</n-h5>
<n-divider />
<PersonalInfo />
</n-tab-pane>
<n-tab-pane name="security">
<template #tab>
<icon-park-outline-lock class="mr-2" />
安全设置
</template>
<n-h5>
安全设置
</n-h5>
<n-divider />
<n-empty description="功能开发中..." />
</n-tab-pane>
<n-tab-pane name="notification">
<template #tab>
<icon-park-outline-remind class="mr-2" />
通知设置
</template>
<n-h5>
通知设置
</n-h5>
<n-divider />
<n-empty description="功能开发中..." />
</n-tab-pane>
<n-tab-pane name="preference">
<template #tab>
<icon-park-outline-setting-one class="mr-2" />
偏好设置
</template>
<n-h5>
偏好设置
</n-h5>
<n-divider />
<n-empty description="功能开发中..." />
</n-tab-pane>
</n-tabs>
</n-card>
</n-space>
<!-- 右侧功能区域 -->
<div class="flex-1 p-2">
<n-h4 class="flex items-center gap-2">
<component :is="currentPageConfig!.icon" /> {{ currentPageConfig!.title }}
</n-h4>
<n-divider />
<component :is="currentPageConfig!.component" />
</div>
</n-flex>
</n-card>
</template>