feat: align with upstream 1.5.9.9

This commit is contained in:
2026-09-12 18:21:11 +08:00
commit 34f0679117
805 changed files with 376275 additions and 0 deletions
+627
View File
@@ -0,0 +1,627 @@
<template>
<div :class="cardClass">
<div class="flex items-center gap-3 pb-5 mb-6">
<div class="p-2.5 bg-primary-10 rounded-xl flex items-center justify-center">
<Share2 :size="20" class="text-primary" />
</div>
<div>
<h2 class="text-base font-black text-text-primary">{{ locale.title }}</h2>
<p class="text-xs text-text-tertiary mt-0.5">{{ locale.desc }}</p>
</div>
</div>
<div v-if="loading" class="flex items-center justify-center gap-2 py-8 text-xs text-text-tertiary">
<Loader2 :size="16" class="animate-spin" />
<span>{{ locale.loading }}</span>
</div>
<div v-else class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- 邮箱绑定 -->
<div v-if="smtpEnabled">
<div class="flex items-center gap-3 mb-4">
<div class="p-2 bg-bg-tertiary rounded-lg border border-border-secondary flex items-center justify-center">
<Mail :size="16" class="text-text-secondary" />
</div>
<h3 class="text-sm font-bold text-text-primary">{{ locale.emailNotifyTitle }}</h3>
</div>
<div class="space-y-4">
<div v-if="userEmail" class="p-3 bg-bg-secondary-20 border border-border-secondary rounded-xl">
<div class="flex items-center justify-between">
<div>
<p
class="text-[10px] text-text-disabled font-black uppercase tracking-widest mb-1"
>
{{ locale.currentEmail }}
</p>
<p class="text-sm font-medium text-text-primary">{{ userEmail }}</p>
</div>
<div
:class="[
'px-2 py-0.5 rounded-full text-[10px] font-black uppercase tracking-wider',
emailVerified
? 'bg-success-10 text-success'
: 'bg-warning-10 text-warning'
]"
>
{{ emailVerified ? locale.verified : locale.pendingVerify }}
</div>
</div>
</div>
<!-- 未绑定状态 -->
<div v-if="!userEmail" class="space-y-3">
<p class="text-xs text-text-tertiary">{{ locale.emailUnboundDesc }}</p>
<div class="flex flex-col sm:flex-row gap-2">
<input
v-model="newEmail"
:disabled="bindingEmail"
type="email"
:placeholder="locale.emailPlaceholder"
class="flex-1 bg-bg-primary border border-border-secondary rounded-xl px-4 py-2 text-sm text-text-primary placeholder:text-text-tertiary focus:outline-none focus:border-primary-30 w-full sm:w-auto"
>
<button
:disabled="!newEmail || bindingEmail"
class="px-4 py-2 bg-bg-tertiary hover:bg-bg-quaternary text-text-primary text-xs font-bold rounded-xl transition-all disabled:opacity-50 whitespace-nowrap"
@click="bindEmail"
>
{{ bindingEmail ? locale.pleaseWait : locale.bindNow }}
</button>
</div>
</div>
<!-- 待验证状态 -->
<div v-else-if="!emailVerified" class="space-y-4 pt-2">
<div
class="p-3 bg-primary-5 border border-primary-10 rounded-xl flex items-start gap-3"
>
<AlertCircle :size="14" class="text-primary shrink-0 mt-0.5" />
<p class="text-[11px] text-text-tertiary leading-relaxed">
{{ locale.emailCodeSentTip }}
</p>
</div>
<div class="space-y-3">
<input
v-model="emailCode"
type="text"
maxlength="6"
:placeholder="locale.emailCodePlaceholder"
:class="[
'w-full bg-bg-primary border rounded-xl px-4 py-3 text-lg font-black tracking-[0.5em] text-center text-text-primary placeholder:text-text-tertiary focus:outline-none transition-all',
emailCodeError
? 'border-error shadow-[0_0_15px_rgba(244,63,94,0.1)]'
: 'border-border-secondary focus:border-primary-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-primary hover:bg-primary-hover text-text-primary text-xs font-black rounded-xl transition-all disabled:opacity-50"
@click="verifyEmailCode"
>
{{ bindingEmail ? locale.verifying : locale.confirmVerify }}
</button>
<button
:disabled="resendingEmail"
class="px-4 py-2.5 bg-bg-tertiary hover:bg-bg-quaternary text-text-primary text-xs font-bold rounded-xl transition-all disabled:opacity-50"
@click="resendVerificationEmail"
>
{{ resendingEmail ? locale.sending : locale.resend }}
</button>
</div>
<button
class="w-full py-2 text-text-tertiary hover:text-text-secondary text-[10px] font-black uppercase tracking-widest transition-all"
@click="changeEmail"
>
{{ locale.changeEmailAddress }}
</button>
</div>
</div>
<!-- 已验证状态 -->
<div v-else class="flex gap-2 pt-2">
<button
class="flex-1 py-2.5 bg-bg-tertiary border border-border-secondary hover:border-border-tertiary text-text-secondary text-xs font-bold rounded-xl transition-all"
@click="changeEmail"
>
{{ locale.changeEmail }}
</button>
<button
:disabled="unbindingEmail"
class="flex-1 py-2.5 bg-error-10 border border-error-20 hover:bg-error-20 text-error text-xs font-black rounded-xl transition-all"
@click="unbindEmail"
>
{{ unbindingEmail ? locale.unbinding : locale.unbindEmail }}
</button>
</div>
</div>
</div>
<!-- MeoW 账号绑定 -->
<div :class="meowCardClass">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center gap-3">
<div class="p-2 bg-primary-10 rounded-lg border border-primary-20 flex items-center justify-center">
<Smartphone :size="16" class="text-primary" />
</div>
<h3 class="text-sm font-bold text-text-primary">{{ locale.meowPushTitle }}</h3>
</div>
</div>
<div class="space-y-4">
<div
v-if="meowUserIdBound"
class="p-3 bg-primary-10 border border-primary-20 rounded-xl"
>
<div class="flex items-center justify-between">
<div>
<p
class="text-[10px] text-text-disabled font-black uppercase tracking-widest mb-1"
>
{{ locale.currentBoundId }}
</p>
<p class="text-sm font-black text-primary tracking-tight">
{{ meowUserIdBound }}
</p>
</div>
<div
class="px-2 py-0.5 bg-primary text-white rounded-full text-[10px] font-black uppercase tracking-wider"
>
{{ locale.connected }}
</div>
</div>
</div>
<!-- 未绑定状态 -->
<div v-if="!meowUserIdBound" class="space-y-3">
<p class="text-xs text-text-tertiary">
{{ locale.meowDesc }}
</p>
<!-- 第一步:输入用户ID -->
<div v-if="!verificationSent" class="flex flex-col sm:flex-row gap-2">
<input
v-model="meowUserId"
:disabled="binding"
type="text"
:placeholder="locale.meowIdPlaceholder"
class="flex-1 bg-bg-primary border border-border-secondary rounded-xl px-4 py-2 text-sm text-text-primary placeholder:text-text-tertiary focus:outline-none focus:border-primary-30 w-full sm:w-auto"
>
<button
:disabled="!meowUserId || binding"
class="px-4 py-2 bg-primary hover:bg-primary-hover text-text-primary text-xs font-black rounded-xl transition-all disabled:opacity-50 shadow-lg shadow-[var(--primary-glow)] whitespace-nowrap"
@click="sendVerificationCode"
>
{{ binding ? locale.sending : locale.sendCode }}
</button>
</div>
<!-- 第二步:输入验证码 -->
<div v-else class="space-y-4">
<div
class="p-3 bg-primary-5 border border-primary-10 rounded-xl flex items-start gap-3"
>
<AlertCircle :size="14" class="text-primary shrink-0 mt-0.5" />
<p class="text-[11px] text-text-tertiary leading-relaxed">
{{ locale.meowCodeSentPrefix }}
<span class="font-bold text-text-primary">{{ meowUserId }}</span
>{{ locale.meowCodeSentSuffix }}
</p>
</div>
<div class="space-y-3">
<input
v-model="verificationCode"
type="text"
maxlength="6"
:placeholder="locale.codePlaceholder"
:class="[
'w-full bg-bg-primary border rounded-xl px-4 py-3 text-lg font-black tracking-[0.5em] text-center text-text-primary placeholder:text-text-tertiary focus:outline-none transition-all',
verificationCodeError
? 'border-error shadow-[0_0_15px_rgba(244,63,94,0.1)]'
: 'border-border-secondary focus:border-primary-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-primary hover:bg-primary-hover text-text-primary text-xs font-black rounded-xl transition-all disabled:opacity-50"
@click="verifyAndBind"
>
{{ binding ? locale.verifying : locale.confirmBind }}
</button>
<button
:disabled="binding"
class="px-4 py-2.5 bg-bg-tertiary hover:bg-bg-quaternary text-text-primary text-xs font-bold rounded-xl transition-all disabled:opacity-50"
@click="cancelVerification"
>
{{ locale.cancel }}
</button>
</div>
</div>
</div>
</div>
<!-- 已绑定状态 -->
<div v-else class="pt-2">
<button
class="w-full py-2.5 bg-error-10 border border-error-20 hover:bg-error-20 text-error text-xs font-black rounded-xl transition-all"
@click="showUnbindConfirm"
>
{{ locale.unbindMeow }}
</button>
</div>
</div>
</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 { computed, onMounted, ref } from 'vue'
import { AlertCircle, Loader2, Mail, Share2, Smartphone } from '@lucide/vue'
import ConfirmDialog from '~/components/UI/ConfirmDialog.vue'
import { useSiteConfig } from '~/composables/useSiteConfig'
import { useToast } from '~/composables/useToast'
import { useLocale } from '~/utils/locale'
const { smtpEnabled } = useSiteConfig()
const { showToast } = useToast()
const { pages } = useLocale()
const locale = computed(() => pages.value?.account?.social || {})
// 卡片样式类常量(sectionClass 由父组件 account/index.vue 控制)
const cardClass =
'rounded-2xl border border-border-secondary bg-bg-secondary-30 p-5 shadow-lg shadow-[0_10px_15px_var(--shadow-color)] transition-all hover:border-border-tertiary'
// MeoW 卡片(蓝紫色调,与邮箱卡片做视觉区分)
const meowCardClass =
'rounded-2xl border border-primary-20 bg-primary-5 p-5 shadow-lg shadow-[0_10px_15px_var(--shadow-color)] transition-all hover:border-border-tertiary'
// 页面状态
const loading = ref(true)
const binding = ref(false)
// 已绑定的 MeoW 用户 ID
const meowUserIdBound = ref('')
// 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: () => {}
})
onMounted(() => {
loadBindings()
})
// 加载绑定状态
const loadBindings = async () => {
try {
loading.value = true
const response = await $fetch('/api/notifications/settings')
if (response.success) {
meowUserIdBound.value = response.data.meowUserId || ''
userEmail.value = response.data.userEmail || ''
emailVerified.value = response.data.emailVerified || false
}
} catch (err) {
console.error(locale.value.loadFailedLog, err)
showToast(locale.value.loadFailed, 'error')
} finally {
loading.value = false
}
}
// 处理验证码输入
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 sendVerificationCode = async () => {
if (!meowUserId.value.trim()) {
showToast(locale.value.meowIdRequired, '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
showToast(locale.value.meowCodeSent, 'success')
} else {
showToast(response.message || locale.value.sendCodeFailed, 'error')
}
} catch (err) {
showToast(err.data?.message || locale.value.sendCodeFailed, 'error')
} finally {
binding.value = false
}
}
// 验证并绑定
const verifyAndBind = async () => {
if (!verificationCode.value || verificationCode.value.length !== 6) {
showToast(locale.value.codeRequired, '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) {
meowUserIdBound.value = meowUserId.value.trim()
meowUserId.value = ''
verificationCode.value = ''
verificationSent.value = false
showToast(locale.value.meowBindSuccess, 'success')
} else {
showToast(response.message || locale.value.verifyFailed, 'error')
verificationCodeError.value = true
}
} catch (err) {
showToast(err.data?.message || locale.value.verifyFailed, 'error')
verificationCodeError.value = true
} finally {
binding.value = false
}
}
// 取消验证
const cancelVerification = () => {
verificationSent.value = false
verificationCode.value = ''
meowUserId.value = ''
}
// 显示解绑确认对话框
const showUnbindConfirm = () => {
confirmDialog.value = {
title: locale.value.unbindMeowTitle,
message: locale.value.unbindMeowMessage,
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) {
meowUserIdBound.value = ''
showToast(locale.value.meowUnbound, 'success')
showConfirmDialog.value = false
} else {
showToast(response.message || locale.value.unbindFailed, 'error')
}
} catch (err) {
showToast(err.data?.message || locale.value.unbindFailed, 'error')
} finally {
confirmDialog.value.loading = false
}
}
// 邮箱绑定相关方法
const bindEmail = async () => {
if (!newEmail.value) {
showToast(locale.value.emailRequired, 'error')
return
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(newEmail.value)) {
showToast(locale.value.emailInvalid, '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 = ''
showToast(locale.value.emailCodeSent, 'success')
} else {
showToast(response.message || locale.value.bindFailed, 'error')
}
} catch (err) {
showToast(err.data?.message || locale.value.bindFailed, '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
showToast(locale.value.codeRequired, '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 = ''
showToast(locale.value.emailVerifySuccess, 'success')
} else {
emailCodeError.value = true
showToast(response.message || locale.value.verifyFailed, 'error')
}
} catch (err) {
emailCodeError.value = true
showToast(err.data?.message || locale.value.verifyFailed, 'error')
} finally {
bindingEmail.value = false
}
}
const changeEmail = () => {
confirmDialog.value = {
title: locale.value.changeEmailTitle,
message: locale.value.changeEmailMessage,
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
showToast(locale.value.emailCleared, '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
showToast(locale.value.emailCodeResent, 'success')
} else {
showToast(response.message || locale.value.sendFailed, 'error')
}
} catch (err) {
showToast(err.data?.message || locale.value.sendFailed, 'error')
} finally {
resendingEmail.value = false
}
}
const unbindEmail = async () => {
confirmDialog.value = {
title: locale.value.unbindEmailTitle,
message: locale.value.unbindEmailMessage,
type: 'warning',
loading: false,
onConfirm: performEmailUnbind,
onCancel: () => {
showConfirmDialog.value = false
}
}
showConfirmDialog.value = true
}
const performEmailUnbind = async () => {
try {
confirmDialog.value.loading = true
unbindingEmail.value = true
const response = await $fetch('/api/user/email/unbind', { method: 'POST' })
if (response.success) {
userEmail.value = ''
emailVerified.value = false
showToast(locale.value.emailUnbound, 'success')
showConfirmDialog.value = false
} else {
showToast(response.message || locale.value.unbindFailed, 'error')
}
} catch (err) {
showToast(err.data?.message || locale.value.unbindFailed, 'error')
} finally {
confirmDialog.value.loading = false
unbindingEmail.value = false
}
}
</script>