feat(notify): add global toggles, percentage threshold, and visibility control

- Add global toggle for account quota notification in admin settings
- Add percentage-based threshold type for per-account quota alerts
- Hide balance notify card on user profile when global toggle is off
- Expose balance_low_notify_enabled and account_quota_notify_enabled in PublicSettings
- Add threshold type (fixed/percentage) to QuotaNotifyToggle with $ / % switcher
This commit is contained in:
erio
2026-04-12 17:49:58 +08:00
parent 889b5b4f3b
commit eba289a7ff
15 changed files with 164 additions and 26 deletions

View File

@@ -1188,10 +1188,13 @@
:resetTimezone="editResetTimezone"
:quotaNotifyDailyEnabled="editQuotaNotifyDailyEnabled"
:quotaNotifyDailyThreshold="editQuotaNotifyDailyThreshold"
:quotaNotifyDailyThresholdType="editQuotaNotifyDailyThresholdType"
:quotaNotifyWeeklyEnabled="editQuotaNotifyWeeklyEnabled"
:quotaNotifyWeeklyThreshold="editQuotaNotifyWeeklyThreshold"
:quotaNotifyWeeklyThresholdType="editQuotaNotifyWeeklyThresholdType"
:quotaNotifyTotalEnabled="editQuotaNotifyTotalEnabled"
:quotaNotifyTotalThreshold="editQuotaNotifyTotalThreshold"
:quotaNotifyTotalThresholdType="editQuotaNotifyTotalThresholdType"
@update:totalLimit="editQuotaLimit = $event"
@update:dailyLimit="editQuotaDailyLimit = $event"
@update:weeklyLimit="editQuotaWeeklyLimit = $event"
@@ -1203,10 +1206,13 @@
@update:resetTimezone="editResetTimezone = $event"
@update:quotaNotifyDailyEnabled="editQuotaNotifyDailyEnabled = $event"
@update:quotaNotifyDailyThreshold="editQuotaNotifyDailyThreshold = $event"
@update:quotaNotifyDailyThresholdType="editQuotaNotifyDailyThresholdType = $event"
@update:quotaNotifyWeeklyEnabled="editQuotaNotifyWeeklyEnabled = $event"
@update:quotaNotifyWeeklyThreshold="editQuotaNotifyWeeklyThreshold = $event"
@update:quotaNotifyWeeklyThresholdType="editQuotaNotifyWeeklyThresholdType = $event"
@update:quotaNotifyTotalEnabled="editQuotaNotifyTotalEnabled = $event"
@update:quotaNotifyTotalThreshold="editQuotaNotifyTotalThreshold = $event"
@update:quotaNotifyTotalThresholdType="editQuotaNotifyTotalThresholdType = $event"
/>
</div>
<!-- 配额控制 ( Anthropic apikey/bedrock) -->
@@ -1232,10 +1238,13 @@
:resetTimezone="editResetTimezone"
:quotaNotifyDailyEnabled="editQuotaNotifyDailyEnabled"
:quotaNotifyDailyThreshold="editQuotaNotifyDailyThreshold"
:quotaNotifyDailyThresholdType="editQuotaNotifyDailyThresholdType"
:quotaNotifyWeeklyEnabled="editQuotaNotifyWeeklyEnabled"
:quotaNotifyWeeklyThreshold="editQuotaNotifyWeeklyThreshold"
:quotaNotifyWeeklyThresholdType="editQuotaNotifyWeeklyThresholdType"
:quotaNotifyTotalEnabled="editQuotaNotifyTotalEnabled"
:quotaNotifyTotalThreshold="editQuotaNotifyTotalThreshold"
:quotaNotifyTotalThresholdType="editQuotaNotifyTotalThresholdType"
@update:totalLimit="editQuotaLimit = $event"
@update:dailyLimit="editQuotaDailyLimit = $event"
@update:weeklyLimit="editQuotaWeeklyLimit = $event"
@@ -1247,10 +1256,13 @@
@update:resetTimezone="editResetTimezone = $event"
@update:quotaNotifyDailyEnabled="editQuotaNotifyDailyEnabled = $event"
@update:quotaNotifyDailyThreshold="editQuotaNotifyDailyThreshold = $event"
@update:quotaNotifyDailyThresholdType="editQuotaNotifyDailyThresholdType = $event"
@update:quotaNotifyWeeklyEnabled="editQuotaNotifyWeeklyEnabled = $event"
@update:quotaNotifyWeeklyThreshold="editQuotaNotifyWeeklyThreshold = $event"
@update:quotaNotifyWeeklyThresholdType="editQuotaNotifyWeeklyThresholdType = $event"
@update:quotaNotifyTotalEnabled="editQuotaNotifyTotalEnabled = $event"
@update:quotaNotifyTotalThreshold="editQuotaNotifyTotalThreshold = $event"
@update:quotaNotifyTotalThresholdType="editQuotaNotifyTotalThresholdType = $event"
/>
</div>
@@ -1992,10 +2004,13 @@ const editWeeklyResetHour = ref<number | null>(null)
const editResetTimezone = ref<string | null>(null)
const editQuotaNotifyDailyEnabled = ref<boolean | null>(null)
const editQuotaNotifyDailyThreshold = ref<number | null>(null)
const editQuotaNotifyDailyThresholdType = ref<string | null>(null)
const editQuotaNotifyWeeklyEnabled = ref<boolean | null>(null)
const editQuotaNotifyWeeklyThreshold = ref<number | null>(null)
const editQuotaNotifyWeeklyThresholdType = ref<string | null>(null)
const editQuotaNotifyTotalEnabled = ref<boolean | null>(null)
const editQuotaNotifyTotalThreshold = ref<number | null>(null)
const editQuotaNotifyTotalThresholdType = ref<string | null>(null)
const openAIWSModeOptions = computed(() => [
{ value: OPENAI_WS_MODE_OFF, label: t('admin.accounts.openai.wsModeOff') },
// TODO: ctx_pool 选项暂时隐藏,待测试完成后恢复
@@ -2198,10 +2213,13 @@ const syncFormFromAccount = (newAccount: Account | null) => {
// Load quota notify config
editQuotaNotifyDailyEnabled.value = (extra?.quota_notify_daily_enabled as boolean) ?? null
editQuotaNotifyDailyThreshold.value = (extra?.quota_notify_daily_threshold as number) ?? null
editQuotaNotifyDailyThresholdType.value = (extra?.quota_notify_daily_threshold_type as string) ?? null
editQuotaNotifyWeeklyEnabled.value = (extra?.quota_notify_weekly_enabled as boolean) ?? null
editQuotaNotifyWeeklyThreshold.value = (extra?.quota_notify_weekly_threshold as number) ?? null
editQuotaNotifyWeeklyThresholdType.value = (extra?.quota_notify_weekly_threshold_type as string) ?? null
editQuotaNotifyTotalEnabled.value = (extra?.quota_notify_total_enabled as boolean) ?? null
editQuotaNotifyTotalThreshold.value = (extra?.quota_notify_total_threshold as number) ?? null
editQuotaNotifyTotalThresholdType.value = (extra?.quota_notify_total_threshold_type as string) ?? null
} else {
editQuotaLimit.value = null
editQuotaDailyLimit.value = null
@@ -3262,9 +3280,11 @@ const handleSubmit = async () => {
} else {
delete newExtra.quota_notify_daily_threshold
}
newExtra.quota_notify_daily_threshold_type = editQuotaNotifyDailyThresholdType.value || 'fixed'
} else {
delete newExtra.quota_notify_daily_enabled
delete newExtra.quota_notify_daily_threshold
delete newExtra.quota_notify_daily_threshold_type
}
if (editQuotaNotifyWeeklyEnabled.value) {
newExtra.quota_notify_weekly_enabled = true
@@ -3273,9 +3293,11 @@ const handleSubmit = async () => {
} else {
delete newExtra.quota_notify_weekly_threshold
}
newExtra.quota_notify_weekly_threshold_type = editQuotaNotifyWeeklyThresholdType.value || 'fixed'
} else {
delete newExtra.quota_notify_weekly_enabled
delete newExtra.quota_notify_weekly_threshold
delete newExtra.quota_notify_weekly_threshold_type
}
if (editQuotaNotifyTotalEnabled.value) {
newExtra.quota_notify_total_enabled = true
@@ -3284,9 +3306,11 @@ const handleSubmit = async () => {
} else {
delete newExtra.quota_notify_total_threshold
}
newExtra.quota_notify_total_threshold_type = editQuotaNotifyTotalThresholdType.value || 'fixed'
} else {
delete newExtra.quota_notify_total_enabled
delete newExtra.quota_notify_total_threshold
delete newExtra.quota_notify_total_threshold_type
}
updatePayload.extra = newExtra
}

View File

@@ -17,17 +17,23 @@ const props = withDefaults(defineProps<{
resetTimezone: string | null
quotaNotifyDailyEnabled?: boolean | null
quotaNotifyDailyThreshold?: number | null
quotaNotifyDailyThresholdType?: string | null
quotaNotifyWeeklyEnabled?: boolean | null
quotaNotifyWeeklyThreshold?: number | null
quotaNotifyWeeklyThresholdType?: string | null
quotaNotifyTotalEnabled?: boolean | null
quotaNotifyTotalThreshold?: number | null
quotaNotifyTotalThresholdType?: string | null
}>(), {
quotaNotifyDailyEnabled: null,
quotaNotifyDailyThreshold: null,
quotaNotifyDailyThresholdType: null,
quotaNotifyWeeklyEnabled: null,
quotaNotifyWeeklyThreshold: null,
quotaNotifyWeeklyThresholdType: null,
quotaNotifyTotalEnabled: null,
quotaNotifyTotalThreshold: null,
quotaNotifyTotalThresholdType: null,
})
const emit = defineEmits<{
@@ -42,10 +48,13 @@ const emit = defineEmits<{
'update:resetTimezone': [value: string | null]
'update:quotaNotifyDailyEnabled': [value: boolean | null]
'update:quotaNotifyDailyThreshold': [value: number | null]
'update:quotaNotifyDailyThresholdType': [value: string | null]
'update:quotaNotifyWeeklyEnabled': [value: boolean | null]
'update:quotaNotifyWeeklyThreshold': [value: number | null]
'update:quotaNotifyWeeklyThresholdType': [value: string | null]
'update:quotaNotifyTotalEnabled': [value: boolean | null]
'update:quotaNotifyTotalThreshold': [value: number | null]
'update:quotaNotifyTotalThresholdType': [value: string | null]
}>()
const enabled = computed(() =>
@@ -228,8 +237,10 @@ const onWeeklyModeChange = (e: Event) => {
v-if="dailyLimit && dailyLimit > 0"
:enabled="props.quotaNotifyDailyEnabled"
:threshold="props.quotaNotifyDailyThreshold"
:threshold-type="props.quotaNotifyDailyThresholdType"
@update:enabled="emit('update:quotaNotifyDailyEnabled', $event)"
@update:threshold="emit('update:quotaNotifyDailyThreshold', $event)"
@update:threshold-type="emit('update:quotaNotifyDailyThresholdType', $event)"
/>
</div>
@@ -292,8 +303,10 @@ const onWeeklyModeChange = (e: Event) => {
v-if="weeklyLimit && weeklyLimit > 0"
:enabled="props.quotaNotifyWeeklyEnabled"
:threshold="props.quotaNotifyWeeklyThreshold"
:threshold-type="props.quotaNotifyWeeklyThresholdType"
@update:enabled="emit('update:quotaNotifyWeeklyEnabled', $event)"
@update:threshold="emit('update:quotaNotifyWeeklyThreshold', $event)"
@update:threshold-type="emit('update:quotaNotifyWeeklyThresholdType', $event)"
/>
</div>
@@ -330,8 +343,10 @@ const onWeeklyModeChange = (e: Event) => {
v-if="totalLimit && totalLimit > 0"
:enabled="props.quotaNotifyTotalEnabled"
:threshold="props.quotaNotifyTotalThreshold"
:threshold-type="props.quotaNotifyTotalThresholdType"
@update:enabled="emit('update:quotaNotifyTotalEnabled', $event)"
@update:threshold="emit('update:quotaNotifyTotalThreshold', $event)"
@update:threshold-type="emit('update:quotaNotifyTotalThresholdType', $event)"
/>
</div>
</div>

View File

@@ -6,12 +6,18 @@ const { t } = useI18n()
defineProps<{
enabled: boolean | null
threshold: number | null
thresholdType: string | null // "fixed" (default) or "percentage"
}>()
const emit = defineEmits<{
'update:enabled': [value: boolean | null]
'update:threshold': [value: number | null]
'update:thresholdType': [value: string | null]
}>()
function toggleType(current: string | null) {
emit('update:thresholdType', current === 'percentage' ? 'fixed' : 'percentage')
}
</script>
<template>
@@ -32,15 +38,32 @@ const emit = defineEmits<{
]"
/>
</button>
<div v-if="enabled" class="relative flex-1">
<span class="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400 text-sm">$</span>
<div v-if="enabled" class="flex items-center gap-1 flex-1">
<button
type="button"
class="px-1.5 py-0.5 text-xs font-medium rounded border transition-colors"
:class="(!thresholdType || thresholdType === 'fixed') ? 'bg-primary-100 text-primary-700 border-primary-300 dark:bg-primary-900/30 dark:text-primary-400 dark:border-primary-700' : 'bg-gray-100 text-gray-500 border-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:border-dark-500'"
@click="toggleType(thresholdType)"
>
$
</button>
<button
type="button"
class="px-1.5 py-0.5 text-xs font-medium rounded border transition-colors"
:class="thresholdType === 'percentage' ? 'bg-primary-100 text-primary-700 border-primary-300 dark:bg-primary-900/30 dark:text-primary-400 dark:border-primary-700' : 'bg-gray-100 text-gray-500 border-gray-200 dark:bg-dark-600 dark:text-gray-400 dark:border-dark-500'"
@click="toggleType(thresholdType)"
>
%
</button>
<input
:value="threshold"
@input="emit('update:threshold', parseFloat(($event.target as HTMLInputElement).value) || null)"
type="number"
min="0"
step="0.01"
class="input pl-6 py-1 text-sm"
:max="thresholdType === 'percentage' ? 100 : undefined"
:step="thresholdType === 'percentage' ? 1 : 0.01"
class="input py-1 text-sm flex-1"
:placeholder="thresholdType === 'percentage' ? t('admin.accounts.quotaNotify.thresholdPlaceholder') : t('admin.accounts.quotaNotify.threshold')"
/>
</div>
</div>