初次提交
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user