whistle.mock-plugins 1.1.3-dev.5 → 1.1.3-dev.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,69 +1,63 @@
1
1
  /**
2
- * MockDataV2.js - 功能模块管理页面(重构后版本)
3
- * 这是重构后的新版本界面,提供更好的用户体验和功能优化
4
- * 当前内容与V1版本相同,等待后续重构
2
+ * MockDataV2.js - 功能模块管理页面(V2版本)
3
+ * 功能和UI与V1版本完全一致,仅保留V2版本标识
5
4
  */
6
5
 
7
6
  import React, { useState, useEffect } from 'react';
8
7
  import { useHistory } from 'react-router-dom';
9
8
  import AppLayout from '../components/AppLayout';
10
- import {
11
- Form, Input, Modal, Button, message, Card, Switch,
12
- Row, Col, Badge, Popconfirm, Alert, Space, Select,
13
- Tooltip, Menu, Dropdown, Pagination, AutoComplete
14
- } from 'antd';
15
- import {
16
- PlusOutlined, DeleteOutlined, EditOutlined, SettingOutlined,
17
- ExportOutlined, ImportOutlined, SearchOutlined, FilterOutlined,
18
- SortAscendingOutlined, ReloadOutlined, ClearOutlined, DownloadOutlined
19
- } from '@ant-design/icons';
9
+ import { Card, Row, Col, Button, Modal, Form, Input, message, Switch, Empty, Spin, Typography, Tooltip, Badge, Select, Pagination } from 'antd';
10
+ import { PlusOutlined, EditOutlined, DeleteOutlined, ApiOutlined, ExportOutlined, InfoCircleOutlined, CalendarOutlined, SortAscendingOutlined } from '@ant-design/icons';
20
11
  import '../styles/mock-data.css';
12
+ import axios from 'axios';
21
13
 
22
- const { Option } = Select;
14
+ const { Text, Title, Paragraph } = Typography;
23
15
  const { TextArea } = Input;
24
16
 
25
- // 刷新缓存工具函数
17
+ // 刷新缓存服务
26
18
  const flushCache = async () => {
27
19
  try {
28
- await fetch('/cgi-bin/flush-cache', { method: 'POST' });
29
- console.log('缓存刷新成功');
20
+ const response = await axios.get(`/_flush_cache`);
21
+ return response.data;
30
22
  } catch (error) {
31
- console.error('缓存刷新失败:', error);
23
+ console.error('刷新缓存失败:', error);
24
+ throw error;
32
25
  }
33
26
  };
34
27
 
35
28
  const MockData = () => {
36
29
  const history = useHistory();
37
-
38
- // 功能模块状态
39
- const [features, setFeatures] = useState([]);
40
- const [form] = Form.useForm();
41
- const [loading, setLoading] = useState(false);
42
- const [modalVisible, setModalVisible] = useState(false);
43
- const [editingFeature, setEditingFeature] = useState(null);
30
+ const [mockFeatures, setMockFeatures] = useState([]);
31
+ const [showModal, setShowModal] = useState(false);
32
+ const [currentFeature, setCurrentFeature] = useState(null);
44
33
 
45
- // 搜索和筛选状态
46
- const [searchTerm, setSearchTerm] = useState('');
47
- const [filteredFeatures, setFilteredFeatures] = useState([]);
48
- const [statusFilter, setStatusFilter] = useState('all'); // all, active, inactive
49
- const [sortBy, setSortBy] = useState('name'); // name, createTime, updateTime
34
+ const [loading, setLoading] = useState(true);
35
+ const [form] = Form.useForm();
50
36
 
51
- // 分页状态
52
- const [currentPage, setCurrentPage] = useState(1);
53
- const [pageSize, setPageSize] = useState(12);
37
+ // 排序和分页配置(支持缓存)
38
+ const [listConfig, setListConfig] = useState(() => {
39
+ const cached = localStorage.getItem('feature-list-config');
40
+ return cached ? JSON.parse(cached) : {
41
+ sortBy: 'name', // name, createdAt, interfaceCount, active
42
+ sortOrder: 'ascend', // ascend, descend
43
+ pageSize: 12,
44
+ current: 1
45
+ };
46
+ });
54
47
 
55
- // 导入/导出状态
56
- const [exportLoading, setExportLoading] = useState(false);
57
- const [importLoading, setImportLoading] = useState(false);
48
+ // 保存列表配置到localStorage
49
+ const saveListConfig = (config) => {
50
+ const newConfig = { ...listConfig, ...config };
51
+ setListConfig(newConfig);
52
+ localStorage.setItem('feature-list-config', JSON.stringify(newConfig));
53
+ };
58
54
 
55
+ // 加载功能列表
59
56
  useEffect(() => {
60
57
  fetchFeatures();
61
58
  }, []);
62
59
 
63
- useEffect(() => {
64
- filterAndSortFeatures();
65
- }, [features, searchTerm, statusFilter, sortBy]);
66
-
60
+ // 获取所有功能模块
67
61
  const fetchFeatures = async () => {
68
62
  try {
69
63
  setLoading(true);
@@ -71,303 +65,461 @@ const MockData = () => {
71
65
  const result = await response.json();
72
66
 
73
67
  if (result.code === 0) {
74
- setFeatures(result.data || []);
68
+ setMockFeatures(result.data || []);
75
69
  } else {
76
- throw new Error(result.message || '获取功能模块失败');
70
+ console.error('获取功能模块失败:', result.message);
71
+ message.error('获取功能模块失败: ' + result.message);
77
72
  }
78
73
  } catch (error) {
79
- console.error('获取功能模块失败:', error);
80
- message.error('获取功能模块失败: ' + error.message);
81
- setFeatures([]);
74
+ console.error('获取功能模块错误:', error);
75
+ message.error('获取功能模块失败, 请检查网络连接');
82
76
  } finally {
83
77
  setLoading(false);
84
78
  }
85
79
  };
86
80
 
87
- const filterAndSortFeatures = () => {
88
- let filtered = [...features];
89
-
90
- // 搜索筛选
91
- if (searchTerm) {
92
- const term = searchTerm.toLowerCase();
93
- filtered = filtered.filter(feature =>
94
- feature.name.toLowerCase().includes(term) ||
95
- (feature.description && feature.description.toLowerCase().includes(term))
96
- );
97
- }
98
-
99
- // 状态筛选
100
- if (statusFilter === 'active') {
101
- filtered = filtered.filter(feature => feature.active !== false);
102
- } else if (statusFilter === 'inactive') {
103
- filtered = filtered.filter(feature => feature.active === false);
81
+ const openModal = (feature = null) => {
82
+ if (feature) {
83
+ setCurrentFeature(feature);
84
+ form.setFieldsValue({
85
+ name: feature.name,
86
+ description: feature.description,
87
+ active: feature.active !== false
88
+ });
89
+ } else {
90
+ setCurrentFeature(null);
91
+ form.resetFields();
92
+ form.setFieldsValue({
93
+ active: true
94
+ });
104
95
  }
105
-
106
- // 排序
107
- filtered.sort((a, b) => {
108
- switch (sortBy) {
109
- case 'name':
110
- return a.name.localeCompare(b.name);
111
- case 'createTime':
112
- return new Date(b.createTime || 0) - new Date(a.createTime || 0);
113
- case 'updateTime':
114
- return new Date(b.updateTime || 0) - new Date(a.updateTime || 0);
115
- default:
116
- return 0;
117
- }
118
- });
119
-
120
- setFilteredFeatures(filtered);
121
- setCurrentPage(1); // 重置到第一页
122
- };
123
-
124
- const handleAddFeature = () => {
125
- form.resetFields();
126
- setEditingFeature(null);
127
- setModalVisible(true);
96
+ setShowModal(true);
128
97
  };
129
98
 
130
- const handleEditFeature = (feature) => {
131
- form.setFieldsValue({
132
- name: feature.name,
133
- description: feature.description
134
- });
135
- setEditingFeature(feature);
136
- setModalVisible(true);
99
+ const closeModal = () => {
100
+ setShowModal(false);
137
101
  };
138
102
 
139
- const handleDeleteFeature = async (id) => {
103
+ // 更新成功后刷新缓存
104
+ const refreshCacheAfterUpdate = async () => {
140
105
  try {
141
- const response = await fetch(`/cgi-bin/features?id=${id}`, {
142
- method: 'DELETE'
143
- });
144
- const result = await response.json();
145
-
146
- if (result.code === 0) {
147
- message.success('功能模块删除成功');
148
- fetchFeatures();
149
-
150
- // 刷新缓存
151
- await flushCache();
152
- } else {
153
- throw new Error(result.message || '删除失败');
154
- }
106
+ await flushCache();
107
+ // 不显示提示,避免干扰主操作的反馈
155
108
  } catch (error) {
156
- console.error('删除功能模块失败:', error);
157
- message.error('删除功能模块失败: ' + error.message);
109
+ // 记录错误但不影响用户体验
110
+ console.error('刷新缓存失败,可能需要等待缓存自动过期:', error);
158
111
  }
159
112
  };
160
113
 
161
- const handleToggleActive = async (id, currentActive) => {
114
+ const handleSubmit = async (values) => {
162
115
  try {
163
- const response = await fetch(`/cgi-bin/features`, {
164
- method: 'PATCH',
116
+ const featureData = {
117
+ ...values
118
+ };
119
+
120
+ // 如果是编辑已有功能,添加ID
121
+ if (currentFeature) {
122
+ featureData.id = currentFeature.id;
123
+ }
124
+
125
+ setLoading(true);
126
+ const response = await fetch('/cgi-bin/features', {
127
+ method: 'POST',
165
128
  headers: {
166
129
  'Content-Type': 'application/json'
167
130
  },
168
- body: JSON.stringify({
169
- id,
170
- active: !currentActive
171
- })
131
+ body: JSON.stringify(featureData)
172
132
  });
173
133
 
174
134
  const result = await response.json();
175
135
 
176
136
  if (result.code === 0) {
177
- message.success(`功能模块${!currentActive ? '启用' : '禁用'}成功`);
137
+ // 刷新功能列表
138
+ message.success(currentFeature ? '功能模块更新成功' : '功能模块创建成功');
178
139
  fetchFeatures();
140
+ closeModal();
179
141
 
180
142
  // 刷新缓存
181
- await flushCache();
143
+ refreshCacheAfterUpdate();
182
144
  } else {
183
- throw new Error(result.message || '操作失败');
145
+ message.error('操作失败: ' + result.message);
184
146
  }
185
147
  } catch (error) {
186
- console.error('切换功能模块状态失败:', error);
187
- message.error('操作失败: ' + error.message);
148
+ console.error('保存功能模块错误:', error);
149
+ message.error('保存失败: ' + error.message);
150
+ } finally {
151
+ setLoading(false);
188
152
  }
189
153
  };
190
154
 
191
- const handleSubmit = async () => {
155
+ const handleToggleActive = async (id, currentActive) => {
192
156
  try {
193
- const values = await form.validateFields();
194
-
195
- const requestData = {
196
- name: values.name,
197
- description: values.description || '',
198
- active: true
199
- };
200
-
201
- const url = editingFeature
202
- ? `/cgi-bin/features?id=${editingFeature.id}`
203
- : '/cgi-bin/features';
204
-
205
- const method = editingFeature ? 'PUT' : 'POST';
206
-
207
- const response = await fetch(url, {
208
- method,
157
+ const response = await fetch(`/cgi-bin/features?id=${id}`, {
158
+ method: 'PATCH',
209
159
  headers: {
210
160
  'Content-Type': 'application/json'
211
161
  },
212
- body: JSON.stringify(requestData)
162
+ body: JSON.stringify({
163
+ active: !currentActive
164
+ })
213
165
  });
214
-
166
+
215
167
  const result = await response.json();
216
168
 
217
169
  if (result.code === 0) {
218
- message.success(editingFeature ? '功能模块更新成功' : '功能模块创建成功');
219
- setModalVisible(false);
220
- fetchFeatures();
170
+ message.success(`${currentActive ? '禁用' : '启用'}功能模块成功`);
171
+ // 更新本地状态
172
+ setMockFeatures(mockFeatures.map(feature =>
173
+ feature.id === id ? { ...feature, active: !currentActive } : feature
174
+ ));
221
175
 
222
- // 刷新缓存
223
- await flushCache();
176
+ // 刷新缓存 - 功能启用状态变化是最关键的需要刷新缓存的操作
177
+ refreshCacheAfterUpdate();
224
178
  } else {
225
- throw new Error(result.message || '操作失败');
179
+ message.error(`${currentActive ? '禁用' : '启用'}功能模块失败: ${result.message}`);
226
180
  }
227
181
  } catch (error) {
228
- console.error('提交失败:', error);
229
- message.error('操作失败: ' + error.message);
182
+ console.error(`${currentActive ? '禁用' : '启用'}功能模块错误:`, error);
183
+ message.error(`操作失败: ${error.message}`);
230
184
  }
231
185
  };
232
186
 
233
- const handleCancel = () => {
234
- setModalVisible(false);
187
+ const deleteFeature = async (id) => {
188
+ Modal.confirm({
189
+ title: '确定要删除此功能吗?',
190
+ content: '这将删除所有相关的接口和模拟数据,此操作无法恢复。',
191
+ okText: '删除',
192
+ okType: 'danger',
193
+ cancelText: '取消',
194
+ onOk: async () => {
195
+ try {
196
+ setLoading(true);
197
+ const response = await fetch(`/cgi-bin/features?id=${id}`, {
198
+ method: 'DELETE'
199
+ });
200
+
201
+ const result = await response.json();
202
+
203
+ if (result.code === 0) {
204
+ message.success('功能模块已成功删除');
205
+ // 更新本地状态
206
+ setMockFeatures(mockFeatures.filter(f => f.id !== id));
207
+
208
+ // 刷新缓存
209
+ refreshCacheAfterUpdate();
210
+ } else {
211
+ message.error('删除失败: ' + result.message);
212
+ }
213
+ } catch (error) {
214
+ console.error('删除功能错误:', error);
215
+ message.error('操作失败: ' + error.message);
216
+ } finally {
217
+ setLoading(false);
218
+ }
219
+ }
220
+ });
235
221
  };
236
222
 
237
- const handleNavigateToInterfaces = (featureId) => {
238
- history.push(`/interface-management/${featureId}`);
223
+ const viewInterfaces = (feature) => {
224
+ // 导航到该功能的接口列表页面
225
+ history.push(`/interface/${feature.id}`);
239
226
  };
240
227
 
241
- // 导出功能
242
- const handleExport = async () => {
228
+ const exportFeatureConfig = async (feature) => {
243
229
  try {
244
- setExportLoading(true);
245
- const response = await fetch('/cgi-bin/features/export');
230
+ // 获取该功能的所有接口
231
+ const response = await fetch(`/cgi-bin/interfaces?featureId=${feature.id}`);
232
+ const result = await response.json();
246
233
 
247
- if (!response.ok) {
248
- throw new Error('导出失败');
249
- }
234
+ // 创建完整配置
235
+ const config = {
236
+ ...feature,
237
+ interfaces: result.code === 0 ? result.data : []
238
+ };
239
+
240
+ const dataStr = JSON.stringify(config, null, 2);
241
+ const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr);
250
242
 
251
- const blob = await response.blob();
252
- const url = window.URL.createObjectURL(blob);
253
- const a = document.createElement('a');
254
- a.href = url;
255
- a.download = `features-${new Date().toISOString().split('T')[0]}.json`;
256
- document.body.appendChild(a);
257
- a.click();
258
- window.URL.revokeObjectURL(url);
259
- document.body.removeChild(a);
243
+ const exportFileDefaultName = `whistle-mock-feature-${feature.id}.json`;
260
244
 
261
- message.success('导出成功');
245
+ const linkElement = document.createElement('a');
246
+ linkElement.setAttribute('href', dataUri);
247
+ linkElement.setAttribute('download', exportFileDefaultName);
248
+ linkElement.click();
249
+
250
+ message.success('配置导出成功');
262
251
  } catch (error) {
263
- console.error('导出失败:', error);
252
+ console.error('导出配置错误:', error);
264
253
  message.error('导出失败: ' + error.message);
265
- } finally {
266
- setExportLoading(false);
267
254
  }
268
255
  };
269
256
 
270
- // 导入功能
271
- const handleImport = (event) => {
272
- const file = event.target.files[0];
273
- if (!file) return;
274
-
275
- const reader = new FileReader();
276
- reader.onload = async (e) => {
277
- try {
278
- setImportLoading(true);
279
- const data = JSON.parse(e.target.result);
280
-
281
- const response = await fetch('/cgi-bin/features/import', {
282
- method: 'POST',
283
- headers: {
284
- 'Content-Type': 'application/json'
285
- },
286
- body: JSON.stringify(data)
287
- });
288
-
289
- const result = await response.json();
290
-
291
- if (result.code === 0) {
292
- message.success('导入成功');
293
- fetchFeatures();
294
- } else {
295
- throw new Error(result.message || '导入失败');
257
+ const importFeatureConfig = () => {
258
+ const input = document.createElement('input');
259
+ input.type = 'file';
260
+ input.accept = '.json';
261
+
262
+ input.onchange = async (e) => {
263
+ const file = e.target.files[0];
264
+ if (!file) return;
265
+
266
+ const reader = new FileReader();
267
+ reader.onload = async (event) => {
268
+ try {
269
+ const config = JSON.parse(event.target.result);
270
+
271
+ // 验证导入的配置
272
+ if (!config.name) {
273
+ message.error('无效的配置文件: 缺少功能名称');
274
+ return;
275
+ }
276
+
277
+ // 创建新功能
278
+ const featureData = {
279
+ name: config.name,
280
+ description: config.description || '',
281
+ active: config.active !== false
282
+ };
283
+
284
+ // 保存功能
285
+ const featureResponse = await fetch('/cgi-bin/features', {
286
+ method: 'POST',
287
+ headers: {
288
+ 'Content-Type': 'application/json'
289
+ },
290
+ body: JSON.stringify(featureData)
291
+ });
292
+
293
+ const featureResult = await featureResponse.json();
294
+
295
+ if (featureResult.code === 0) {
296
+ const newFeature = featureResult.data;
297
+
298
+ // 导入接口配置
299
+ if (Array.isArray(config.interfaces) && config.interfaces.length > 0) {
300
+ message.loading({ content: '正在导入接口配置...', key: 'importInfo' });
301
+
302
+ let successCount = 0;
303
+ let errorCount = 0;
304
+
305
+ for (const interfaceItem of config.interfaces) {
306
+ try {
307
+ // 创建接口,使用新功能ID
308
+ const interfaceData = {
309
+ ...interfaceItem,
310
+ featureId: newFeature.id,
311
+ id: undefined // 不使用原接口ID
312
+ };
313
+
314
+ await fetch('/cgi-bin/interfaces', {
315
+ method: 'POST',
316
+ headers: {
317
+ 'Content-Type': 'application/json'
318
+ },
319
+ body: JSON.stringify(interfaceData)
320
+ });
321
+
322
+ successCount++;
323
+ } catch (error) {
324
+ console.error('导入接口配置失败:', error);
325
+ errorCount++;
326
+ }
327
+ }
328
+
329
+ if (errorCount > 0) {
330
+ message.info({ content: `导入完成,成功 ${successCount} 个接口,失败 ${errorCount} 个接口`, key: 'importInfo', duration: 3 });
331
+ } else {
332
+ message.success({ content: `成功导入 ${successCount} 个接口`, key: 'importInfo', duration: 2 });
333
+ }
334
+ } else {
335
+ message.success('功能导入成功(无接口配置)');
336
+ }
337
+
338
+ // 刷新功能列表
339
+ fetchFeatures();
340
+ } else {
341
+ message.error('导入功能失败: ' + featureResult.message);
342
+ }
343
+ } catch (error) {
344
+ console.error('导入配置错误:', error);
345
+ message.error('导入失败: ' + error.message);
296
346
  }
297
- } catch (error) {
298
- console.error('导入失败:', error);
299
- message.error('导入失败: ' + error.message);
300
- } finally {
301
- setImportLoading(false);
302
- // 清除文件选择
303
- event.target.value = '';
304
- }
347
+ };
348
+
349
+ reader.readAsText(file);
305
350
  };
306
-
307
- reader.readAsText(file);
351
+
352
+ input.click();
308
353
  };
309
354
 
310
- // 清空搜索
311
- const handleClearSearch = () => {
312
- setSearchTerm('');
355
+ // 排序处理
356
+ const handleSortChange = (value) => {
357
+ saveListConfig({ sortBy: value, current: 1 });
313
358
  };
314
359
 
315
- // 刷新数据
316
- const handleRefresh = () => {
317
- fetchFeatures();
318
- message.success('数据已刷新');
360
+ const handleSortOrderChange = (value) => {
361
+ saveListConfig({ sortOrder: value, current: 1 });
319
362
  };
320
363
 
321
- // 获取分页数据
322
- const getPaginatedFeatures = () => {
323
- const startIndex = (currentPage - 1) * pageSize;
324
- const endIndex = startIndex + pageSize;
325
- return filteredFeatures.slice(startIndex, endIndex);
364
+ const handlePageSizeChange = (value) => {
365
+ saveListConfig({ pageSize: value, current: 1 });
326
366
  };
327
367
 
328
- // 筛选菜单
329
- const filterMenu = (
330
- <Menu
331
- selectedKeys={[statusFilter]}
332
- onClick={({ key }) => setStatusFilter(key)}
333
- >
334
- <Menu.Item key="all">全部状态</Menu.Item>
335
- <Menu.Item key="active">启用</Menu.Item>
336
- <Menu.Item key="inactive">禁用</Menu.Item>
337
- </Menu>
338
- );
368
+ const handlePageChange = (page, pageSize) => {
369
+ saveListConfig({ current: page, pageSize });
370
+ };
339
371
 
340
- // 排序菜单
341
- const sortMenu = (
342
- <Menu
343
- selectedKeys={[sortBy]}
344
- onClick={({ key }) => setSortBy(key)}
345
- >
346
- <Menu.Item key="name">按名称排序</Menu.Item>
347
- <Menu.Item key="createTime">按创建时间排序</Menu.Item>
348
- <Menu.Item key="updateTime">按更新时间排序</Menu.Item>
349
- </Menu>
350
- );
372
+ // 获取排序后的功能列表
373
+ const getSortedFeatures = () => {
374
+ const sorted = [...mockFeatures].sort((a, b) => {
375
+ let aValue, bValue;
376
+
377
+ switch (listConfig.sortBy) {
378
+ case 'name':
379
+ aValue = a.name || '';
380
+ bValue = b.name || '';
381
+ return listConfig.sortOrder === 'ascend'
382
+ ? aValue.localeCompare(bValue, 'zh-CN')
383
+ : bValue.localeCompare(aValue, 'zh-CN');
384
+
385
+ case 'createdAt':
386
+ aValue = new Date(a.createdAt || 0).getTime();
387
+ bValue = new Date(b.createdAt || 0).getTime();
388
+ return listConfig.sortOrder === 'ascend' ? aValue - bValue : bValue - aValue;
389
+
390
+ case 'interfaceCount':
391
+ aValue = a.interfaceCount || 0;
392
+ bValue = b.interfaceCount || 0;
393
+ return listConfig.sortOrder === 'ascend' ? aValue - bValue : bValue - aValue;
394
+
395
+ case 'active':
396
+ aValue = a.active ? 1 : 0;
397
+ bValue = b.active ? 1 : 0;
398
+ return listConfig.sortOrder === 'ascend' ? aValue - bValue : bValue - aValue;
399
+
400
+ default:
401
+ return 0;
402
+ }
403
+ });
404
+
405
+ return sorted;
406
+ };
351
407
 
352
- const getStatusText = (filter) => {
353
- switch (filter) {
354
- case 'active': return '启用';
355
- case 'inactive': return '禁用';
356
- default: return '全部状态';
357
- }
408
+ // 获取分页后的功能列表
409
+ const getPaginatedFeatures = () => {
410
+ const sortedFeatures = getSortedFeatures();
411
+ const startIndex = (listConfig.current - 1) * listConfig.pageSize;
412
+ const endIndex = startIndex + listConfig.pageSize;
413
+ return sortedFeatures.slice(startIndex, endIndex);
358
414
  };
359
415
 
360
- const getSortText = (sort) => {
361
- switch (sort) {
362
- case 'createTime': return '创建时间';
363
- case 'updateTime': return '更新时间';
364
- default: return '名称';
365
- }
416
+ // 渲染功能模块卡片
417
+ const renderFeatureCard = (feature) => {
418
+ const formattedDate = feature.createdAt
419
+ ? new Date(feature.createdAt).toLocaleDateString()
420
+ : '未知日期';
421
+
422
+ // 处理卡片点击事件
423
+ const handleCardClick = (e) => {
424
+ // 检查点击目标是否是开关或其父元素
425
+ const target = e.target;
426
+ const switchElement = target.closest('.ant-switch') || target.closest('.feature-name .ant-switch');
427
+
428
+ // 如果点击的是开关区域,则不执行跳转
429
+ if (switchElement) {
430
+ return;
431
+ }
432
+
433
+ // 检查是否点击了操作按钮区域
434
+ const actionElement = target.closest('.ant-card-actions') || target.closest('.ant-card-actions li');
435
+ if (actionElement) {
436
+ return;
437
+ }
438
+
439
+ // 执行跳转到接口管理界面
440
+ viewInterfaces(feature);
441
+ };
442
+
443
+ // 处理开关点击事件,阻止事件冒泡
444
+ const handleSwitchClick = (checked, e) => {
445
+ e.stopPropagation(); // 阻止事件冒泡到卡片
446
+ handleToggleActive(feature.id, feature.active);
447
+ };
448
+
449
+ return (
450
+ <Col xs={24} sm={12} md={8} lg={6} key={feature.id} style={{ marginBottom: 16 }}>
451
+ <Badge.Ribbon
452
+ text={feature.active ? '已启用' : '已禁用'}
453
+ color={feature.active ? '#52c41a' : '#f5222d'}
454
+ style={{ display: 'block' }}
455
+ >
456
+ <Card
457
+ className={`feature-card ${!feature.active ? 'inactive-feature' : ''}`}
458
+ onClick={handleCardClick}
459
+ style={{ cursor: 'pointer' }}
460
+ actions={[
461
+ <Tooltip title="管理接口">
462
+ <ApiOutlined key="interfaces" onClick={(e) => {
463
+ e.stopPropagation();
464
+ viewInterfaces(feature);
465
+ }} />
466
+ </Tooltip>,
467
+ <Tooltip title="编辑功能">
468
+ <EditOutlined key="edit" onClick={(e) => {
469
+ e.stopPropagation();
470
+ openModal(feature);
471
+ }} />
472
+ </Tooltip>,
473
+ <Tooltip title="删除功能">
474
+ <DeleteOutlined key="delete" onClick={(e) => {
475
+ e.stopPropagation();
476
+ deleteFeature(feature.id);
477
+ }} />
478
+ </Tooltip>,
479
+ <Tooltip title="导出配置">
480
+ <ExportOutlined key="export" onClick={(e) => {
481
+ e.stopPropagation();
482
+ exportFeatureConfig(feature);
483
+ }} />
484
+ </Tooltip>
485
+ ]}
486
+ >
487
+ <div className="feature-card-content">
488
+ <div className="feature-name">
489
+ <Title level={4} ellipsis={{ tooltip: feature.name }}>
490
+ {feature.name}
491
+ </Title>
492
+
493
+ <Switch
494
+ checked={feature.active}
495
+ onChange={handleSwitchClick}
496
+ size="small"
497
+ onClick={() => {event.stopPropagation()}}
498
+ />
499
+ </div>
500
+
501
+ <Paragraph className="feature-description" ellipsis={{ rows: 2, expandable: false, tooltip: feature.description }}>
502
+ {feature.description || '无描述'}
503
+ </Paragraph>
504
+
505
+ <div className="feature-stat">
506
+ <span>
507
+ <InfoCircleOutlined /> {feature.interfaceCount || 0} 个接口
508
+ </span>
509
+ <span className="feature-date">
510
+ <CalendarOutlined /> {formattedDate}
511
+ </span>
512
+ </div>
513
+ </div>
514
+ </Card>
515
+ </Badge.Ribbon>
516
+ </Col>
517
+ );
366
518
  };
367
519
 
368
520
  return (
369
521
  <AppLayout>
370
- <div className="mock-data-container">
522
+ <div className="page-container">
371
523
  {/* V2版本标识 */}
372
524
  <div style={{
373
525
  position: 'absolute',
@@ -380,308 +532,193 @@ const MockData = () => {
380
532
  fontSize: '12px',
381
533
  zIndex: 1000
382
534
  }}>
383
- 重构后版本 V2
535
+ V2版本
384
536
  </div>
385
537
 
386
- {/* 头部操作栏 */}
387
- <Card className="header-card" bordered={false}>
388
- <div className="header-content">
389
- <div className="header-left">
390
- <h2 className="page-title">功能模块管理</h2>
391
- <div className="search-container">
392
- <Input
393
- placeholder="搜索功能模块名称或描述"
394
- value={searchTerm}
395
- onChange={(e) => setSearchTerm(e.target.value)}
396
- style={{ width: 300 }}
397
- prefix={<SearchOutlined />}
398
- allowClear
399
- onClear={handleClearSearch}
400
- />
401
- </div>
538
+ <div className="page-title-bar">
539
+ <div>
540
+ <h1 className="page-title">功能模块管理</h1>
541
+ <div className="page-description">
542
+ 创建和管理功能模块,为每个功能配置独立的接口
543
+ </div>
544
+ </div>
545
+ <div className="page-actions">
546
+ <Button
547
+ type="primary"
548
+ icon={<PlusOutlined />}
549
+ onClick={() => openModal()}
550
+ >
551
+ 新建功能
552
+ </Button>
553
+ <Button
554
+ type="primary"
555
+ icon={<ExportOutlined />}
556
+ onClick={importFeatureConfig}
557
+ style={{ backgroundColor: '#52c41a', borderColor: '#52c41a' }}
558
+ >
559
+ 导入功能
560
+ </Button>
561
+ </div>
562
+ </div>
563
+
564
+ {/* 排序和分页控制栏 */}
565
+ {mockFeatures.length > 0 && !loading && (
566
+ <div style={{
567
+ marginBottom: 16,
568
+ padding: '12px 16px',
569
+ background: '#fafafa',
570
+ borderRadius: '6px',
571
+ display: 'flex',
572
+ justifyContent: 'space-between',
573
+ alignItems: 'center',
574
+ flexWrap: 'wrap',
575
+ gap: '8px'
576
+ }}>
577
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px', flexWrap: 'wrap' }}>
578
+ <span style={{ fontSize: '14px', color: '#666' }}>
579
+ <SortAscendingOutlined /> 排序:
580
+ </span>
581
+ <Select
582
+ value={listConfig.sortBy}
583
+ onChange={handleSortChange}
584
+ style={{ width: 120 }}
585
+ size="small"
586
+ >
587
+ <Select.Option value="name">名称</Select.Option>
588
+ <Select.Option value="createdAt">创建时间</Select.Option>
589
+ <Select.Option value="interfaceCount">接口数量</Select.Option>
590
+ <Select.Option value="active">状态</Select.Option>
591
+ </Select>
592
+ <Select
593
+ value={listConfig.sortOrder}
594
+ onChange={handleSortOrderChange}
595
+ style={{ width: 80 }}
596
+ size="small"
597
+ >
598
+ <Select.Option value="ascend">升序</Select.Option>
599
+ <Select.Option value="descend">降序</Select.Option>
600
+ </Select>
402
601
  </div>
403
602
 
404
- <div className="header-right">
405
- <Space size="middle">
406
- <Tooltip title="刷新数据">
407
- <Button
408
- icon={<ReloadOutlined />}
409
- onClick={handleRefresh}
410
- loading={loading}
411
- />
412
- </Tooltip>
413
-
414
- <Dropdown overlay={filterMenu} trigger={['click']}>
415
- <Button icon={<FilterOutlined />}>
416
- {getStatusText(statusFilter)}
417
- </Button>
418
- </Dropdown>
419
-
420
- <Dropdown overlay={sortMenu} trigger={['click']}>
421
- <Button icon={<SortAscendingOutlined />}>
422
- 按{getSortText(sortBy)}
423
- </Button>
424
- </Dropdown>
425
-
426
- <Button
427
- icon={<ExportOutlined />}
428
- onClick={handleExport}
429
- loading={exportLoading}
430
- >
431
- 导出
432
- </Button>
433
-
434
- <div style={{ position: 'relative' }}>
435
- <input
436
- type="file"
437
- accept=".json"
438
- onChange={handleImport}
439
- style={{ display: 'none' }}
440
- id="import-input"
441
- />
442
- <Button
443
- icon={<ImportOutlined />}
444
- loading={importLoading}
445
- onClick={() => document.getElementById('import-input').click()}
446
- >
447
- 导入
448
- </Button>
449
- </div>
450
-
451
- <Button
452
- type="primary"
453
- icon={<PlusOutlined />}
454
- onClick={handleAddFeature}
455
- >
456
- 添加功能模块
457
- </Button>
458
- </Space>
603
+ <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
604
+ <span style={{ fontSize: '14px', color: '#666' }}>每页显示:</span>
605
+ <Select
606
+ value={listConfig.pageSize}
607
+ onChange={handlePageSizeChange}
608
+ style={{ width: 80 }}
609
+ size="small"
610
+ >
611
+ <Select.Option value={8}>8</Select.Option>
612
+ <Select.Option value={12}>12</Select.Option>
613
+ <Select.Option value={16}>16</Select.Option>
614
+ <Select.Option value={24}>24</Select.Option>
615
+ </Select>
616
+ <span style={{ fontSize: '14px', color: '#999' }}>
617
+ 共 {mockFeatures.length} 个功能模块
618
+ </span>
459
619
  </div>
460
620
  </div>
461
- </Card>
462
-
463
- {/* 统计信息卡片 */}
464
- <Card className="stats-card" bordered={false}>
465
- <Row gutter={[24, 24]}>
466
- <Col xs={24} sm={8} md={6}>
467
- <div className="stat-item">
468
- <div className="stat-number">{features.length}</div>
469
- <div className="stat-label">总功能模块</div>
470
- </div>
471
- </Col>
472
- <Col xs={24} sm={8} md={6}>
473
- <div className="stat-item">
474
- <div className="stat-number text-success">
475
- {features.filter(f => f.active !== false).length}
476
- </div>
477
- <div className="stat-label">启用模块</div>
478
- </div>
479
- </Col>
480
- <Col xs={24} sm={8} md={6}>
481
- <div className="stat-item">
482
- <div className="stat-number text-warning">
483
- {features.filter(f => f.active === false).length}
484
- </div>
485
- <div className="stat-label">禁用模块</div>
486
- </div>
487
- </Col>
488
- <Col xs={24} sm={8} md={6}>
489
- <div className="stat-item">
490
- <div className="stat-number text-info">{filteredFeatures.length}</div>
491
- <div className="stat-label">筛选结果</div>
492
- </div>
493
- </Col>
494
- </Row>
495
- </Card>
496
-
497
- {/* 功能模块列表 */}
498
- <Card className="list-card" bordered={false} title="功能模块列表">
621
+ )}
622
+
623
+ <div className="feature-list-container">
499
624
  {loading ? (
500
- <div className="loading-container">
501
- <Row gutter={[16, 16]}>
502
- {[...Array(6)].map((_, index) => (
503
- <Col xs={24} sm={12} md={8} lg={6} key={index}>
504
- <Card loading />
505
- </Col>
506
- ))}
507
- </Row>
508
- </div>
509
- ) : getPaginatedFeatures().length === 0 ? (
510
- <div className="empty-state">
511
- <div className="empty-icon">📦</div>
512
- <div className="empty-text">
513
- {searchTerm || statusFilter !== 'all' ? '没有找到匹配的功能模块' : '暂无功能模块'}
514
- </div>
515
- <div className="empty-description">
516
- {searchTerm || statusFilter !== 'all' ? '尝试调整搜索条件或筛选器' : '点击上方按钮创建你的第一个功能模块'}
517
- </div>
518
- {!searchTerm && statusFilter === 'all' && (
519
- <Button
520
- type="primary"
521
- icon={<PlusOutlined />}
522
- onClick={handleAddFeature}
523
- style={{ marginTop: 16 }}
524
- >
525
- 创建功能模块
526
- </Button>
527
- )}
625
+ <div className="loading">
626
+ <div className="loading-spinner"></div>
627
+ <div>正在加载功能模块...</div>
528
628
  </div>
529
- ) : (
629
+ ) : mockFeatures.length > 0 ? (
530
630
  <>
531
631
  <Row gutter={[16, 16]}>
532
- {getPaginatedFeatures().map(feature => (
533
- <Col xs={24} sm={12} md={8} lg={6} key={feature.id}>
534
- <Card
535
- className={`feature-card ${feature.active === false ? 'inactive' : ''}`}
536
- hoverable
537
- actions={[
538
- <Tooltip title="编辑">
539
- <EditOutlined
540
- key="edit"
541
- onClick={() => handleEditFeature(feature)}
542
- />
543
- </Tooltip>,
544
- <Tooltip title="接口管理">
545
- <SettingOutlined
546
- key="setting"
547
- onClick={() => handleNavigateToInterfaces(feature.id)}
548
- />
549
- </Tooltip>,
550
- <Popconfirm
551
- title="确定要删除这个功能模块吗?"
552
- description="删除后将无法恢复,请谨慎操作。"
553
- onConfirm={() => handleDeleteFeature(feature.id)}
554
- okText="确定"
555
- cancelText="取消"
556
- >
557
- <Tooltip title="删除">
558
- <DeleteOutlined key="delete" className="text-danger" />
559
- </Tooltip>
560
- </Popconfirm>
561
- ]}
562
- >
563
- <div className="feature-card-header">
564
- <div className="feature-card-title">
565
- <span className="feature-name">{feature.name}</span>
566
- <Switch
567
- checked={feature.active !== false}
568
- onChange={() => handleToggleActive(feature.id, feature.active !== false)}
569
- size="small"
570
- className="feature-switch"
571
- />
572
- </div>
573
- <Badge
574
- status={feature.active === false ? 'error' : 'success'}
575
- text={feature.active === false ? '已禁用' : '已启用'}
576
- className="feature-status"
577
- />
578
- </div>
579
-
580
- <div className="feature-card-content">
581
- {feature.description ? (
582
- <p className="feature-description">
583
- {feature.description}
584
- </p>
585
- ) : (
586
- <p className="feature-description empty">
587
- 暂无描述
588
- </p>
589
- )}
590
-
591
- <div className="feature-meta">
592
- {feature.createTime && (
593
- <div className="meta-item">
594
- <span className="meta-label">创建:</span>
595
- <span className="meta-value">
596
- {new Date(feature.createTime).toLocaleDateString()}
597
- </span>
598
- </div>
599
- )}
600
- {feature.updateTime && feature.updateTime !== feature.createTime && (
601
- <div className="meta-item">
602
- <span className="meta-label">更新:</span>
603
- <span className="meta-value">
604
- {new Date(feature.updateTime).toLocaleDateString()}
605
- </span>
606
- </div>
607
- )}
608
- </div>
609
- </div>
610
- </Card>
611
- </Col>
612
- ))}
632
+ {getPaginatedFeatures().map(feature => renderFeatureCard(feature))}
613
633
  </Row>
614
634
 
615
- {/* 分页 */}
616
- {filteredFeatures.length > pageSize && (
617
- <div className="pagination-container">
635
+ {/* 分页组件 */}
636
+ {mockFeatures.length > listConfig.pageSize && (
637
+ <div style={{
638
+ marginTop: 24,
639
+ textAlign: 'center',
640
+ padding: '16px',
641
+ borderTop: '1px solid #f0f0f0'
642
+ }}>
618
643
  <Pagination
619
- current={currentPage}
620
- pageSize={pageSize}
621
- total={filteredFeatures.length}
622
- showSizeChanger
623
- showQuickJumper
644
+ current={listConfig.current}
645
+ pageSize={listConfig.pageSize}
646
+ total={mockFeatures.length}
647
+ onChange={handlePageChange}
648
+ onShowSizeChange={handlePageChange}
649
+ showSizeChanger={true}
650
+ showQuickJumper={true}
624
651
  showTotal={(total, range) =>
625
- `共 ${total} 个功能模块,显示第 ${range[0]}-${range[1]} 个`
652
+ `第 ${range[0]}-${range[1]} 条,共 ${total} 个功能模块`
626
653
  }
627
- pageSizeOptions={['8', '12', '24', '48']}
628
- onChange={(page, size) => {
629
- setCurrentPage(page);
630
- setPageSize(size);
631
- }}
654
+ pageSizeOptions={['8', '12', '16', '24']}
655
+ size="default"
632
656
  />
633
657
  </div>
634
658
  )}
635
659
  </>
660
+ ) : (
661
+ <div className="empty-data">
662
+ <div className="empty-icon">📂</div>
663
+ <div className="empty-text">暂无功能,请点击"新建功能"按钮创建</div>
664
+ <div className="empty-actions">
665
+ <button className="create-button" onClick={() => openModal()}>
666
+ 创建新功能
667
+ </button>
668
+ <button className="import-button-large" onClick={importFeatureConfig}>
669
+ 导入已有功能
670
+ </button>
671
+ </div>
672
+ </div>
636
673
  )}
637
- </Card>
638
-
639
- {/* 添加/编辑功能模块模态框 */}
640
- <Modal
641
- title={editingFeature ? '编辑功能模块' : '添加功能模块'}
642
- open={modalVisible}
643
- onOk={handleSubmit}
644
- onCancel={handleCancel}
645
- okText={editingFeature ? '保存' : '创建'}
646
- cancelText="取消"
647
- destroyOnClose
674
+ </div>
675
+ </div>
676
+
677
+ <Modal
678
+ title={currentFeature ? '编辑功能' : '新建功能'}
679
+ open={showModal}
680
+ onCancel={closeModal}
681
+ footer={null}
682
+ destroyOnClose
683
+ width={500}
684
+ >
685
+ <Form
686
+ form={form}
687
+ layout="vertical"
688
+ onFinish={handleSubmit}
689
+ initialValues={{
690
+ name: '',
691
+ description: '',
692
+ active: true
693
+ }}
648
694
  >
649
- <Form
650
- form={form}
651
- layout="vertical"
652
- initialValues={{
653
- name: '',
654
- description: ''
655
- }}
695
+ <Form.Item
696
+ name="name"
697
+ label="功能名称"
698
+ rules={[{ required: true, message: '请输入功能名称' }]}
656
699
  >
657
- <Form.Item
658
- name="name"
659
- label="功能模块名称"
660
- rules={[
661
- { required: true, message: '请输入功能模块名称' },
662
- { max: 50, message: '名称长度不能超过50个字符' }
663
- ]}
664
- >
665
- <Input placeholder="请输入功能模块名称" />
666
- </Form.Item>
667
-
668
- <Form.Item
669
- name="description"
670
- label="描述"
671
- rules={[
672
- { max: 200, message: '描述长度不能超过200个字符' }
673
- ]}
674
- >
675
- <TextArea
676
- placeholder="请输入功能模块描述(可选)"
677
- rows={3}
678
- showCount
679
- maxLength={200}
680
- />
681
- </Form.Item>
682
- </Form>
683
- </Modal>
684
- </div>
700
+ <Input placeholder="请输入功能名称" />
701
+ </Form.Item>
702
+
703
+ <Form.Item
704
+ name="description"
705
+ label="功能描述"
706
+ >
707
+ <TextArea
708
+ placeholder="请输入功能描述(可选)"
709
+ autoSize={{ minRows: 3, maxRows: 6 }}
710
+ />
711
+ </Form.Item>
712
+ <div className="form-actions">
713
+ <Button onClick={closeModal}>
714
+ 取消
715
+ </Button>
716
+ <Button type="primary" htmlType="submit" loading={loading}>
717
+ 确定
718
+ </Button>
719
+ </div>
720
+ </Form>
721
+ </Modal>
685
722
  </AppLayout>
686
723
  );
687
724
  };