diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 578da41..f29bb30 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -58,11 +58,11 @@ export default function Dashboard() { } trend="up" trendValue={`+${stats?.newUsersToday ?? 0}`} trendLabel="今日新增" /> } trend="up" trendValue={`+${stats?.newKbsToday ?? 0}`} trendLabel="今日新增" /> - } /> + } /> } suffix={`${stats?.totalFiles ?? 0} 个文件`} /> - } trend="up" trendValue={`${stats?.totalAiCallsToday ?? 0} 次`} trendLabel="今日调用" /> + } trend="up" trendValue={`${stats?.todayAiCalls ?? 0} 次`} trendLabel="今日调用" /> } trend={stats?.failedImportCount ? 'down' : 'up'} trendValue={stats?.failedImportCount ? `${stats.failedImportCount} 失败` : '全部成功'} trendLabel="状态" /> } trend={stats?.failedTasks ? 'down' : 'up'} trendValue={stats?.failedTasks ? '需要关注' : '无异常'} trendLabel="7 天内" /> } trend={stats?.upcomingExpirations ? 'down' : 'up'} trendValue="30 天内" trendLabel="到期" /> diff --git a/src/pages/NotificationAdmin.tsx b/src/pages/NotificationAdmin.tsx index a61baa9..a0d32e8 100644 --- a/src/pages/NotificationAdmin.tsx +++ b/src/pages/NotificationAdmin.tsx @@ -4,6 +4,7 @@ import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { api } from '@/services/http-client' import type { NotificationTemplate, NotificationLog } from '@/types/api' +import { BellOutlined, SendOutlined, ExclamationCircleOutlined } from '@ant-design/icons' const { Title } = Typography @@ -115,7 +116,7 @@ export default function NotificationAdmin() { /> - + + + ) } + +function AdminNotificationsCard() { + const qc = useQueryClient() + + const { data: adminNotifs } = useQuery({ + queryKey: ['admin', 'admin-notifications'], + queryFn: (): Promise => api.get('/admin-api/admin-notifications').then(d => d ?? []), + refetchInterval: 30_000, + }) + + const sendAlert = useMutation({ + mutationFn: (body: { type: string; title: string; content?: string }) => + api.post('/admin-api/admin-notifications/send', body), + onSuccess: () => { message.success('通知已发送'); qc.invalidateQueries({ queryKey: ['admin', 'admin-notifications'] }) }, + }) + + const markRead = useMutation({ + mutationFn: (id: string) => api.post(`/admin-api/admin-notifications/${id}/read`), + onSuccess: () => qc.invalidateQueries({ queryKey: ['admin', 'admin-notifications'] }), + }) + + const columns = [ + { title: '类型', dataIndex: 'type', width: 100, render: (t: string) => {t} }, + { title: '标题', dataIndex: 'title', width: 250, ellipsis: true }, + { title: '内容', dataIndex: 'content', width: 200, ellipsis: true, render: (c: string) => c || '-' }, + { title: '已读', dataIndex: 'readAt', width: 70, render: (d: string | null) => d ? : }, + { title: '时间', dataIndex: 'createdAt', width: 120, render: (d: string) => new Date(d).toLocaleString() }, + { title: '操作', width: 80, render: (_: any, r: any) => !r.readAt ? : null }, + ] + + return ( + Admin 通知} extra={ + + + + + }> +
+ + ) +} diff --git a/src/pages/ReleaseAdmin.tsx b/src/pages/ReleaseAdmin.tsx index 4b4fa71..1cc1fa6 100644 --- a/src/pages/ReleaseAdmin.tsx +++ b/src/pages/ReleaseAdmin.tsx @@ -12,6 +12,7 @@ export default function ReleaseAdmin() { const [tab, setTab] = useState('changelogs') const [modalOpen, setModalOpen] = useState(false) const [editing, setEditing] = useState(null) + const [checklistVer, setChecklistVer] = useState('1.0') const [form] = Form.useForm() const qc = useQueryClient() @@ -29,7 +30,7 @@ export default function ReleaseAdmin() { const { data: checklist } = useQuery({ queryKey: ['release', 'checklist'], - queryFn: () => api.get('/admin-api/release/checklists/v1.0').then(d => d ?? []), + queryFn: () => api.get(`/admin-api/release/checklists/${checklistVer}`).then(d => d ?? []), enabled: tab === 'checklist', }) @@ -121,6 +122,10 @@ export default function ReleaseAdmin() { key: 'checklist', label: '回归清单', children: ( + + 版本: + setChecklistVer(e.target.value)} style={{ width: 100 }} placeholder="1.0" /> + {(checklist || []).map((item: any) => (
toggleCheck.mutate({ id: item.id, checked: e.target.checked })}> diff --git a/src/pages/Secrets.tsx b/src/pages/Secrets.tsx index 434a204..7040772 100644 --- a/src/pages/Secrets.tsx +++ b/src/pages/Secrets.tsx @@ -1,20 +1,23 @@ -import { PlusOutlined, DeleteOutlined, KeyOutlined } from '@ant-design/icons' +import { PlusOutlined, DeleteOutlined, KeyOutlined, DollarOutlined, RotateRightOutlined, StopOutlined } from '@ant-design/icons' import { useState } from 'react' -import { useQuery, useQueryClient } from '@tanstack/react-query' -import { Table, Button, Typography, App, Modal, Input, Select, Tag, Tabs, DatePicker } from 'antd' +import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query' +import { Table, Button, Typography, App, Modal, Input, Select, Tag, Tabs, DatePicker, InputNumber, message } from 'antd' import { api } from '@/services/http-client' import dayjs from 'dayjs' const { Title, Text } = Typography function SecretsPage() { - const { modal, message } = App.useApp() + const { modal } = App.useApp() const qc = useQueryClient() const [addOpen, setAddOpen] = useState(false) + const [billOpen, setBillOpen] = useState(false) const [form, setForm] = useState({ name: '', provider: 'deepseek', value: '', expiresAt: '' }) + const [billForm, setBillForm] = useState({ provider: 'deepseek', billMonth: dayjs().format('YYYY-MM'), amount: 0, notes: '' }) const { data: secrets } = useQuery({ queryKey: ['secrets'], queryFn: (): Promise => api.get('/admin-api/secrets') }) const { data: logs } = useQuery({ queryKey: ['secrets', 'logs'], queryFn: (): Promise => api.get('/admin-api/secrets/logs') }) + const { data: bills } = useQuery({ queryKey: ['vendor', 'bills'], queryFn: (): Promise => api.get('/admin-api/vendor/bills').then(d => d ?? []) }) const addSecret = async () => { await api.post('/admin-api/secrets', form) @@ -22,18 +25,45 @@ function SecretsPage() { setAddOpen(false); qc.invalidateQueries({ queryKey: ['secrets'] }) } + const rotateKey = useMutation({ + mutationFn: (id: string) => api.post(`/admin-api/vendor/secrets/${id}/rotate`), + onSuccess: () => { message.success('已轮换'); qc.invalidateQueries({ queryKey: ['secrets'] }) }, + }) + + const revokeKey = useMutation({ + mutationFn: (id: string) => api.post(`/admin-api/vendor/secrets/${id}/revoke`), + onSuccess: () => { message.success('已吊销'); qc.invalidateQueries({ queryKey: ['secrets'] }) }, + }) + const deleteSecret = (id: string) => modal.confirm({ title: '删除密钥', okType: 'danger', onOk: async () => { await api.delete(`/admin-api/secrets/${id}`); qc.invalidateQueries({ queryKey: ['secrets'] }) }, }) + const addBill = async () => { + await api.post('/admin-api/vendor/bills', billForm) + message.success('账单已录入') + setBillOpen(false); qc.invalidateQueries({ queryKey: ['vendor', 'bills'] }) + } + + const deleteBill = (id: string) => modal.confirm({ + title: '删除账单', okType: 'danger', + onOk: async () => { await api.delete(`/admin-api/vendor/bills/${id}`); qc.invalidateQueries({ queryKey: ['vendor', 'bills'] }) }, + }) + const secCols = [ - { title: '名称', dataIndex: 'name', width: 150 }, - { title: '服务商', dataIndex: 'provider', width: 100, render: (v: string) => {v} }, + { title: '名称', dataIndex: 'name', width: 120 }, + { title: '服务商', dataIndex: 'provider', width: 90, render: (v: string) => {v} }, { title: '末四位', dataIndex: 'maskLast4', width: 80, render: (v: string) => ****{v} }, - { title: '状态', dataIndex: 'status', width: 70, render: (v: string) => {v} }, + { title: '状态', dataIndex: 'status', width: 70, render: (v: string) => {v} }, { title: '到期', dataIndex: 'expiresAt', width: 100, render: (d: string) => d ? dayjs(d).format('MM-DD') : 永久 }, - { title: '操作', width: 80, render: (_:any, r:any) => + + + <KeyOutlined /> 密钥与供应商
}, + { key: 'keys', label: '密钥列表', children:
}, { key: 'logs', label: '访问日志', children:
}, + { key: 'bills', label: '服务商账单', children:
}, ]} /> setAddOpen(false)} okText="添加"> setForm({...form, name: e.target.value})} style={{ marginBottom: 12 }} /> @@ -58,6 +98,12 @@ function SecretsPage() { setForm({...form, value: e.target.value})} style={{ marginBottom: 12 }} /> setForm({...form, expiresAt: d?.toISOString() || ''})} style={{ width: '100%' }} /> + setBillOpen(false)} okText="录入"> + setBillForm({...billForm, billMonth: e.target.value})} style={{ marginBottom: 12 }} /> + setBillForm({...billForm, amount: v || 0})} style={{ width: '100%', marginBottom: 12 }} min={0} step={0.01} /> + setBillForm({...billForm, notes: e.target.value})} /> + ) } diff --git a/src/services/mock-data.ts b/src/services/mock-data.ts index 57d1b64..836c346 100644 --- a/src/services/mock-data.ts +++ b/src/services/mock-data.ts @@ -17,7 +17,7 @@ export const MOCK_DASHBOARD_STATS: DashboardStats = { activeUsersToday: 347, totalKnowledgeBases: 892, newKbsToday: 15, - totalAiCallsToday: 4521, + todayAiCalls: 4521, totalFiles: 3412, totalStorageBytes: 15_728_640_000, userTrend: makeTrend(30, 40, 30), diff --git a/src/types/admin.ts b/src/types/admin.ts index 3c1979f..f6f85c5 100644 --- a/src/types/admin.ts +++ b/src/types/admin.ts @@ -23,7 +23,7 @@ export interface DashboardStats { activeUsersToday: number totalKnowledgeBases: number newKbsToday: number - totalAiCallsToday: number + todayAiCalls: number totalFiles: number totalStorageBytes: number todayImportCount: number