- 学校 logo 替换为黑字版本,修改 useSiteConfig 默认值和 fallback - 首页通知卡片补齐浅色样式:status-tag(三态)、unread-indicator、 invite-actions、accept-btn、reject-btn - 投稿区域状态徽章由绝对定位改为搜索标题行内布局,背景改为浅色 - 消息设置页面整体改为浅色主题,主色系改为 indigo 以匹配 ModernLight
871 lines
31 KiB
Vue
871 lines
31 KiB
Vue
<template>
|
||
<div class="min-h-screen bg-zinc-50 text-zinc-900 pb-24">
|
||
<!-- 顶部导航栏 -->
|
||
<div
|
||
class="sticky top-0 z-30 bg-white/80 backdrop-blur-xl border-b border-zinc-200/50 px-4 py-4 mb-8"
|
||
>
|
||
<div class="max-w-[1000px] mx-auto flex items-center justify-between">
|
||
<div class="flex items-center gap-4">
|
||
<button
|
||
class="p-2 hover:bg-zinc-100 rounded-xl transition-all text-zinc-500 hover:text-zinc-900"
|
||
@click="goBack"
|
||
>
|
||
<ArrowLeft :size="20" />
|
||
</button>
|
||
<div>
|
||
<h1 class="text-xl font-black text-zinc-900 tracking-tight">消息设置</h1>
|
||
<p class="text-[10px] text-zinc-500 font-medium uppercase tracking-widest mt-0.5">
|
||
Notification Settings
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
:disabled="loading || saving"
|
||
class="flex items-center gap-2 px-6 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-black rounded-xl shadow-lg shadow-indigo-500/20 transition-all active:scale-95 disabled:opacity-50"
|
||
@click="saveSettings"
|
||
>
|
||
<template v-if="saving"> <Loader2 :size="14" class="animate-spin" /> 保存中... </template>
|
||
<template v-else> <Save :size="14" /> 保存设置 </template>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="max-w-[1000px] mx-auto px-4">
|
||
<div v-if="loading" class="flex flex-col items-center justify-center py-32">
|
||
<Loader2 :size="32" class="text-indigo-500 animate-spin mb-4" />
|
||
<p class="text-zinc-500 text-sm font-medium">加载设置中...</p>
|
||
</div>
|
||
|
||
<div v-else class="space-y-8">
|
||
<!-- 站内通知设置 -->
|
||
<section :class="sectionClass">
|
||
<div class="flex items-center gap-3 border-b border-zinc-200/50 pb-5 mb-6">
|
||
<div class="p-2.5 bg-indigo-500/10 rounded-xl">
|
||
<Bell :size="20" class="text-indigo-600" />
|
||
</div>
|
||
<div>
|
||
<h2 class="text-base font-black text-zinc-900">站内消息设置</h2>
|
||
<p class="text-xs text-zinc-500 mt-0.5">配置您希望在系统内接收的消息类型</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<!-- 歌曲被选中消息 -->
|
||
<div :class="itemClass">
|
||
<div class="flex-1">
|
||
<h3 class="text-sm font-bold text-zinc-800">歌曲被选中消息</h3>
|
||
<p class="text-[11px] text-zinc-500 mt-1">当您投稿的歌曲被选中播放时通知您</p>
|
||
</div>
|
||
<div class="shrink-0">
|
||
<input
|
||
v-model="localSettings.songSelectedNotify"
|
||
type="checkbox"
|
||
class="w-5 h-5 rounded border-zinc-300 bg-white accent-indigo-600 cursor-pointer"
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 歌曲已播放消息 -->
|
||
<div :class="itemClass">
|
||
<div class="flex-1">
|
||
<h3 class="text-sm font-bold text-zinc-800">歌曲已播放消息</h3>
|
||
<p class="text-[11px] text-zinc-500 mt-1">当您投稿的歌曲播放完成时通知您</p>
|
||
</div>
|
||
<div class="shrink-0">
|
||
<input
|
||
v-model="localSettings.songPlayedNotify"
|
||
type="checkbox"
|
||
class="w-5 h-5 rounded border-zinc-300 bg-white accent-indigo-600 cursor-pointer"
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 歌曲获得投票消息 -->
|
||
<div :class="itemClass">
|
||
<div class="flex-1">
|
||
<h3 class="text-sm font-bold text-zinc-800">歌曲获得投票消息</h3>
|
||
<p class="text-[11px] text-zinc-500 mt-1">当您投稿的歌曲获得投票时通知您</p>
|
||
</div>
|
||
<div class="shrink-0">
|
||
<input
|
||
v-model="localSettings.songVotedNotify"
|
||
type="checkbox"
|
||
class="w-5 h-5 rounded border-zinc-300 bg-white accent-indigo-600 cursor-pointer"
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 系统通知 -->
|
||
<div :class="itemClass">
|
||
<div class="flex-1">
|
||
<h3 class="text-sm font-bold text-zinc-800">系统通知</h3>
|
||
<p class="text-[11px] text-zinc-500 mt-1">接收系统重要通知和公告</p>
|
||
</div>
|
||
<div class="shrink-0">
|
||
<input
|
||
v-model="localSettings.systemNotify"
|
||
type="checkbox"
|
||
class="w-5 h-5 rounded border-zinc-300 bg-white accent-indigo-600 cursor-pointer"
|
||
>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 投票阈值设置 -->
|
||
<div :class="[itemClass, 'md:col-span-1']">
|
||
<div class="flex-1">
|
||
<h3 class="text-sm font-bold text-zinc-800">投票通知阈值</h3>
|
||
<p class="text-[11px] text-zinc-500 mt-1">当投票数达到此阈值时才发送通知</p>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<input
|
||
v-model.number="localSettings.songVotedThreshold"
|
||
type="number"
|
||
max="100"
|
||
min="1"
|
||
class="w-16 bg-white border border-zinc-200 rounded-lg px-2 py-1.5 text-xs text-center focus:outline-none focus:border-indigo-500/30"
|
||
>
|
||
<span class="text-[10px] font-black text-zinc-500 uppercase">票</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 通知刷新间隔 -->
|
||
<div :class="[itemClass, 'md:col-span-1']">
|
||
<div class="flex-1">
|
||
<h3 class="text-sm font-bold text-zinc-800">通知刷新间隔</h3>
|
||
<p class="text-[11px] text-zinc-500 mt-1">设置系统自动检查新通知的时间间隔</p>
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
<input
|
||
v-model.number="localSettings.refreshInterval"
|
||
type="range"
|
||
max="300"
|
||
min="30"
|
||
step="30"
|
||
class="w-24 h-1.5 bg-zinc-200 rounded-full appearance-none accent-indigo-600 cursor-pointer"
|
||
>
|
||
<span class="text-[11px] font-bold text-indigo-600 min-w-[40px] text-right"
|
||
>{{ localSettings.refreshInterval }}s</span
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 社交账号绑定 -->
|
||
<section :class="sectionClass">
|
||
<div class="flex items-center gap-3 border-b border-zinc-200/50 pb-5 mb-6">
|
||
<div class="p-2.5 bg-purple-500/10 rounded-xl">
|
||
<Link :size="20" class="text-purple-600" />
|
||
</div>
|
||
<div>
|
||
<h2 class="text-base font-black text-zinc-900">社交账号绑定</h2>
|
||
<p class="text-xs text-zinc-500 mt-0.5">绑定您的社交账号以接收实时推送通知</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||
<!-- 邮箱绑定 -->
|
||
<div v-if="smtpEnabled" :class="cardClass">
|
||
<div class="flex items-center gap-3 mb-4">
|
||
<div class="p-2 bg-white rounded-lg border border-zinc-200">
|
||
<Mail :size="16" class="text-zinc-500" />
|
||
</div>
|
||
<h3 class="text-sm font-bold text-zinc-800">邮箱消息通知</h3>
|
||
</div>
|
||
|
||
<div class="space-y-4">
|
||
<div v-if="userEmail" class="p-3 bg-zinc-50 border border-zinc-200 rounded-xl">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p
|
||
class="text-[10px] text-zinc-500 font-black uppercase tracking-widest mb-1"
|
||
>
|
||
当前绑定邮箱
|
||
</p>
|
||
<p class="text-sm font-medium text-zinc-800">{{ userEmail }}</p>
|
||
</div>
|
||
<div
|
||
:class="[
|
||
'px-2 py-0.5 rounded-full text-[10px] font-black uppercase tracking-wider',
|
||
emailVerified
|
||
? 'bg-emerald-500/10 text-emerald-600'
|
||
: 'bg-amber-500/10 text-amber-600'
|
||
]"
|
||
>
|
||
{{ emailVerified ? '已验证' : '待验证' }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 未绑定状态 -->
|
||
<div v-if="!userEmail" class="space-y-3">
|
||
<p class="text-xs text-zinc-500">绑定邮箱后,您可以接收到实时的邮件通知提醒。</p>
|
||
<div class="flex flex-col sm:flex-row gap-2">
|
||
<input
|
||
v-model="newEmail"
|
||
:disabled="bindingEmail"
|
||
type="email"
|
||
placeholder="请输入邮箱地址"
|
||
class="flex-1 bg-white border border-zinc-200 rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-indigo-500/30 w-full sm:w-auto"
|
||
>
|
||
<button
|
||
:disabled="!newEmail || bindingEmail"
|
||
class="px-4 py-2 bg-zinc-100 hover:bg-zinc-200 text-zinc-800 text-xs font-bold rounded-xl transition-all disabled:opacity-50 whitespace-nowrap"
|
||
@click="bindEmail"
|
||
>
|
||
{{ bindingEmail ? '请稍候...' : '立即绑定' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 待验证状态 -->
|
||
<div v-else-if="!emailVerified" class="space-y-4 pt-2">
|
||
<div
|
||
class="p-3 bg-indigo-500/5 border border-indigo-500/10 rounded-xl flex items-start gap-3"
|
||
>
|
||
<AlertCircle :size="14" class="text-indigo-600 shrink-0 mt-0.5" />
|
||
<p class="text-[11px] text-zinc-600 leading-relaxed">
|
||
验证码已发送至您的邮箱,请在 5 分钟内完成验证。若未收到邮件,请检查垃圾箱。
|
||
</p>
|
||
</div>
|
||
|
||
<div class="space-y-3">
|
||
<input
|
||
v-model="emailCode"
|
||
type="text"
|
||
maxlength="6"
|
||
placeholder="输入 6 位数字验证码"
|
||
:class="[
|
||
'w-full bg-white border rounded-xl px-4 py-3 text-lg font-black tracking-[0.5em] text-center focus:outline-none transition-all',
|
||
emailCodeError
|
||
? 'border-rose-500 shadow-[0_0_15px_rgba(244,63,94,0.1)]'
|
||
: 'border-zinc-200 focus:border-indigo-500/30'
|
||
]"
|
||
@input="handleEmailCodeInput"
|
||
@keydown="handleEmailCodeKeydown"
|
||
>
|
||
<div class="grid grid-cols-2 gap-2">
|
||
<button
|
||
:disabled="bindingEmail || emailCode.length !== 6"
|
||
class="px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-black rounded-xl transition-all disabled:opacity-50"
|
||
@click="verifyEmailCode"
|
||
>
|
||
{{ bindingEmail ? '验证中...' : '确认验证' }}
|
||
</button>
|
||
<button
|
||
:disabled="resendingEmail"
|
||
class="px-4 py-2.5 bg-zinc-100 hover:bg-zinc-200 text-zinc-700 text-xs font-bold rounded-xl transition-all disabled:opacity-50"
|
||
@click="resendVerificationEmail"
|
||
>
|
||
{{ resendingEmail ? '发送中...' : '重新发送' }}
|
||
</button>
|
||
</div>
|
||
<button
|
||
class="w-full py-2 text-zinc-500 hover:text-zinc-700 text-[10px] font-black uppercase tracking-widest transition-all"
|
||
@click="changeEmail"
|
||
>
|
||
更换邮箱地址
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 已验证状态 -->
|
||
<div v-else class="flex gap-2 pt-2">
|
||
<button
|
||
class="flex-1 py-2.5 bg-zinc-50 border border-zinc-200 hover:border-zinc-300 text-zinc-500 text-xs font-bold rounded-xl transition-all"
|
||
@click="changeEmail"
|
||
>
|
||
更换邮箱
|
||
</button>
|
||
<button
|
||
:disabled="unbindingEmail"
|
||
class="flex-1 py-2.5 bg-rose-500/10 border border-rose-500/20 hover:bg-rose-500/20 text-rose-600 text-xs font-black rounded-xl transition-all"
|
||
@click="unbindEmail"
|
||
>
|
||
{{ unbindingEmail ? '正在解绑...' : '解绑邮箱' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MeoW 账号绑定 -->
|
||
<div :class="[cardClass, 'border-indigo-500/20 bg-indigo-500/[0.02]']">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<div class="flex items-center gap-3">
|
||
<div class="p-2 bg-indigo-500/10 rounded-lg border border-indigo-500/20">
|
||
<Smartphone :size="16" class="text-indigo-600" />
|
||
</div>
|
||
<h3 class="text-sm font-bold text-zinc-800">MeoW App 推送</h3>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="space-y-4">
|
||
<div
|
||
v-if="localSettings.meowUserId"
|
||
class="p-3 bg-indigo-500/10 border border-indigo-500/20 rounded-xl"
|
||
>
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p
|
||
class="text-[10px] text-indigo-600/60 font-black uppercase tracking-widest mb-1"
|
||
>
|
||
当前绑定 ID
|
||
</p>
|
||
<p class="text-sm font-black text-indigo-600 tracking-tight">
|
||
{{ localSettings.meowUserId }}
|
||
</p>
|
||
</div>
|
||
<div
|
||
class="px-2 py-0.5 bg-indigo-600 text-white rounded-full text-[10px] font-black uppercase tracking-wider"
|
||
>
|
||
已连接
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 未绑定状态 -->
|
||
<div v-if="!localSettings.meowUserId" class="space-y-3">
|
||
<p class="text-xs text-zinc-500">
|
||
通过 MeoW 客户端接收更及时的系统通知和歌曲状态变更提醒。
|
||
</p>
|
||
|
||
<!-- 第一步:输入用户ID -->
|
||
<div v-if="!verificationSent" class="flex flex-col sm:flex-row gap-2">
|
||
<input
|
||
v-model="meowUserId"
|
||
:disabled="binding"
|
||
type="text"
|
||
placeholder="请输入 MeoW 用户 ID"
|
||
class="flex-1 bg-white border border-zinc-200 rounded-xl px-4 py-2 text-sm focus:outline-none focus:border-indigo-500/30 w-full sm:w-auto"
|
||
>
|
||
<button
|
||
:disabled="!meowUserId || binding"
|
||
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-black rounded-xl transition-all disabled:opacity-50 shadow-lg shadow-indigo-500/20 whitespace-nowrap"
|
||
@click="sendVerificationCode"
|
||
>
|
||
{{ binding ? '发送中...' : '发送验证码' }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 第二步:输入验证码 -->
|
||
<div v-else class="space-y-4">
|
||
<div
|
||
class="p-3 bg-indigo-500/5 border border-indigo-500/10 rounded-xl flex items-start gap-3"
|
||
>
|
||
<AlertCircle :size="14" class="text-indigo-600 shrink-0 mt-0.5" />
|
||
<p class="text-[11px] text-zinc-600 leading-relaxed">
|
||
验证码已发送至 MeoW ID:
|
||
<span class="font-bold text-zinc-800">{{ meowUserId }}</span
|
||
>,请在客户端查收。
|
||
</p>
|
||
</div>
|
||
|
||
<div class="space-y-3">
|
||
<input
|
||
v-model="verificationCode"
|
||
type="text"
|
||
maxlength="6"
|
||
placeholder="输入 6 位验证码"
|
||
:class="[
|
||
'w-full bg-white border rounded-xl px-4 py-3 text-lg font-black tracking-[0.5em] text-center focus:outline-none transition-all',
|
||
verificationCodeError
|
||
? 'border-rose-500 shadow-[0_0_15px_rgba(244,63,94,0.1)]'
|
||
: 'border-zinc-200 focus:border-indigo-500/30'
|
||
]"
|
||
@input="handleVerificationCodeInput"
|
||
@keydown="handleVerificationCodeKeydown"
|
||
>
|
||
<div class="grid grid-cols-2 gap-2">
|
||
<button
|
||
:disabled="binding || verificationCode.length !== 6"
|
||
class="px-4 py-2.5 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-black rounded-xl transition-all disabled:opacity-50"
|
||
@click="verifyAndBind"
|
||
>
|
||
{{ binding ? '验证中...' : '确认绑定' }}
|
||
</button>
|
||
<button
|
||
:disabled="binding"
|
||
class="px-4 py-2.5 bg-zinc-100 hover:bg-zinc-200 text-zinc-700 text-xs font-bold rounded-xl transition-all disabled:opacity-50"
|
||
@click="cancelVerification"
|
||
>
|
||
取消
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 已绑定状态 -->
|
||
<div v-else class="pt-2">
|
||
<button
|
||
class="w-full py-2.5 bg-rose-500/10 border border-rose-500/20 hover:bg-rose-500/20 text-rose-600 text-xs font-black rounded-xl transition-all"
|
||
@click="showUnbindConfirm"
|
||
>
|
||
解除 MeoW 账号绑定
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 确认对话框 -->
|
||
<ConfirmDialog
|
||
v-model:show="showConfirmDialog"
|
||
:loading="confirmDialog.loading"
|
||
:message="confirmDialog.message"
|
||
:title="confirmDialog.title"
|
||
:type="confirmDialog.type"
|
||
@cancel="confirmDialog.onCancel"
|
||
@confirm="confirmDialog.onConfirm"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { nextTick, onMounted, ref } from 'vue'
|
||
import {
|
||
Bell,
|
||
Mail,
|
||
Link,
|
||
MessageSquare,
|
||
Clock,
|
||
ArrowLeft,
|
||
Save,
|
||
Shield,
|
||
Trash2,
|
||
User,
|
||
ExternalLink,
|
||
CheckCircle2,
|
||
AlertCircle,
|
||
Loader2,
|
||
Smartphone
|
||
} from '@lucide/vue'
|
||
import ConfirmDialog from '~/components/UI/ConfirmDialog.vue'
|
||
import { useSiteConfig } from '~/composables/useSiteConfig'
|
||
import { useToast } from '~/composables/useToast'
|
||
|
||
const { siteTitle, smtpEnabled, initSiteConfig } = useSiteConfig()
|
||
const { showToast } = useToast()
|
||
|
||
// 样式类常量
|
||
const sectionClass = 'bg-white border border-zinc-200 rounded-3xl p-6 md:p-8 shadow-lg'
|
||
const cardClass =
|
||
'bg-zinc-50 border border-zinc-200 rounded-2xl p-5 transition-all hover:border-zinc-300'
|
||
const itemClass =
|
||
'flex items-center justify-between p-4 bg-white border border-zinc-200 rounded-2xl hover:bg-zinc-50 transition-all group'
|
||
|
||
// 页面状态
|
||
const loading = ref(true)
|
||
const saving = ref(false)
|
||
const binding = ref(false)
|
||
|
||
// 通知设置
|
||
const localSettings = ref({
|
||
songSelectedNotify: true,
|
||
songPlayedNotify: true,
|
||
songVotedNotify: true,
|
||
songVotedThreshold: 5,
|
||
systemNotify: true,
|
||
refreshInterval: 60,
|
||
meowUserId: ''
|
||
})
|
||
|
||
// MeoW 绑定相关
|
||
const meowUserId = ref('')
|
||
const verificationSent = ref(false)
|
||
const verificationCode = ref('')
|
||
const verificationCodeError = ref(false)
|
||
|
||
// 邮箱绑定相关
|
||
const userEmail = ref('')
|
||
const emailVerified = ref(false)
|
||
const newEmail = ref('')
|
||
const bindingEmail = ref(false)
|
||
const resendingEmail = ref(false)
|
||
const unbindingEmail = ref(false)
|
||
const emailCode = ref('')
|
||
const emailCodeError = ref(false)
|
||
|
||
// 确认对话框相关
|
||
const showConfirmDialog = ref(false)
|
||
const confirmDialog = ref({
|
||
title: '',
|
||
message: '',
|
||
type: 'warning',
|
||
loading: false,
|
||
onConfirm: () => {},
|
||
onCancel: () => {}
|
||
})
|
||
|
||
// 通知显示函数
|
||
const showNotification = (message, type = 'info') => {
|
||
showToast(message, type)
|
||
}
|
||
|
||
// 返回主页
|
||
const goBack = () => {
|
||
navigateTo('/')
|
||
}
|
||
|
||
// 页面初始化
|
||
onMounted(async () => {
|
||
await initSiteConfig()
|
||
|
||
// 设置页面标题
|
||
if (typeof document !== 'undefined' && siteTitle.value) {
|
||
document.title = `通知设置 | ${siteTitle.value}`
|
||
}
|
||
|
||
await loadSettings()
|
||
})
|
||
|
||
// 处理验证码输入
|
||
const handleVerificationCodeInput = (event) => {
|
||
const value = event.target.value.replace(/[^0-9]/g, '')
|
||
verificationCode.value = value
|
||
if (verificationCodeError.value) {
|
||
verificationCodeError.value = false
|
||
}
|
||
}
|
||
|
||
// 处理验证码输入键盘事件
|
||
const handleVerificationCodeKeydown = (event) => {
|
||
if (event.key === 'Enter' && verificationCode.value.length === 6) {
|
||
verifyAndBind()
|
||
}
|
||
}
|
||
|
||
// 加载设置
|
||
const loadSettings = async () => {
|
||
try {
|
||
loading.value = true
|
||
const response = await $fetch('/api/notifications/settings')
|
||
|
||
if (response.success) {
|
||
localSettings.value = {
|
||
songSelectedNotify: response.data.songSelectedNotify || false,
|
||
songPlayedNotify: response.data.songPlayedNotify || false,
|
||
songVotedNotify: response.data.songVotedNotify || false,
|
||
songVotedThreshold: response.data.songVotedThreshold || 5,
|
||
systemNotify: response.data.systemNotify || true,
|
||
refreshInterval: response.data.refreshInterval || 60,
|
||
meowUserId: response.data.meowUserId || ''
|
||
}
|
||
|
||
userEmail.value = response.data.userEmail || ''
|
||
emailVerified.value = response.data.emailVerified || false
|
||
}
|
||
} catch (err) {
|
||
console.error('加载设置失败:', err)
|
||
showNotification('加载设置失败,请刷新页面重试', 'error')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 发送验证码
|
||
const sendVerificationCode = async () => {
|
||
if (!meowUserId.value.trim()) {
|
||
showNotification('请输入 MeoW 用户 ID', 'error')
|
||
return
|
||
}
|
||
|
||
try {
|
||
binding.value = true
|
||
const response = await $fetch('/api/meow/bind', {
|
||
method: 'POST',
|
||
body: {
|
||
action: 'send_verification',
|
||
meowId: meowUserId.value.trim()
|
||
}
|
||
})
|
||
|
||
if (response.success) {
|
||
verificationSent.value = true
|
||
showNotification('验证码已发送到您的 MeoW 账号', 'success')
|
||
} else {
|
||
showNotification(response.message || '发送验证码失败', 'error')
|
||
}
|
||
} catch (err) {
|
||
showNotification(err.data?.message || '发送验证码失败', 'error')
|
||
} finally {
|
||
binding.value = false
|
||
}
|
||
}
|
||
|
||
// 验证并绑定
|
||
const verifyAndBind = async () => {
|
||
if (!verificationCode.value || verificationCode.value.length !== 6) {
|
||
showNotification('请输入6位验证码', 'error')
|
||
verificationCodeError.value = true
|
||
return
|
||
}
|
||
|
||
try {
|
||
binding.value = true
|
||
const response = await $fetch('/api/meow/bind', {
|
||
method: 'POST',
|
||
body: {
|
||
action: 'verify_and_bind',
|
||
meowId: meowUserId.value.trim(),
|
||
verificationCode: verificationCode.value
|
||
}
|
||
})
|
||
|
||
if (response.success) {
|
||
localSettings.value.meowUserId = meowUserId.value.trim()
|
||
meowUserId.value = ''
|
||
verificationCode.value = ''
|
||
verificationSent.value = false
|
||
showNotification('MeoW 账号绑定成功!', 'success')
|
||
} else {
|
||
showNotification(response.message || '验证失败', 'error')
|
||
verificationCodeError.value = true
|
||
}
|
||
} catch (err) {
|
||
showNotification(err.data?.message || '验证失败', 'error')
|
||
verificationCodeError.value = true
|
||
} finally {
|
||
binding.value = false
|
||
}
|
||
}
|
||
|
||
// 取消验证
|
||
const cancelVerification = () => {
|
||
verificationSent.value = false
|
||
verificationCode.value = ''
|
||
meowUserId.value = ''
|
||
}
|
||
|
||
// 显示解绑确认对话框
|
||
const showUnbindConfirm = () => {
|
||
confirmDialog.value = {
|
||
title: '解绑 MeoW 账号',
|
||
message: '确定要解绑 MeoW 账号吗?解绑后将无法接收推送通知。',
|
||
type: 'danger',
|
||
loading: false,
|
||
onConfirm: performUnbind,
|
||
onCancel: () => {
|
||
showConfirmDialog.value = false
|
||
}
|
||
}
|
||
showConfirmDialog.value = true
|
||
}
|
||
|
||
// 执行解绑操作
|
||
const performUnbind = async () => {
|
||
try {
|
||
confirmDialog.value.loading = true
|
||
const response = await $fetch('/api/meow/unbind', { method: 'POST' })
|
||
|
||
if (response.success) {
|
||
localSettings.value.meowUserId = ''
|
||
showNotification('MeoW 账号已解绑', 'success')
|
||
showConfirmDialog.value = false
|
||
} else {
|
||
showNotification(response.message || '解绑失败', 'error')
|
||
}
|
||
} catch (err) {
|
||
showNotification(err.data?.message || '解绑失败', 'error')
|
||
} finally {
|
||
confirmDialog.value.loading = false
|
||
}
|
||
}
|
||
|
||
// 邮箱绑定相关方法
|
||
const bindEmail = async () => {
|
||
if (!newEmail.value) {
|
||
showNotification('请输入邮箱地址', 'error')
|
||
return
|
||
}
|
||
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||
if (!emailRegex.test(newEmail.value)) {
|
||
showNotification('请输入有效的邮箱地址', 'error')
|
||
return
|
||
}
|
||
|
||
bindingEmail.value = true
|
||
try {
|
||
const response = await $fetch('/api/user/email/bind', {
|
||
method: 'POST',
|
||
body: { email: newEmail.value }
|
||
})
|
||
|
||
if (response.success) {
|
||
userEmail.value = newEmail.value
|
||
emailVerified.value = false
|
||
newEmail.value = ''
|
||
showNotification('验证码已发送,请查收邮箱', 'success')
|
||
} else {
|
||
showNotification(response.message || '绑定失败', 'error')
|
||
}
|
||
} catch (err) {
|
||
showNotification(err.data?.message || '绑定失败', 'error')
|
||
} finally {
|
||
bindingEmail.value = false
|
||
}
|
||
}
|
||
|
||
const handleEmailCodeInput = (event) => {
|
||
const value = event.target.value.replace(/[^0-9]/g, '')
|
||
emailCode.value = value
|
||
if (emailCodeError.value) emailCodeError.value = false
|
||
}
|
||
|
||
const handleEmailCodeKeydown = (event) => {
|
||
if (event.key === 'Enter' && emailCode.value.length === 6) verifyEmailCode()
|
||
}
|
||
|
||
const verifyEmailCode = async () => {
|
||
if (emailCode.value.length !== 6) {
|
||
emailCodeError.value = true
|
||
showNotification('请输入6位验证码', 'error')
|
||
return
|
||
}
|
||
try {
|
||
bindingEmail.value = true
|
||
const response = await $fetch('/api/user/email/verify-code', {
|
||
method: 'POST',
|
||
body: { email: userEmail.value, code: emailCode.value }
|
||
})
|
||
if (response.success) {
|
||
emailVerified.value = true
|
||
emailCode.value = ''
|
||
showNotification('邮箱验证成功', 'success')
|
||
} else {
|
||
emailCodeError.value = true
|
||
showNotification(response.message || '验证失败', 'error')
|
||
}
|
||
} catch (err) {
|
||
emailCodeError.value = true
|
||
showNotification(err.data?.message || '验证失败', 'error')
|
||
} finally {
|
||
bindingEmail.value = false
|
||
}
|
||
}
|
||
|
||
const changeEmail = () => {
|
||
confirmDialog.value = {
|
||
title: '更换邮箱',
|
||
message: '更换邮箱将清除当前绑定的邮箱信息,需要重新验证新邮箱。确定要继续吗?',
|
||
type: 'warning',
|
||
loading: false,
|
||
onConfirm: performChangeEmail,
|
||
onCancel: () => {
|
||
showConfirmDialog.value = false
|
||
}
|
||
}
|
||
showConfirmDialog.value = true
|
||
}
|
||
|
||
const performChangeEmail = () => {
|
||
userEmail.value = ''
|
||
emailVerified.value = false
|
||
newEmail.value = ''
|
||
emailCode.value = ''
|
||
emailCodeError.value = false
|
||
showConfirmDialog.value = false
|
||
showNotification('已清除邮箱信息', 'info')
|
||
}
|
||
|
||
const resendVerificationEmail = async () => {
|
||
try {
|
||
resendingEmail.value = true
|
||
const response = await $fetch('/api/user/email/resend-verification', { method: 'POST' })
|
||
if (response.success) {
|
||
emailCode.value = ''
|
||
emailCodeError.value = false
|
||
showNotification('验证码已重新发送', 'success')
|
||
} else {
|
||
showNotification(response.message || '发送失败', 'error')
|
||
}
|
||
} catch (err) {
|
||
showNotification(err.data?.message || '发送失败', 'error')
|
||
} finally {
|
||
resendingEmail.value = false
|
||
}
|
||
}
|
||
|
||
const unbindEmail = async () => {
|
||
confirmDialog.value = {
|
||
title: '确认解绑邮箱',
|
||
message: '解绑后将无法接收邮件通知,确定要继续吗?',
|
||
type: 'warning',
|
||
loading: false,
|
||
onConfirm: performEmailUnbind,
|
||
onCancel: () => {
|
||
showConfirmDialog.value = false
|
||
}
|
||
}
|
||
showConfirmDialog.value = true
|
||
}
|
||
|
||
const performEmailUnbind = async () => {
|
||
try {
|
||
confirmDialog.value.loading = true
|
||
const response = await $fetch('/api/user/email/unbind', { method: 'POST' })
|
||
if (response.success) {
|
||
userEmail.value = ''
|
||
emailVerified.value = false
|
||
showNotification('邮箱已解绑', 'success')
|
||
showConfirmDialog.value = false
|
||
} else {
|
||
showNotification(response.message || '解绑失败', 'error')
|
||
}
|
||
} catch (err) {
|
||
showNotification(err.data?.message || '解绑失败', 'error')
|
||
} finally {
|
||
confirmDialog.value.loading = false
|
||
}
|
||
}
|
||
|
||
// 保存设置
|
||
const saveSettings = async () => {
|
||
try {
|
||
saving.value = true
|
||
const response = await $fetch('/api/notifications/settings', {
|
||
method: 'POST',
|
||
body: localSettings.value
|
||
})
|
||
if (response.success) {
|
||
showNotification('设置保存成功', 'success')
|
||
} else {
|
||
showNotification(response.message || '保存失败', 'error')
|
||
}
|
||
} catch (err) {
|
||
showNotification(err.data?.message || '保存失败', 'error')
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
input[type='number']::-webkit-inner-spin-button,
|
||
input[type='number']::-webkit-outer-spin-button {
|
||
-webkit-appearance: none;
|
||
margin: 0;
|
||
}
|
||
input[type='number'] {
|
||
-moz-appearance: textfield;
|
||
}
|
||
|
||
@keyframes shake {
|
||
0%,
|
||
100% {
|
||
transform: translateX(0);
|
||
}
|
||
25% {
|
||
transform: translateX(-4px);
|
||
}
|
||
75% {
|
||
transform: translateX(4px);
|
||
}
|
||
}
|
||
</style>
|