Commit 81287e96 authored by erio's avatar erio
Browse files

feat(notify): improve balance notify card UX

- Show system default threshold as placeholder in custom threshold input
- Display user's primary email with "Primary" badge
- Support adding multiple pending emails before verification
- Each pending email has independent send/verify/resend flow
- Expose balance_low_notify_threshold in PublicSettings API
- Clean up timers on unmount to prevent leaks
parent 79d154ed
...@@ -193,6 +193,7 @@ type PublicSettings struct { ...@@ -193,6 +193,7 @@ type PublicSettings struct {
Version string `json:"version"` Version string `json:"version"`
BalanceLowNotifyEnabled bool `json:"balance_low_notify_enabled"` BalanceLowNotifyEnabled bool `json:"balance_low_notify_enabled"`
AccountQuotaNotifyEnabled bool `json:"account_quota_notify_enabled"` AccountQuotaNotifyEnabled bool `json:"account_quota_notify_enabled"`
BalanceLowNotifyThreshold float64 `json:"balance_low_notify_threshold"`
} }
// OverloadCooldownSettings 529过载冷却配置 DTO // OverloadCooldownSettings 529过载冷却配置 DTO
......
...@@ -63,5 +63,6 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) { ...@@ -63,5 +63,6 @@ func (h *SettingHandler) GetPublicSettings(c *gin.Context) {
Version: h.version, Version: h.version,
BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled, BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled,
AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled, AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled,
BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold,
}) })
} }
...@@ -183,6 +183,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings ...@@ -183,6 +183,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
SettingKeyOIDCConnectEnabled, SettingKeyOIDCConnectEnabled,
SettingKeyOIDCConnectProviderName, SettingKeyOIDCConnectProviderName,
SettingKeyBalanceLowNotifyEnabled, SettingKeyBalanceLowNotifyEnabled,
SettingKeyBalanceLowNotifyThreshold,
SettingKeyAccountQuotaNotifyEnabled, SettingKeyAccountQuotaNotifyEnabled,
} }
...@@ -222,6 +223,11 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings ...@@ -222,6 +223,11 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
settings[SettingKeyTablePageSizeOptions], settings[SettingKeyTablePageSizeOptions],
) )
var balanceLowNotifyThreshold float64
if v, err := strconv.ParseFloat(settings[SettingKeyBalanceLowNotifyThreshold], 64); err == nil && v >= 0 {
balanceLowNotifyThreshold = v
}
return &PublicSettings{ return &PublicSettings{
RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true", RegistrationEnabled: settings[SettingKeyRegistrationEnabled] == "true",
EmailVerifyEnabled: emailVerifyEnabled, EmailVerifyEnabled: emailVerifyEnabled,
...@@ -253,6 +259,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings ...@@ -253,6 +259,7 @@ func (s *SettingService) GetPublicSettings(ctx context.Context) (*PublicSettings
OIDCOAuthProviderName: oidcProviderName, OIDCOAuthProviderName: oidcProviderName,
BalanceLowNotifyEnabled: settings[SettingKeyBalanceLowNotifyEnabled] == "true", BalanceLowNotifyEnabled: settings[SettingKeyBalanceLowNotifyEnabled] == "true",
AccountQuotaNotifyEnabled: settings[SettingKeyAccountQuotaNotifyEnabled] == "true", AccountQuotaNotifyEnabled: settings[SettingKeyAccountQuotaNotifyEnabled] == "true",
BalanceLowNotifyThreshold: balanceLowNotifyThreshold,
}, nil }, nil
} }
...@@ -308,6 +315,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any ...@@ -308,6 +315,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
BalanceLowNotifyEnabled bool `json:"balance_low_notify_enabled"` BalanceLowNotifyEnabled bool `json:"balance_low_notify_enabled"`
AccountQuotaNotifyEnabled bool `json:"account_quota_notify_enabled"` AccountQuotaNotifyEnabled bool `json:"account_quota_notify_enabled"`
BalanceLowNotifyThreshold float64 `json:"balance_low_notify_threshold"`
}{ }{
RegistrationEnabled: settings.RegistrationEnabled, RegistrationEnabled: settings.RegistrationEnabled,
EmailVerifyEnabled: settings.EmailVerifyEnabled, EmailVerifyEnabled: settings.EmailVerifyEnabled,
...@@ -340,6 +348,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any ...@@ -340,6 +348,7 @@ func (s *SettingService) GetPublicSettingsForInjection(ctx context.Context) (any
Version: s.version, Version: s.version,
BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled, BalanceLowNotifyEnabled: settings.BalanceLowNotifyEnabled,
AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled, AccountQuotaNotifyEnabled: settings.AccountQuotaNotifyEnabled,
BalanceLowNotifyThreshold: settings.BalanceLowNotifyThreshold,
}, nil }, nil
} }
......
...@@ -154,8 +154,9 @@ type PublicSettings struct { ...@@ -154,8 +154,9 @@ type PublicSettings struct {
OIDCOAuthProviderName string OIDCOAuthProviderName string
Version string Version string
BalanceLowNotifyEnabled bool BalanceLowNotifyEnabled bool
AccountQuotaNotifyEnabled bool AccountQuotaNotifyEnabled bool
BalanceLowNotifyThreshold float64
} }
// StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制) // StreamTimeoutSettings 流超时处理配置(仅控制超时后的处理方式,超时判定由网关配置控制)
......
...@@ -4,90 +4,113 @@ ...@@ -4,90 +4,113 @@
<h2 class="text-lg font-medium text-gray-900 dark:text-white"> <h2 class="text-lg font-medium text-gray-900 dark:text-white">
{{ t('profile.balanceNotify.title') }} {{ t('profile.balanceNotify.title') }}
</h2> </h2>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{{ t('profile.balanceNotify.description') }}
</p>
</div> </div>
<div class="px-6 py-6 space-y-6"> <div class="px-6 py-6 space-y-6">
<!-- Enable toggle --> <!-- Enable toggle -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <label class="input-label mb-0">{{ t('profile.balanceNotify.enabled') }}</label>
<label class="input-label">{{ t('profile.balanceNotify.enabled') }}</label>
</div>
<label class="relative inline-flex items-center cursor-pointer"> <label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" v-model="notifyEnabled" @change="handleToggle" class="sr-only peer" /> <input type="checkbox" v-model="notifyEnabled" @change="handleToggle" class="sr-only peer" />
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:after:border-gray-600 peer-checked:bg-primary-600"></div> <div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 dark:peer-focus:ring-primary-800 rounded-full peer dark:bg-gray-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:after:border-gray-600 peer-checked:bg-primary-600"></div>
</label> </label>
</div> </div>
<!-- Custom threshold --> <template v-if="notifyEnabled">
<div v-if="notifyEnabled"> <!-- Custom threshold -->
<label class="input-label"> <div>
{{ t('profile.balanceNotify.threshold') }} <label class="input-label">
<span class="text-xs text-gray-400 ml-2">{{ t('profile.balanceNotify.thresholdHint') }}</span> {{ t('profile.balanceNotify.threshold') }}
</label> <span class="text-xs text-gray-400 ml-2">{{ t('profile.balanceNotify.thresholdHint') }}</span>
<div class="flex items-center gap-2"> </label>
<span class="text-gray-500">$</span> <div class="flex items-center gap-2">
<input <span class="text-gray-500">$</span>
v-model.number="customThreshold" <input
type="number" v-model.number="customThreshold"
min="0" type="number"
step="0.01" min="0"
class="input flex-1" step="0.01"
:placeholder="t('profile.balanceNotify.thresholdPlaceholder')" class="input flex-1"
@blur="handleThresholdUpdate" :placeholder="systemDefaultThreshold > 0 ? `${t('profile.balanceNotify.systemDefault')} $${systemDefaultThreshold}` : t('profile.balanceNotify.thresholdPlaceholder')"
/> @blur="handleThresholdUpdate"
/>
</div>
</div> </div>
</div>
<!-- Extra emails --> <!-- Primary email (always shown, with toggle) -->
<div v-if="notifyEnabled"> <div>
<label class="input-label">{{ t('profile.balanceNotify.extraEmails') }}</label> <label class="input-label">{{ t('profile.balanceNotify.extraEmails') }}</label>
<div class="space-y-2 mb-3">
<!-- Existing emails list --> <div class="flex items-center justify-between px-3 py-2 bg-gray-50 dark:bg-dark-700 rounded-lg">
<div v-if="extraEmails.length > 0" class="space-y-2 mb-4"> <span class="text-sm text-gray-700 dark:text-gray-300">{{ userEmail }}</span>
<div v-for="email in extraEmails" :key="email" <span class="text-xs text-gray-400">{{ t('profile.balanceNotify.primaryEmail') }}</span>
class="flex items-center justify-between px-3 py-2 bg-gray-50 dark:bg-dark-700 rounded-lg"> </div>
<span class="text-sm text-gray-700 dark:text-gray-300">{{ email }}</span>
<button @click="handleRemoveEmail(email)" class="text-red-500 hover:text-red-700 text-sm">
{{ t('profile.balanceNotify.removeEmail') }}
</button>
</div> </div>
</div>
<!-- Add new email --> <!-- Verified extra emails -->
<div class="space-y-2"> <div v-if="extraEmails.length > 0" class="space-y-2 mb-3">
<div v-for="email in extraEmails" :key="email"
class="flex items-center justify-between px-3 py-2 bg-gray-50 dark:bg-dark-700 rounded-lg">
<span class="text-sm text-gray-700 dark:text-gray-300">{{ email }}</span>
<button @click="handleRemoveEmail(email)" class="text-red-500 hover:text-red-700 text-sm">
{{ t('profile.balanceNotify.removeEmail') }}
</button>
</div>
</div>
<!-- Pending (unverified) emails -->
<div v-if="pendingEmails.length > 0" class="space-y-2 mb-3">
<div v-for="(pe, idx) in pendingEmails" :key="pe.email"
class="flex items-center gap-2 px-3 py-2 bg-yellow-50 dark:bg-yellow-900/10 rounded-lg border border-yellow-200 dark:border-yellow-800">
<span class="flex-1 text-sm text-gray-700 dark:text-gray-300">{{ pe.email }}</span>
<div v-if="!pe.codeSent" class="flex items-center gap-1">
<button @click="sendCodeFor(idx)" :disabled="pe.sending" class="text-xs text-primary-600 hover:text-primary-700">
{{ t('profile.balanceNotify.sendCode') }}
</button>
<button @click="pendingEmails.splice(idx, 1)" class="text-xs text-red-500 hover:text-red-700 ml-1">
{{ t('profile.balanceNotify.removeEmail') }}
</button>
</div>
<div v-else class="flex items-center gap-1">
<input
v-model="pe.code"
type="text"
maxlength="6"
class="w-20 rounded border border-gray-300 px-2 py-1 text-xs dark:border-dark-500 dark:bg-dark-700"
:placeholder="t('profile.balanceNotify.codePlaceholder')"
/>
<button @click="verifyPending(idx)" :disabled="!pe.code || pe.code.length !== 6 || pe.verifying" class="text-xs text-primary-600 hover:text-primary-700">
{{ t('profile.balanceNotify.verify') }}
</button>
<span v-if="pe.countdown > 0" class="text-xs text-gray-400">{{ pe.countdown }}s</span>
<button v-else @click="sendCodeFor(idx)" :disabled="pe.sending" class="text-xs text-gray-500 hover:text-gray-700">
{{ t('profile.balanceNotify.resend') }}
</button>
</div>
</div>
</div>
<!-- Add new email input -->
<div class="flex gap-2"> <div class="flex gap-2">
<input <input
v-model="newEmail" v-model="newEmail"
type="email" type="email"
class="input flex-1" class="input flex-1"
:placeholder="t('profile.balanceNotify.emailPlaceholder')" :placeholder="t('profile.balanceNotify.emailPlaceholder')"
:disabled="codeSent" @keyup.enter="addPendingEmail"
/> />
<button <button
@click="handleSendCode" @click="addPendingEmail"
:disabled="!newEmail || sendingCode || codeCountdown > 0" :disabled="!newEmail"
class="btn btn-outline whitespace-nowrap" class="btn btn-secondary whitespace-nowrap"
> >
{{ codeCountdown > 0 ? `${codeCountdown}s` : (codeSent ? t('profile.balanceNotify.codeSent') : t('profile.balanceNotify.sendCode')) }} {{ t('common.add') }}
</button>
</div>
<div v-if="codeSent" class="flex gap-2">
<input
v-model="verifyCode"
type="text"
maxlength="6"
class="input flex-1"
:placeholder="t('profile.balanceNotify.codePlaceholder')"
/>
<button
@click="handleVerify"
:disabled="!verifyCode || verifyCode.length !== 6 || verifying"
class="btn btn-primary whitespace-nowrap"
>
{{ t('profile.balanceNotify.verify') }}
</button> </button>
</div> </div>
</div> </div>
</div> </template>
</div> </div>
</div> </div>
</template> </template>
...@@ -100,10 +123,22 @@ import { useAppStore } from '@/stores/app' ...@@ -100,10 +123,22 @@ import { useAppStore } from '@/stores/app'
import { userAPI } from '@/api' import { userAPI } from '@/api'
import { extractApiErrorMessage } from '@/utils/apiError' import { extractApiErrorMessage } from '@/utils/apiError'
interface PendingEmail {
email: string
codeSent: boolean
code: string
sending: boolean
verifying: boolean
countdown: number
timer: ReturnType<typeof setInterval> | null
}
const props = defineProps<{ const props = defineProps<{
enabled: boolean enabled: boolean
threshold: number | null threshold: number | null
extraEmails: string[] extraEmails: string[]
systemDefaultThreshold: number
userEmail: string
}>() }>()
const { t } = useI18n() const { t } = useI18n()
...@@ -113,23 +148,19 @@ const appStore = useAppStore() ...@@ -113,23 +148,19 @@ const appStore = useAppStore()
const notifyEnabled = ref(props.enabled) const notifyEnabled = ref(props.enabled)
const customThreshold = ref<number | null>(props.threshold) const customThreshold = ref<number | null>(props.threshold)
const extraEmails = ref<string[]>([...props.extraEmails]) const extraEmails = ref<string[]>([...props.extraEmails])
const pendingEmails = ref<PendingEmail[]>([])
const newEmail = ref('') const newEmail = ref('')
const verifyCode = ref('')
const codeSent = ref(false)
const sendingCode = ref(false)
const verifying = ref(false)
const codeCountdown = ref(0)
let countdownTimer: ReturnType<typeof setInterval> | null = null
onUnmounted(() => {
if (countdownTimer) clearInterval(countdownTimer)
})
watch(() => props.enabled, (val) => { notifyEnabled.value = val }) watch(() => props.enabled, (val) => { notifyEnabled.value = val })
watch(() => props.threshold, (val) => { customThreshold.value = val }) watch(() => props.threshold, (val) => { customThreshold.value = val })
watch(() => props.extraEmails, (val) => { extraEmails.value = [...val] }) watch(() => props.extraEmails, (val) => { extraEmails.value = [...val] })
onUnmounted(() => {
for (const pe of pendingEmails.value) {
if (pe.timer) clearInterval(pe.timer)
}
})
const handleToggle = async () => { const handleToggle = async () => {
try { try {
const updated = await userAPI.updateProfile({ balance_notify_enabled: notifyEnabled.value }) const updated = await userAPI.updateProfile({ balance_notify_enabled: notifyEnabled.value })
...@@ -150,47 +181,56 @@ const handleThresholdUpdate = async () => { ...@@ -150,47 +181,56 @@ const handleThresholdUpdate = async () => {
} }
} }
const handleSendCode = async () => { function addPendingEmail() {
if (!newEmail.value) return const email = newEmail.value.trim()
sendingCode.value = true if (!email) return
if (email === props.userEmail || extraEmails.value.includes(email) || pendingEmails.value.some(p => p.email === email)) {
appStore.showError(t('common.error'))
return
}
pendingEmails.value.push({ email, codeSent: false, code: '', sending: false, verifying: false, countdown: 0, timer: null })
newEmail.value = ''
}
async function sendCodeFor(idx: number) {
const pe = pendingEmails.value[idx]
if (!pe) return
pe.sending = true
try { try {
await userAPI.sendNotifyEmailCode(newEmail.value) await userAPI.sendNotifyEmailCode(pe.email)
codeSent.value = true pe.codeSent = true
codeCountdown.value = 60 pe.countdown = 60
countdownTimer = setInterval(() => { pe.timer = setInterval(() => {
codeCountdown.value-- pe.countdown--
if (codeCountdown.value <= 0) { if (pe.countdown <= 0 && pe.timer) {
if (countdownTimer) clearInterval(countdownTimer) clearInterval(pe.timer)
countdownTimer = null pe.timer = null
} }
}, 1000) }, 1000)
appStore.showSuccess(t('profile.balanceNotify.codeSent')) appStore.showSuccess(t('profile.balanceNotify.codeSent'))
} catch (err: unknown) { } catch (err: unknown) {
appStore.showError(extractApiErrorMessage(err, t('common.error'))) appStore.showError(extractApiErrorMessage(err, t('common.error')))
} finally { } finally {
sendingCode.value = false pe.sending = false
} }
} }
const handleVerify = async () => { async function verifyPending(idx: number) {
if (!verifyCode.value || verifyCode.value.length !== 6) return const pe = pendingEmails.value[idx]
verifying.value = true if (!pe || !pe.code || pe.code.length !== 6) return
pe.verifying = true
try { try {
await userAPI.verifyNotifyEmail(newEmail.value, verifyCode.value) await userAPI.verifyNotifyEmail(pe.email, pe.code)
extraEmails.value.push(newEmail.value) extraEmails.value.push(pe.email)
newEmail.value = '' if (pe.timer) clearInterval(pe.timer)
verifyCode.value = '' pendingEmails.value.splice(idx, 1)
codeSent.value = false
if (countdownTimer) clearInterval(countdownTimer)
codeCountdown.value = 0
appStore.showSuccess(t('profile.balanceNotify.verifySuccess')) appStore.showSuccess(t('profile.balanceNotify.verifySuccess'))
// Refresh user data
const updated = await userAPI.getProfile() const updated = await userAPI.getProfile()
authStore.user = updated authStore.user = updated
} catch (err: unknown) { } catch (err: unknown) {
appStore.showError(extractApiErrorMessage(err, t('common.error'))) appStore.showError(extractApiErrorMessage(err, t('common.error')))
} finally { } finally {
verifying.value = false pe.verifying = false
} }
} }
......
...@@ -911,17 +911,19 @@ export default { ...@@ -911,17 +911,19 @@ export default {
thresholdHint: 'Leave empty to use system default', thresholdHint: 'Leave empty to use system default',
thresholdPlaceholder: 'Enter amount', thresholdPlaceholder: 'Enter amount',
systemDefault: 'System Default', systemDefault: 'System Default',
extraEmails: 'Extra Notification Emails', extraEmails: 'Notification Emails',
primaryEmail: 'Primary',
noExtraEmails: 'No extra notification emails', noExtraEmails: 'No extra notification emails',
enterEmail: 'Enter email address', enterEmail: 'Enter email address',
addEmail: 'Add Email', addEmail: 'Add Email',
emailPlaceholder: 'Enter email address', emailPlaceholder: 'Enter email address',
sendCode: 'Send Code', sendCode: 'Send Code',
resend: 'Resend',
codeSent: 'Verification code sent', codeSent: 'Verification code sent',
codeSentTo: 'Code sent to {email}', codeSentTo: 'Code sent to {email}',
enterCode: 'Enter verification code', enterCode: 'Enter verification code',
codePlaceholder: '6-digit code', codePlaceholder: '6-digit code',
verify: 'Verify & Add', verify: 'Verify',
emailAdded: 'Email added', emailAdded: 'Email added',
emailRemoved: 'Email removed', emailRemoved: 'Email removed',
verifySuccess: 'Email added successfully', verifySuccess: 'Email added successfully',
......
...@@ -915,17 +915,19 @@ export default { ...@@ -915,17 +915,19 @@ export default {
thresholdHint: '留空使用系统默认值', thresholdHint: '留空使用系统默认值',
thresholdPlaceholder: '输入金额', thresholdPlaceholder: '输入金额',
systemDefault: '系统默认值', systemDefault: '系统默认值',
extraEmails: '额外通知邮箱', extraEmails: '通知邮箱',
primaryEmail: '主邮箱',
noExtraEmails: '暂无额外通知邮箱', noExtraEmails: '暂无额外通知邮箱',
enterEmail: '输入邮箱地址', enterEmail: '输入邮箱地址',
addEmail: '添加邮箱', addEmail: '添加邮箱',
emailPlaceholder: '输入邮箱地址', emailPlaceholder: '输入邮箱地址',
sendCode: '发送验证码', sendCode: '发送验证码',
resend: '重发',
codeSent: '验证码已发送', codeSent: '验证码已发送',
codeSentTo: '验证码已发送到 {email}', codeSentTo: '验证码已发送到 {email}',
enterCode: '输入验证码', enterCode: '输入验证码',
codePlaceholder: '6位验证码', codePlaceholder: '6位验证码',
verify: '确认添加', verify: '验证',
emailAdded: '邮箱已添加', emailAdded: '邮箱已添加',
emailRemoved: '邮箱已移除', emailRemoved: '邮箱已移除',
verifySuccess: '邮箱添加成功', verifySuccess: '邮箱添加成功',
......
...@@ -342,6 +342,7 @@ export const useAppStore = defineStore('app', () => { ...@@ -342,6 +342,7 @@ export const useAppStore = defineStore('app', () => {
version: siteVersion.value, version: siteVersion.value,
balance_low_notify_enabled: false, balance_low_notify_enabled: false,
account_quota_notify_enabled: false, account_quota_notify_enabled: false,
balance_low_notify_threshold: 0,
} }
} }
......
...@@ -119,6 +119,7 @@ export interface PublicSettings { ...@@ -119,6 +119,7 @@ export interface PublicSettings {
version: string version: string
balance_low_notify_enabled: boolean balance_low_notify_enabled: boolean
account_quota_notify_enabled: boolean account_quota_notify_enabled: boolean
balance_low_notify_threshold: number
} }
export interface AuthResponse { export interface AuthResponse {
......
...@@ -19,6 +19,8 @@ ...@@ -19,6 +19,8 @@
:enabled="user.balance_notify_enabled ?? true" :enabled="user.balance_notify_enabled ?? true"
:threshold="user.balance_notify_threshold" :threshold="user.balance_notify_threshold"
:extra-emails="user.balance_notify_extra_emails ?? []" :extra-emails="user.balance_notify_extra_emails ?? []"
:system-default-threshold="systemDefaultThreshold"
:user-email="user.email"
/> />
<ProfilePasswordForm /> <ProfilePasswordForm />
<ProfileTotpCard /> <ProfileTotpCard />
...@@ -41,11 +43,12 @@ import { Icon } from '@/components/icons' ...@@ -41,11 +43,12 @@ import { Icon } from '@/components/icons'
const { t } = useI18n(); const authStore = useAuthStore(); const user = computed(() => authStore.user) const { t } = useI18n(); const authStore = useAuthStore(); const user = computed(() => authStore.user)
const contactInfo = ref('') const contactInfo = ref('')
const balanceLowNotifyEnabled = ref(false) const balanceLowNotifyEnabled = ref(false)
const systemDefaultThreshold = ref(0)
const WalletIcon = { render: () => h('svg', { fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [h('path', { d: 'M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12' })]) } const WalletIcon = { render: () => h('svg', { fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [h('path', { d: 'M21 12a2.25 2.25 0 00-2.25-2.25H15a3 3 0 11-6 0H5.25A2.25 2.25 0 003 12' })]) }
const BoltIcon = { render: () => h('svg', { fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [h('path', { d: 'm3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z' })]) } const BoltIcon = { render: () => h('svg', { fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [h('path', { d: 'm3.75 13.5 10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z' })]) }
const CalendarIcon = { render: () => h('svg', { fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [h('path', { d: 'M6.75 3v2.25M17.25 3v2.25' })]) } const CalendarIcon = { render: () => h('svg', { fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor', 'stroke-width': '1.5' }, [h('path', { d: 'M6.75 3v2.25M17.25 3v2.25' })]) }
onMounted(async () => { try { const s = await authAPI.getPublicSettings(); contactInfo.value = s.contact_info || ''; balanceLowNotifyEnabled.value = s.balance_low_notify_enabled ?? false } catch (error) { console.error('Failed to load contact info:', error) } }) onMounted(async () => { try { const s = await authAPI.getPublicSettings(); contactInfo.value = s.contact_info || ''; balanceLowNotifyEnabled.value = s.balance_low_notify_enabled ?? false; systemDefaultThreshold.value = s.balance_low_notify_threshold ?? 0 } catch (error) { console.error('Failed to load settings:', error) } })
const formatCurrency = (v: number) => `$${v.toFixed(2)}` const formatCurrency = (v: number) => `$${v.toFixed(2)}`
</script> </script>
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment