- 恢复 variables.css 完整的 ClassicDark/ModernDark 等主题定义 - 恢复 nuxt.config.ts 的主题系统逻辑(默认改为 ClassicLight,保留切换能力) - 修复 BackupManager / VotersModal / ProfileForm / LyricsModal / RequestForm / SongList / ScheduleList / StatsSlide / ArtistSlide 等数十处组件内部硬编码深色(#1a1a1a / #040e15 等)绕过主题的问题,全部改为使用 CSS 变量 (var(--bg-primary) 等) - 这样实现了:在浅色模式下,所有页面均正确显示浅色;在深色模式下,又能正常呈现深色设计,且恢复了右上角的深浅色切换能力
253 lines
8.2 KiB
Vue
253 lines
8.2 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: var(--input-bg, #09090b);
|
|
border: 1px solid var(--input-border, #27272a);
|
|
border-radius: 0.75rem;
|
|
color: var(--input-text, #f4f4f5);
|
|
font-size: 0.875rem;
|
|
line-height: 1.25rem;
|
|
height: 46px;
|
|
}
|
|
|
|
.custom-select-profile :deep(> .flex:hover) {
|
|
border-color: var(--input-border-focus, #3f3f46);
|
|
}
|
|
|
|
.custom-select-profile :deep(> .border-blue-500\/50) {
|
|
border-color: var(--primary, rgba(34, 197, 94, 0.5)) !important;
|
|
--tw-ring-color: var(--primary-light, 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: var(--input-bg, #09090b) !important;
|
|
}
|
|
|
|
.custom-select-profile :deep(.text-\[11px\]) {
|
|
font-size: 0.875rem;
|
|
line-height: 1.25rem;
|
|
font-weight: normal;
|
|
}
|
|
</style>
|