Files
BYW-VoiceHub/app/components/Auth/ProfileForm.vue
T
Henry_Du 4ce4649aaf
Build VoiceHub FPK / build-fpk (push) Failing after 56m5s
Build and Push Docker Image / build-and-push (push) Canceled after 1h9m35s
初次提交
2026-09-06 08:19:02 +08:00

253 lines
8.1 KiB
Vue

<template>
<div class="bg-zinc-900/40 border border-zinc-900 rounded-3xl p-6 md:p-8 shadow-2xl">
<div class="flex items-center gap-3 border-b border-zinc-800/50 pb-5 mb-6">
<div class="p-2.5 bg-green-500/10 rounded-xl">
<UserCircle2 :size="20" class="text-green-500" />
</div>
<div>
<h2 class="text-base font-black text-zinc-100">个人信息设置</h2>
<p class="text-xs text-zinc-500 mt-0.5">修改您的个人资料和学校信息</p>
</div>
</div>
<form @submit.prevent="handleUpdateProfile" class="space-y-5 max-w-md">
<!-- 昵称 -->
<div class="space-y-2">
<label class="text-sm font-medium text-zinc-300">昵称</label>
<input
v-model="form.name"
type="text"
placeholder="留空则显示用户名"
class="w-full px-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl text-sm text-zinc-100 focus:outline-none focus:border-green-500/50 focus:ring-1 focus:ring-green-500/50 transition-all placeholder:text-zinc-600"
/>
</div>
<!-- 年级与班级 -->
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<label class="text-sm font-medium text-zinc-300">年级</label>
<CustomSelect
v-model="form.grade"
:options="gradeOptions"
placeholder="请选择年级"
class-name="w-full custom-select-profile"
/>
</div>
<div class="space-y-2">
<label class="text-sm font-medium text-zinc-300">班级</label>
<CustomSelect
v-model="form.class"
:options="classOptions"
placeholder="请选择班级"
class-name="w-full custom-select-profile"
/>
</div>
</div>
<!-- 学号 -->
<div class="space-y-2">
<label class="text-sm font-medium text-zinc-300">学号</label>
<input
v-model="form.studentId"
type="text"
placeholder="选填"
class="w-full px-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl text-sm text-zinc-100 focus:outline-none focus:border-green-500/50 focus:ring-1 focus:ring-green-500/50 transition-all placeholder:text-zinc-600"
/>
</div>
<!-- 邮箱 -->
<div class="space-y-2">
<label class="text-sm font-medium text-zinc-300">邮箱</label>
<input
v-model="form.email"
type="email"
placeholder="选填"
class="w-full px-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl text-sm text-zinc-100 focus:outline-none focus:border-green-500/50 focus:ring-1 focus:ring-green-500/50 transition-all placeholder:text-zinc-600"
/>
</div>
<div v-if="emailChangedAndNotEmpty" class="space-y-2">
<label class="text-sm font-medium text-zinc-300">邮箱验证码</label>
<div class="flex gap-2">
<input
v-model="form.emailCode"
type="text"
maxlength="6"
placeholder="输入6位数字验证码"
class="flex-1 px-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl text-sm text-zinc-100 focus:outline-none focus:border-green-500/50 focus:ring-1 focus:ring-green-500/50 transition-all"
/>
<button
type="button"
:disabled="!form.email || emailActionLoading"
class="px-4 py-3 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 text-xs font-bold rounded-xl transition-all disabled:opacity-50 whitespace-nowrap border border-zinc-700"
@click="sendProfileEmailCode"
>
{{ emailActionLoading ? '发送中...' : '发送验证码' }}
</button>
</div>
</div>
<!-- 保存按钮 -->
<button
type="submit"
:disabled="loading || !isChanged"
class="w-full py-3.5 bg-green-600 hover:bg-green-500 disabled:opacity-50 disabled:hover:bg-green-600 text-white rounded-xl text-sm font-black tracking-widest transition-all shadow-lg shadow-green-900/20 mt-6"
>
<span v-if="loading" class="flex items-center justify-center gap-2">
<Loader2 class="w-4 h-4 animate-spin" />
保存中...
</span>
<span v-else>保存修改</span>
</button>
</form>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { UserCircle2, Loader2 } from '@lucide/vue'
import { useAuth } from '~/composables/useAuth'
import { useToast } from '~/composables/useToast'
import CustomSelect from '~/components/UI/Common/CustomSelect.vue'
const auth = useAuth()
const { showToast } = useToast()
const loading = ref(false)
const emailActionLoading = ref(false)
const form = reactive({
name: '',
grade: '',
class: '',
studentId: '',
email: '',
emailCode: ''
})
const gradeOptions = [
{ label: '高一', value: '高一' },
{ label: '高二', value: '高二' },
{ label: '高三', value: '高三' }
]
const classOptions = Array.from({ length: 16 }, (_, i) => ({
label: `${i + 1}班`,
value: (i + 1).toString()
}))
// 初始数据用于比较是否被修改
const initialData = ref({})
onMounted(() => {
if (auth.user.value) {
form.name = auth.user.value.name || ''
form.grade = auth.user.value.grade || ''
form.class = auth.user.value.class || ''
form.studentId = auth.user.value.studentId || ''
form.email = auth.user.value.email || ''
form.emailCode = ''
initialData.value = { ...form }
}
})
const emailChangedAndNotEmpty = computed(() => {
const currentEmail = (form.email || '').trim().toLowerCase()
const prevEmail = (initialData.value?.email || '').trim().toLowerCase()
return currentEmail && currentEmail !== prevEmail
})
const isChanged = computed(() => {
return form.name !== initialData.value.name ||
form.grade !== initialData.value.grade ||
form.class !== initialData.value.class ||
form.studentId !== initialData.value.studentId ||
form.email !== initialData.value.email ||
(emailChangedAndNotEmpty.value && form.emailCode.length === 6)
})
const sendProfileEmailCode = async () => {
const emailStr = (form.email || '').toString().trim()
if (!emailStr) return
emailActionLoading.value = true
try {
await $fetch('/api/user/email/send-code', {
method: 'POST',
body: { email: emailStr },
...auth.getAuthConfig()
})
showToast('验证码已发送,请查收邮箱', 'success')
} catch (err) {
showToast(err?.data?.message || err?.message || '验证码发送失败', 'error')
} finally {
emailActionLoading.value = false
}
}
const handleUpdateProfile = async () => {
if (form.name) {
const chineseRegex = /^[\u4e00-\u9fa5]+$/
if (!chineseRegex.test(form.name)) {
showToast('昵称只能包含汉字', 'error')
return
}
}
if (emailChangedAndNotEmpty.value && !form.emailCode) {
showToast('请填写邮箱验证码', 'error')
return
}
loading.value = true
try {
const response = await $fetch('/api/user/profile', {
method: 'PUT',
body: form
})
if (response.success) {
showToast('个人信息更新成功', 'success')
initialData.value = { ...form, emailCode: '' }
form.emailCode = ''
await auth.refreshUser()
}
} catch (error) {
showToast(error.data?.message || '更新失败', 'error')
} finally {
loading.value = false
}
}
</script>
<style scoped>
.custom-select-profile :deep(> .flex) {
padding: 10px 16px;
background-color: #09090b;
border: 1px solid #27272a;
border-radius: 0.75rem;
color: #f4f4f5;
font-size: 0.875rem;
line-height: 1.25rem;
height: 46px;
}
.custom-select-profile :deep(> .flex:hover) {
border-color: #3f3f46;
}
.custom-select-profile :deep(> .border-blue-500\/50) {
border-color: rgba(34, 197, 94, 0.5) !important;
--tw-ring-color: rgba(34, 197, 94, 0.5);
box-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
background-color: #09090b !important;
}
.custom-select-profile :deep(.text-\[11px\]) {
font-size: 0.875rem;
line-height: 1.25rem;
font-weight: normal;
}
</style>