Commit a04ae28a authored by 陈曦's avatar 陈曦
Browse files

merge v0.1.111

parents 68f67198 ad64190b
......@@ -39,7 +39,15 @@
</template>
<template #table>
<DataTable :columns="columns" :data="announcements" :loading="loading">
<DataTable
:columns="columns"
:data="announcements"
:loading="loading"
:server-side-sort="true"
default-sort-key="created_at"
default-sort-order="desc"
@sort="handleSort"
>
<template #cell-title="{ value, row }">
<div class="min-w-0">
<div class="flex items-center gap-2">
......@@ -68,7 +76,7 @@
</span>
</template>
<template #cell-notifyMode="{ row }">
<template #cell-notify_mode="{ row }">
<span
:class="[
'badge',
......@@ -100,7 +108,7 @@
</div>
</template>
<template #cell-createdAt="{ value }">
<template #cell-created_at="{ value }">
<span class="text-sm text-gray-500 dark:text-dark-400">{{ formatDateTime(value) }}</span>
</template>
......@@ -236,7 +244,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useAppStore } from '@/stores/app'
import { getPersistedPageSize } from '@/composables/usePersistedPageSize'
......@@ -276,6 +284,11 @@ const pagination = reactive({
pages: 0
})
const sortState = reactive({
sort_by: 'created_at',
sort_order: 'desc' as 'asc' | 'desc'
})
const statusFilterOptions = computed(() => [
{ value: '', label: t('admin.announcements.allStatus') },
{ value: 'draft', label: t('admin.announcements.statusLabels.draft') },
......@@ -295,12 +308,12 @@ const notifyModeOptions = computed(() => [
])
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: 'title', label: t('admin.announcements.columns.title'), sortable: true },
{ key: 'status', label: t('admin.announcements.columns.status'), sortable: true },
{ key: 'notify_mode', label: t('admin.announcements.columns.notifyMode'), sortable: true },
{ key: 'targeting', label: t('admin.announcements.columns.targeting') },
{ key: 'timeRange', label: t('admin.announcements.columns.timeRange') },
{ key: 'createdAt', label: t('admin.announcements.columns.createdAt') },
{ key: 'created_at', label: t('admin.announcements.columns.createdAt'), sortable: true },
{ key: 'actions', label: t('admin.announcements.columns.actions') }
])
......@@ -321,15 +334,21 @@ const targetingSummary = (targeting: AnnouncementTargeting) => {
let currentController: AbortController | null = null
async function loadAnnouncements() {
if (currentController) currentController.abort()
currentController = new AbortController()
currentController?.abort()
const requestController = new AbortController()
currentController = requestController
const { signal } = requestController
try {
loading.value = true
const res = await adminAPI.announcements.list(pagination.page, pagination.page_size, {
status: filters.status || undefined,
search: searchQuery.value || undefined
})
search: searchQuery.value || undefined,
sort_by: sortState.sort_by,
sort_order: sortState.sort_order
}, { signal })
if (signal.aborted || currentController !== requestController) return
announcements.value = res.items
pagination.total = res.total
......@@ -337,11 +356,21 @@ async function loadAnnouncements() {
pagination.page = res.page
pagination.page_size = res.page_size
} catch (error: any) {
if (currentController.signal.aborted || error?.name === 'AbortError') return
if (
signal.aborted ||
currentController !== requestController ||
error?.name === 'AbortError' ||
error?.code === 'ERR_CANCELED'
) {
return
}
console.error('Error loading announcements:', error)
appStore.showError(error.response?.data?.detail || t('admin.announcements.failedToLoad'))
} finally {
loading.value = false
if (currentController === requestController) {
loading.value = false
currentController = null
}
}
}
......@@ -361,6 +390,13 @@ function handleStatusChange() {
loadAnnouncements()
}
function handleSort(key: string, order: 'asc' | 'desc') {
sortState.sort_by = key
sortState.sort_order = order
pagination.page = 1
loadAnnouncements()
}
let searchDebounceTimer: number | null = null
function handleSearch() {
if (searchDebounceTimer) window.clearTimeout(searchDebounceTimer)
......@@ -562,4 +598,9 @@ onMounted(async () => {
await loadSubscriptionGroups()
await loadAnnouncements()
})
onUnmounted(() => {
if (searchDebounceTimer) window.clearTimeout(searchDebounceTimer)
currentController?.abort()
})
</script>
......@@ -48,7 +48,15 @@
</template>
<template #table>
<DataTable :columns="columns" :data="channels" :loading="loading">
<DataTable
:columns="columns"
:data="channels"
:loading="loading"
:server-side-sort="true"
default-sort-key="created_at"
default-sort-order="desc"
@sort="handleSort"
>
<template #cell-name="{ value }">
<span class="font-medium text-gray-900 dark:text-white">{{ value }}</span>
</template>
......@@ -486,6 +494,10 @@ const pagination = reactive({
page_size: getPersistedPageSize(),
total: 0
})
const sortState = reactive({
sort_by: 'created_at',
sort_order: 'desc' as 'asc' | 'desc'
})
// Dialog state
const showDialog = ref(false)
......@@ -766,7 +778,9 @@ async function loadChannels() {
try {
const response = await adminAPI.channels.list(pagination.page, pagination.page_size, {
status: filters.status || undefined,
search: searchQuery.value || undefined
search: searchQuery.value || undefined,
sort_by: sortState.sort_by,
sort_order: sortState.sort_order
}, { signal: ctrl.signal })
if (ctrl.signal.aborted || abortController !== ctrl) return
......@@ -825,6 +839,13 @@ function handlePageSizeChange(pageSize: number) {
loadChannels()
}
function handleSort(key: string, order: 'asc' | 'desc') {
sortState.sort_by = key
sortState.sort_order = order
pagination.page = 1
loadChannels()
}
// ── Dialog ──
function resetForm() {
form.name = ''
......
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