Files
ystp/frontend/src/pages/dashboard/DashboardSettingsPage.vue
2026-07-26 02:03:43 +08:00

310 lines
11 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.
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { getProfile, sendVerification, updatePassword, updateProfile, type UserProfile } from '@/services/api'
import { ApiError } from '@/services/http'
import { useAuthStore } from '@/stores/auth'
const auth = useAuthStore()
const loading = ref(true)
const profileForm = ref({ email: '', username: '', currentPassword: '' })
const profileBusy = ref(false)
const profileMessage = ref<string | null>(null)
const profileError = ref<string | null>(null)
const passwordForm = ref({ current: '', next: '', confirm: '' })
const passwordBusy = ref(false)
const passwordMessage = ref<string | null>(null)
const passwordError = ref<string | null>(null)
const verificationBusy = ref(false)
const verificationMessage = ref<string | null>(null)
const verificationError = ref<string | null>(null)
const canResendVerification = computed(() => Boolean(auth.user && !auth.user.email_verified))
const emailChangeRequested = computed(() => {
const email = profileForm.value.email.trim().toLowerCase()
return Boolean(email && auth.user && email !== auth.user.email)
})
function syncProfile(user: UserProfile) {
profileForm.value = { email: user.email ?? '', username: user.username ?? '', currentPassword: '' }
}
onMounted(async () => {
if (!auth.token) {
loading.value = false
return
}
try {
const user = await getProfile(auth.token)
auth.updateUser(user)
syncProfile(user)
} catch (err) {
if (err instanceof ApiError) {
profileError.value = `[${err.code}] ${err.message}`
} else {
profileError.value = '加载失败,请稍后再试'
}
} finally {
loading.value = false
}
})
async function resendVerification() {
if (!auth.token) return
verificationBusy.value = true
verificationMessage.value = null
verificationError.value = null
try {
const resp = await sendVerification(auth.token)
auth.updateUser(await getProfile(auth.token))
verificationMessage.value = resp.message
} catch (err) {
if (err instanceof ApiError) {
verificationError.value = `[${err.code}] ${err.message}`
} else {
verificationError.value = '发送失败,请稍后再试'
}
} finally {
verificationBusy.value = false
}
}
async function saveProfile() {
if (!auth.token || !auth.user) return
profileBusy.value = true
profileMessage.value = null
profileError.value = null
try {
const email = profileForm.value.email.trim().toLowerCase()
const username = profileForm.value.username.trim()
const payload: { email?: string; username?: string; current_password?: string } = {}
if (email && email !== auth.user.email) {
if (!profileForm.value.currentPassword) {
profileError.value = '修改邮箱需要输入当前密码'
return
}
payload.email = email
payload.current_password = profileForm.value.currentPassword
}
if (username && username !== auth.user.username) payload.username = username
if (!payload.email && !payload.username) {
profileMessage.value = '暂无更新'
return
}
const resp = await updateProfile(auth.token, payload)
if (resp.token) {
auth.setAuth(resp.token, resp.user)
} else {
auth.updateUser(resp.user)
}
syncProfile(resp.user)
profileMessage.value = resp.message
} catch (err) {
if (err instanceof ApiError) {
profileError.value = `[${err.code}] ${err.message}`
} else {
profileError.value = '更新失败,请稍后再试'
}
} finally {
profileBusy.value = false
}
}
async function changePassword() {
if (!auth.token) return
passwordBusy.value = true
passwordMessage.value = null
passwordError.value = null
try {
const currentPassword = passwordForm.value.current.trim()
const nextPassword = passwordForm.value.next.trim()
const confirm = passwordForm.value.confirm.trim()
if (!currentPassword || !nextPassword) {
passwordError.value = '请填写当前密码与新密码'
return
}
if (nextPassword !== confirm) {
passwordError.value = '两次输入的新密码不一致'
return
}
const resp = await updatePassword(auth.token, currentPassword, nextPassword)
passwordMessage.value = resp.message
passwordForm.value = { current: '', next: '', confirm: '' }
} catch (err) {
if (err instanceof ApiError) {
passwordError.value = `[${err.code}] ${err.message}`
} else {
passwordError.value = '更新失败,请稍后再试'
}
} finally {
passwordBusy.value = false
}
}
</script>
<template>
<div class="space-y-6">
<div class="space-y-1">
<h1 class="text-xl font-semibold text-slate-900">账号设置</h1>
<p class="text-sm text-slate-600">更新个人资料邮箱验证与密码</p>
</div>
<div v-if="loading" class="text-sm text-slate-600">加载中</div>
<div v-else class="space-y-6">
<div class="rounded-xl border border-slate-200 bg-white p-6">
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="text-sm font-medium text-slate-900">账号资料</div>
<span
class="rounded-full px-2 py-1 text-xs"
:class="auth.user?.email_verified ? 'bg-emerald-50 text-emerald-700' : 'bg-amber-50 text-amber-800'"
>
{{ auth.user?.email_verified ? '邮箱已验证' : '邮箱未验证' }}
</span>
</div>
<div class="mt-4 grid grid-cols-1 gap-3 md:grid-cols-2">
<label class="space-y-1">
<div class="text-xs font-medium text-slate-600">邮箱</div>
<input
v-model="profileForm.email"
type="email"
class="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800"
placeholder="name@company.com"
/>
</label>
<label class="space-y-1">
<div class="text-xs font-medium text-slate-600">用户名</div>
<input
v-model="profileForm.username"
type="text"
class="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800"
placeholder="你的用户名"
/>
</label>
</div>
<label v-if="emailChangeRequested" class="mt-3 block max-w-md space-y-1">
<div class="text-xs font-medium text-slate-600">当前密码修改邮箱必填</div>
<input
v-model="profileForm.currentPassword"
type="password"
autocomplete="current-password"
class="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800"
placeholder="用于确认是本人操作"
/>
</label>
<div
v-if="auth.user?.pending_email"
class="mt-3 rounded-lg border border-amber-200 bg-amber-50 p-3 text-sm text-amber-900"
>
待确认新邮箱{{ auth.user.pending_email }}确认前仍使用当前邮箱登录和找回密码
</div>
<div v-if="profileMessage" class="mt-4 rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-900">
{{ profileMessage }}
</div>
<div v-if="profileError" class="mt-4 rounded-lg border border-rose-200 bg-rose-50 p-3 text-sm text-rose-900">
{{ profileError }}
</div>
<div class="mt-4 flex flex-wrap items-center gap-3">
<button
type="button"
class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 disabled:opacity-50"
:disabled="profileBusy"
@click="saveProfile"
>
{{ profileBusy ? '保存中…' : '保存资料' }}
</button>
<div v-if="canResendVerification">
<button
type="button"
class="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800 hover:bg-amber-100 disabled:opacity-50"
:disabled="verificationBusy"
@click="resendVerification"
>
{{ verificationBusy ? '发送中…' : '重新发送验证邮件' }}
</button>
</div>
</div>
<div
v-if="verificationMessage"
class="mt-3 rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-900"
>
{{ verificationMessage }}
</div>
<div v-if="verificationError" class="mt-3 rounded-lg border border-rose-200 bg-rose-50 p-3 text-sm text-rose-900">
{{ verificationError }}
</div>
<div class="mt-2 text-xs text-slate-500">开发环境邮件关闭时链接会打印在后端日志中</div>
</div>
<div class="rounded-xl border border-slate-200 bg-white p-6">
<div class="text-sm font-medium text-slate-900">修改密码</div>
<div class="mt-4 grid grid-cols-1 gap-3 md:grid-cols-3">
<label class="space-y-1">
<div class="text-xs font-medium text-slate-600">当前密码</div>
<input
v-model="passwordForm.current"
type="password"
class="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800"
placeholder="当前密码"
/>
</label>
<label class="space-y-1">
<div class="text-xs font-medium text-slate-600">新密码</div>
<input
v-model="passwordForm.next"
type="password"
class="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800"
placeholder="至少 8 位"
/>
</label>
<label class="space-y-1">
<div class="text-xs font-medium text-slate-600">确认新密码</div>
<input
v-model="passwordForm.confirm"
type="password"
class="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm text-slate-800"
placeholder="再次输入新密码"
/>
</label>
</div>
<div v-if="passwordMessage" class="mt-4 rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-sm text-emerald-900">
{{ passwordMessage }}
</div>
<div v-if="passwordError" class="mt-4 rounded-lg border border-rose-200 bg-rose-50 p-3 text-sm text-rose-900">
{{ passwordError }}
</div>
<div class="mt-4">
<button
type="button"
class="rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-950 disabled:opacity-50"
:disabled="passwordBusy"
@click="changePassword"
>
{{ passwordBusy ? '更新中…' : '更新密码' }}
</button>
</div>
</div>
</div>
</div>
</template>