Skip to content

Commit c8e0096

Browse files
committed
add metrics
1 parent 52a042e commit c8e0096

6 files changed

Lines changed: 731 additions & 1 deletion

File tree

src/components/sider.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
AppstoreOutlined,
33
FileTextOutlined,
4+
LineChartOutlined,
45
PlusOutlined,
56
SettingOutlined,
67
UserOutlined,
@@ -197,6 +198,13 @@ const SiderMenu = ({ selectedKeys }: SiderMenuProps) => {
197198
icon: <FileTextOutlined />,
198199
label: <Link to={rootRouterPath.auditLogs}>操作日志</Link>,
199200
},
201+
{
202+
key: 'realtime-metrics',
203+
icon: <LineChartOutlined />,
204+
label: (
205+
<Link to={rootRouterPath.realtimeMetrics}>实时数据</Link>
206+
),
207+
},
200208
{
201209
key: 'apps',
202210
icon: <AppstoreOutlined />,
@@ -264,6 +272,12 @@ const SiderMenu = ({ selectedKeys }: SiderMenuProps) => {
264272
<Link to={rootRouterPath.adminUsers}>用户管理</Link>
265273
),
266274
},
275+
{
276+
key: 'admin-apps',
277+
label: (
278+
<Link to={rootRouterPath.adminApps}>应用管理</Link>
279+
),
280+
},
267281
{
268282
key: 'admin-metrics',
269283
label: (

src/pages/admin-apps.tsx

Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import { CopyOutlined, EditOutlined, SearchOutlined } from '@ant-design/icons';
2+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
3+
import {
4+
Button,
5+
Card,
6+
Form,
7+
Input,
8+
message,
9+
Modal,
10+
Select,
11+
Space,
12+
Spin,
13+
Table,
14+
Typography,
15+
} from 'antd';
16+
import dayjs from 'dayjs';
17+
import { useEffect, useState } from 'react';
18+
import { api } from '@/services/api';
19+
20+
const { Title } = Typography;
21+
22+
export const Component = () => {
23+
const queryClient = useQueryClient();
24+
const [searchKeyword, setSearchKeyword] = useState('');
25+
const [debouncedSearch, setDebouncedSearch] = useState('');
26+
const [isModalOpen, setIsModalOpen] = useState(false);
27+
const [editingApp, setEditingApp] = useState<AdminApp | null>(null);
28+
const [form] = Form.useForm();
29+
30+
// Debounce search
31+
useEffect(() => {
32+
const timer = setTimeout(() => setDebouncedSearch(searchKeyword), 300);
33+
return () => clearTimeout(timer);
34+
}, [searchKeyword]);
35+
36+
const { data, isLoading } = useQuery({
37+
queryKey: ['adminApps', debouncedSearch],
38+
queryFn: () => api.searchApps(debouncedSearch || undefined),
39+
});
40+
41+
const updateMutation = useMutation({
42+
mutationFn: ({ id, data }: { id: number; data: Partial<AdminApp> }) =>
43+
api.updateApp(id, data),
44+
onSuccess: () => {
45+
message.success('应用信息已更新');
46+
setIsModalOpen(false);
47+
queryClient.invalidateQueries({ queryKey: ['adminApps'] });
48+
},
49+
onError: (error) => {
50+
message.error((error as Error).message);
51+
},
52+
});
53+
54+
const handleEdit = (record: AdminApp) => {
55+
setEditingApp(record);
56+
form.setFieldsValue({
57+
name: record.name,
58+
platform: record.platform,
59+
userId: record.userId,
60+
downloadUrl: record.downloadUrl || '',
61+
status: record.status || '',
62+
ignoreBuildTime: record.ignoreBuildTime,
63+
});
64+
setIsModalOpen(true);
65+
};
66+
67+
const handleSave = async () => {
68+
try {
69+
const values = await form.validateFields();
70+
if (!editingApp) return;
71+
72+
const updateData: Partial<AdminApp> = {
73+
name: values.name,
74+
platform: values.platform,
75+
userId: values.userId || null,
76+
downloadUrl: values.downloadUrl || null,
77+
status: values.status || null,
78+
ignoreBuildTime: values.ignoreBuildTime || null,
79+
};
80+
81+
updateMutation.mutate({ id: editingApp.id, data: updateData });
82+
} catch (error) {
83+
message.error((error as Error).message);
84+
}
85+
};
86+
87+
const columns = [
88+
{
89+
title: 'ID',
90+
dataIndex: 'id',
91+
key: 'id',
92+
width: 60,
93+
},
94+
{
95+
title: '名称',
96+
dataIndex: 'name',
97+
key: 'name',
98+
width: 150,
99+
},
100+
{
101+
title: 'App Key',
102+
dataIndex: 'appKey',
103+
key: 'appKey',
104+
width: 200,
105+
render: (key: string) => (
106+
<Space>
107+
<span className="font-mono text-xs">{key}</span>
108+
<Button
109+
type="text"
110+
size="small"
111+
icon={<CopyOutlined />}
112+
onClick={() => {
113+
navigator.clipboard.writeText(key);
114+
message.success('已复制');
115+
}}
116+
/>
117+
</Space>
118+
),
119+
},
120+
{
121+
title: '平台',
122+
dataIndex: 'platform',
123+
key: 'platform',
124+
width: 80,
125+
render: (platform: string) => (
126+
<span className={
127+
platform === 'ios' ? 'text-blue-600' :
128+
platform === 'android' ? 'text-green-600' : 'text-orange-600'
129+
}>
130+
{platform}
131+
</span>
132+
),
133+
},
134+
{
135+
title: '用户ID',
136+
dataIndex: 'userId',
137+
key: 'userId',
138+
width: 80,
139+
},
140+
{
141+
title: '状态',
142+
dataIndex: 'status',
143+
key: 'status',
144+
width: 80,
145+
render: (status: string | null) => status || '-',
146+
},
147+
{
148+
title: '忽略构建时间',
149+
dataIndex: 'ignoreBuildTime',
150+
key: 'ignoreBuildTime',
151+
width: 120,
152+
render: (v: string | null) => (
153+
<span className={v === 'enabled' ? 'text-green-600' : ''}>
154+
{v === 'enabled' ? '是' : v === 'disabled' ? '否' : '-'}
155+
</span>
156+
),
157+
},
158+
{
159+
title: '创建时间',
160+
dataIndex: 'createdAt',
161+
key: 'createdAt',
162+
width: 160,
163+
render: (date: string | undefined) =>
164+
date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '-',
165+
},
166+
{
167+
title: '操作',
168+
key: 'action',
169+
width: 80,
170+
render: (_: unknown, record: AdminApp) => (
171+
<Button
172+
type="link"
173+
icon={<EditOutlined />}
174+
onClick={() => handleEdit(record)}
175+
>
176+
编辑
177+
</Button>
178+
),
179+
},
180+
];
181+
182+
return (
183+
<div className="p-6">
184+
<Card>
185+
<div className="flex justify-between items-center mb-4">
186+
<Title level={4} className="m-0!">
187+
应用管理
188+
</Title>
189+
<Input
190+
placeholder="搜索应用名称或 App Key"
191+
prefix={<SearchOutlined />}
192+
value={searchKeyword}
193+
onChange={(e) => setSearchKeyword(e.target.value)}
194+
style={{ width: 300 }}
195+
allowClear
196+
/>
197+
</div>
198+
199+
<Spin spinning={isLoading}>
200+
<Table
201+
dataSource={data?.data || []}
202+
columns={columns}
203+
rowKey="id"
204+
pagination={{ pageSize: 20 }}
205+
scroll={{ x: 1000 }}
206+
/>
207+
</Spin>
208+
</Card>
209+
210+
<Modal
211+
title={`编辑应用: ${editingApp?.name}`}
212+
open={isModalOpen}
213+
onCancel={() => setIsModalOpen(false)}
214+
footer={[
215+
<Button key="cancel" onClick={() => setIsModalOpen(false)}>
216+
取消
217+
</Button>,
218+
<Button
219+
key="save"
220+
type="primary"
221+
loading={updateMutation.isPending}
222+
onClick={handleSave}
223+
>
224+
保存
225+
</Button>,
226+
]}
227+
width={600}
228+
>
229+
<Form form={form} layout="vertical" className="mt-4">
230+
<Space className="w-full" direction="vertical" size="middle">
231+
<Form.Item name="name" label="名称" className="mb-0!" rules={[{ required: true }]}>
232+
<Input />
233+
</Form.Item>
234+
<Form.Item name="platform" label="平台" className="mb-0!">
235+
<Select
236+
options={[
237+
{ value: 'ios', label: 'iOS' },
238+
{ value: 'android', label: 'Android' },
239+
{ value: 'harmony', label: 'HarmonyOS' },
240+
]}
241+
/>
242+
</Form.Item>
243+
<Form.Item name="userId" label="用户ID" className="mb-0!">
244+
<Input type="number" placeholder="留空表示无归属" />
245+
</Form.Item>
246+
<Form.Item name="downloadUrl" label="下载链接" className="mb-0!">
247+
<Input placeholder="应用商店链接" />
248+
</Form.Item>
249+
<Form.Item name="status" label="状态" className="mb-0!">
250+
<Input placeholder="自定义状态" />
251+
</Form.Item>
252+
<Form.Item name="ignoreBuildTime" label="忽略构建时间" className="mb-0!">
253+
<Select
254+
allowClear
255+
options={[
256+
{ value: 'enabled', label: '启用' },
257+
{ value: 'disabled', label: '禁用' },
258+
]}
259+
/>
260+
</Form.Item>
261+
</Space>
262+
</Form>
263+
</Modal>
264+
</div>
265+
);
266+
};

0 commit comments

Comments
 (0)