feat: announcement支持强制弹窗通知

This commit is contained in:
shaw
2026-03-07 15:06:13 +08:00
parent a42a1f08e9
commit 7079edc2d0
25 changed files with 840 additions and 154 deletions

View File

@@ -68,6 +68,19 @@
</span>
</template>
<template #cell-notifyMode="{ row }">
<span
:class="[
'badge',
row.notify_mode === 'popup'
? 'badge-warning'
: 'badge-gray'
]"
>
{{ row.notify_mode === 'popup' ? t('admin.announcements.notifyModeLabels.popup') : t('admin.announcements.notifyModeLabels.silent') }}
</span>
</template>
<template #cell-targeting="{ row }">
<span class="text-sm text-gray-600 dark:text-gray-300">
{{ targetingSummary(row.targeting) }}
@@ -163,7 +176,11 @@
<label class="input-label">{{ t('admin.announcements.form.status') }}</label>
<Select v-model="form.status" :options="statusOptions" />
</div>
<div></div>
<div>
<label class="input-label">{{ t('admin.announcements.form.notifyMode') }}</label>
<Select v-model="form.notify_mode" :options="notifyModeOptions" />
<p class="input-hint">{{ t('admin.announcements.form.notifyModeHint') }}</p>
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
@@ -271,9 +288,15 @@ const statusOptions = computed(() => [
{ value: 'archived', label: t('admin.announcements.statusLabels.archived') }
])
const notifyModeOptions = computed(() => [
{ value: 'silent', label: t('admin.announcements.notifyModeLabels.silent') },
{ value: 'popup', label: t('admin.announcements.notifyModeLabels.popup') }
])
const columns = computed<Column[]>(() => [
{ key: 'title', label: t('admin.announcements.columns.title') },
{ key: 'status', label: t('admin.announcements.columns.status') },
{ key: 'notifyMode', label: t('admin.announcements.columns.notifyMode') },
{ key: 'targeting', label: t('admin.announcements.columns.targeting') },
{ key: 'timeRange', label: t('admin.announcements.columns.timeRange') },
{ key: 'createdAt', label: t('admin.announcements.columns.createdAt') },
@@ -357,6 +380,7 @@ const form = reactive({
title: '',
content: '',
status: 'draft',
notify_mode: 'silent',
starts_at_str: '',
ends_at_str: '',
targeting: { any_of: [] } as AnnouncementTargeting
@@ -378,6 +402,7 @@ function resetForm() {
form.title = ''
form.content = ''
form.status = 'draft'
form.notify_mode = 'silent'
form.starts_at_str = ''
form.ends_at_str = ''
form.targeting = { any_of: [] }
@@ -387,6 +412,7 @@ function fillFormFromAnnouncement(a: Announcement) {
form.title = a.title
form.content = a.content
form.status = a.status
form.notify_mode = a.notify_mode || 'silent'
// Backend returns RFC3339 strings
form.starts_at_str = a.starts_at ? formatDateTimeLocalInput(Math.floor(new Date(a.starts_at).getTime() / 1000)) : ''
@@ -420,6 +446,7 @@ function buildCreatePayload() {
title: form.title,
content: form.content,
status: form.status as any,
notify_mode: form.notify_mode as any,
targeting: form.targeting,
starts_at: startsAt ?? undefined,
ends_at: endsAt ?? undefined
@@ -432,6 +459,7 @@ function buildUpdatePayload(original: Announcement) {
if (form.title !== original.title) payload.title = form.title
if (form.content !== original.content) payload.content = form.content
if (form.status !== original.status) payload.status = form.status
if (form.notify_mode !== (original.notify_mode || 'silent')) payload.notify_mode = form.notify_mode
// starts_at / ends_at: distinguish unchanged vs clear(0) vs set
const originalStarts = original.starts_at ? Math.floor(new Date(original.starts_at).getTime() / 1000) : null