feat(notification): add scheduled email pipeline with prefs/groups and DLQ UI

This commit is contained in:
2026-03-01 22:28:51 +08:00
parent 809a3dc522
commit 7e97aaa7fc
13 changed files with 2416 additions and 88 deletions

View File

@@ -1,14 +1,49 @@
<script setup lang="ts">
import { reactive } from 'vue'
import { session } from '../stores/session'
import { onMounted, reactive, ref } from 'vue'
import type { NotificationPrefs } from '../types'
import * as api from '../services/api'
import { t } from '../i18n'
import { useToast } from '../composables/useToast'
const form = reactive({
displayName: 'Product Operator',
email: session.email,
const { success, error } = useToast()
const loading = ref(false)
const form = reactive<NotificationPrefs>({
subscribed: true,
dnd_start: '',
dnd_end: '',
locale: 'zh',
timezone: 'Asia/Shanghai',
notifications: true,
daily_summary_enabled: true,
daily_summary_time: '09:30',
})
async function loadPrefs() {
loading.value = true
try {
const prefs = await api.getNotificationPrefs()
Object.assign(form, prefs)
} catch {
error(t('load_settings_failed'))
} finally {
loading.value = false
}
}
async function savePrefs() {
loading.value = true
try {
const updated = await api.updateNotificationPrefs(form)
Object.assign(form, updated)
success(t('save_settings_success'))
} catch {
error(t('save_settings_failed'))
} finally {
loading.value = false
}
}
onMounted(loadPrefs)
</script>
<template>
@@ -18,24 +53,39 @@ const form = reactive({
<p>{{ t('user_settings_subtitle') }}</p>
</header>
<form class="card settings-form" @submit.prevent>
<label>
{{ t('display_name') }}
<input v-model="form.displayName" type="text" />
</label>
<label>
{{ t('email') }}
<input v-model="form.email" type="email" />
<form class="card settings-form" @submit.prevent="savePrefs">
<label class="inline">
<input v-model="form.subscribed" type="checkbox" />
{{ t('reminders') }}
</label>
<label>
{{ t('timezone') }}
<input v-model="form.timezone" type="text" />
<input v-model="form.timezone" type="text" placeholder="Asia/Shanghai" />
</label>
<label>
{{ t('locale') }}
<select v-model="form.locale">
<option value="zh">中文</option>
<option value="en">English</option>
</select>
</label>
<label>
{{ t('dnd_start') }}
<input v-model="form.dnd_start" type="time" />
</label>
<label>
{{ t('dnd_end') }}
<input v-model="form.dnd_end" type="time" />
</label>
<label class="inline">
<input v-model="form.notifications" type="checkbox" />
{{ t('reminders') }}
<input v-model="form.daily_summary_enabled" type="checkbox" />
{{ t('daily_summary_enabled') }}
</label>
<button class="btn primary" type="submit">{{ t('save_settings') }}</button>
<label>
{{ t('daily_summary_time') }}
<input v-model="form.daily_summary_time" type="time" />
</label>
<button class="btn primary" type="submit" :disabled="loading">{{ loading ? t('loading_wait') : t('save_settings') }}</button>
</form>
</section>
</template>