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

merge v0.1.111

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