feat(notification): show device monitoring stats

This commit is contained in:
2026-09-10 20:04:02 +08:00
parent b6b5119e0f
commit 573b0c4bc7
3 changed files with 71 additions and 23 deletions
+11 -1
View File
@@ -1,6 +1,16 @@
import { api } from '@/api/client'
import type { ListParams, NotificationDevice, PaginatedResponse } from '@/api/types'
import type {
ApiResponse,
ListParams,
NotificationDevice,
NotificationDeviceStats,
PaginatedResponse,
} from '@/api/types'
export function fetchDevices(params: ListParams) {
return api.get<PaginatedResponse<NotificationDevice>>('/api/admin/v1/notification/devices', params)
}
export function fetchDeviceStats() {
return api.get<ApiResponse<NotificationDeviceStats>>('/api/admin/v1/notification/devices/stats')
}
+19 -9
View File
@@ -389,15 +389,25 @@ export interface LocaleRow {
}
export interface NotificationDevice {
id: number | string
device_id?: string
user_uid?: string | null
platform?: string
model?: string | null
is_online?: boolean
active_sessions_count?: number
last_seen_at?: string | null
created_at?: string
kind: 'device'
id: string
device_client_id: string
platform: 'ios' | 'android' | 'web' | 'desktop'
push_provider: string | null
app_key: string | null
device_model: string | null
is_online: boolean
has_push_token: boolean
active_sessions_count: number
last_seen_at: string | null
created_at: string
updated_at: string
}
export interface NotificationDeviceStats {
total_devices: number
online_devices: number
platform_stats: Partial<Record<NotificationDevice['platform'], number>>
}
export type ReportStatus = 'open' | 'resolved' | 'dismissed'
+41 -13
View File
@@ -1,13 +1,15 @@
import { createFileRoute } from '@tanstack/react-router'
import { useQuery } from '@tanstack/react-query'
import { PERM, requirePermission } from '@/auth/permissions'
import { useState } from 'react'
import type { ColumnDef } from '@tanstack/react-table'
import type { NotificationDevice } from '@/api/types'
import { fetchDevices } from '@/api/modules/devices'
import { fetchDevices, fetchDeviceStats } from '@/api/modules/devices'
import { useListPage } from '@/lib/list-page'
import { DataTable } from '@/components/data-table/data-table'
import { PageHeader } from '@/components/page-header'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
Select,
SelectContent,
@@ -26,9 +28,9 @@ const columns: ColumnDef<NotificationDevice>[] = [
header: '设备',
cell: ({ row }) => (
<div>
<div className="font-medium">{row.original.model ?? row.original.device_id ?? '—'}</div>
<div className="font-medium">{row.original.device_model ?? row.original.device_client_id}</div>
<code className="font-mono text-xs text-muted-foreground">
{String(row.original.device_id ?? row.original.id)}
{row.original.device_client_id}
</code>
</div>
),
@@ -37,10 +39,8 @@ const columns: ColumnDef<NotificationDevice>[] = [
header: '平台',
cell: ({ row }) => <Badge variant="secondary">{row.original.platform ?? '—'}</Badge>,
},
{
header: '用户',
cell: ({ row }) => <code className="font-mono text-xs">{row.original.user_uid ?? '未绑定'}</code>,
},
{ header: '应用', cell: ({ row }) => row.original.app_key ?? '—' },
{ header: '推送', cell: ({ row }) => (row.original.has_push_token ? row.original.push_provider ?? '已配置' : '未配置') },
{
header: '在线',
cell: ({ row }) =>
@@ -63,22 +63,35 @@ const columns: ColumnDef<NotificationDevice>[] = [
function DevicesPage() {
const [platform, setPlatform] = useState<string>('all')
const [online, setOnline] = useState<string>('all')
const stats = useQuery({ queryKey: ['device-stats'], queryFn: fetchDeviceStats })
const list = useListPage(
'devices',
fetchDevices,
platform === 'all' ? {} : { platform },
{
...(platform === 'all' ? {} : { platform }),
...(online === 'all' ? {} : { is_online: online === 'online' }),
},
)
const stat = stats.data?.data
return (
<div>
<PageHeader
title="推送设备"
description="注册的通知设备与在线状态"
actions={
actions={<div className="flex gap-2">
<Select value={online} onValueChange={setOnline}>
<SelectTrigger size="sm" className="w-28"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="online">线</SelectItem>
<SelectItem value="offline">线</SelectItem>
</SelectContent>
</Select>
<Select value={platform} onValueChange={setPlatform}>
<SelectTrigger size="sm" className="w-32">
<SelectValue />
</SelectTrigger>
<SelectTrigger size="sm" className="w-32"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="ios">iOS</SelectItem>
@@ -87,8 +100,14 @@ function DevicesPage() {
<SelectItem value="desktop">Desktop</SelectItem>
</SelectContent>
</Select>
}
</div>}
/>
<div className="mb-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<DeviceStatCard title="设备总数" value={stat?.total_devices} />
<DeviceStatCard title="当前在线" value={stat?.online_devices} />
<DeviceStatCard title="iOS" value={stat?.platform_stats.ios} />
<DeviceStatCard title="Android" value={stat?.platform_stats.android} />
</div>
<DataTable
columns={columns}
data={list.rows}
@@ -100,3 +119,12 @@ function DevicesPage() {
</div>
)
}
function DeviceStatCard({ title, value }: { title: string; value?: number }) {
return (
<Card>
<CardHeader className="pb-2"><CardTitle className="text-sm font-medium">{title}</CardTitle></CardHeader>
<CardContent><div className="text-2xl font-semibold">{value ?? '—'}</div></CardContent>
</Card>
)
}