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
|
-
|
|
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 {
|
|
14
|
+
const { Text, Title, Paragraph } = Typography;
|
|
23
15
|
const { TextArea } = Input;
|
|
24
16
|
|
|
25
|
-
//
|
|
17
|
+
// 刷新缓存服务
|
|
26
18
|
const flushCache = async () => {
|
|
27
19
|
try {
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
const response = await axios.get(`/_flush_cache`);
|
|
21
|
+
return response.data;
|
|
30
22
|
} catch (error) {
|
|
31
|
-
console.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 [
|
|
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 [
|
|
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 [
|
|
53
|
-
|
|
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
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
+
setMockFeatures(result.data || []);
|
|
75
69
|
} else {
|
|
76
|
-
|
|
70
|
+
console.error('获取功能模块失败:', result.message);
|
|
71
|
+
message.error('获取功能模块失败: ' + result.message);
|
|
77
72
|
}
|
|
78
73
|
} catch (error) {
|
|
79
|
-
console.error('
|
|
80
|
-
message.error('
|
|
81
|
-
setFeatures([]);
|
|
74
|
+
console.error('获取功能模块错误:', error);
|
|
75
|
+
message.error('获取功能模块失败, 请检查网络连接');
|
|
82
76
|
} finally {
|
|
83
77
|
setLoading(false);
|
|
84
78
|
}
|
|
85
79
|
};
|
|
86
80
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
131
|
-
|
|
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
|
-
|
|
103
|
+
// 更新成功后刷新缓存
|
|
104
|
+
const refreshCacheAfterUpdate = async () => {
|
|
140
105
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
157
|
-
|
|
109
|
+
// 记录错误但不影响用户体验
|
|
110
|
+
console.error('刷新缓存失败,可能需要等待缓存自动过期:', error);
|
|
158
111
|
}
|
|
159
112
|
};
|
|
160
113
|
|
|
161
|
-
const
|
|
114
|
+
const handleSubmit = async (values) => {
|
|
162
115
|
try {
|
|
163
|
-
const
|
|
164
|
-
|
|
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
|
-
|
|
137
|
+
// 刷新功能列表
|
|
138
|
+
message.success(currentFeature ? '功能模块更新成功' : '功能模块创建成功');
|
|
178
139
|
fetchFeatures();
|
|
140
|
+
closeModal();
|
|
179
141
|
|
|
180
142
|
// 刷新缓存
|
|
181
|
-
|
|
143
|
+
refreshCacheAfterUpdate();
|
|
182
144
|
} else {
|
|
183
|
-
|
|
145
|
+
message.error('操作失败: ' + result.message);
|
|
184
146
|
}
|
|
185
147
|
} catch (error) {
|
|
186
|
-
console.error('
|
|
187
|
-
message.error('
|
|
148
|
+
console.error('保存功能模块错误:', error);
|
|
149
|
+
message.error('保存失败: ' + error.message);
|
|
150
|
+
} finally {
|
|
151
|
+
setLoading(false);
|
|
188
152
|
}
|
|
189
153
|
};
|
|
190
154
|
|
|
191
|
-
const
|
|
155
|
+
const handleToggleActive = async (id, currentActive) => {
|
|
192
156
|
try {
|
|
193
|
-
const
|
|
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(
|
|
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(
|
|
219
|
-
|
|
220
|
-
|
|
170
|
+
message.success(`${currentActive ? '禁用' : '启用'}功能模块成功`);
|
|
171
|
+
// 更新本地状态
|
|
172
|
+
setMockFeatures(mockFeatures.map(feature =>
|
|
173
|
+
feature.id === id ? { ...feature, active: !currentActive } : feature
|
|
174
|
+
));
|
|
221
175
|
|
|
222
|
-
// 刷新缓存
|
|
223
|
-
|
|
176
|
+
// 刷新缓存 - 功能启用状态变化是最关键的需要刷新缓存的操作
|
|
177
|
+
refreshCacheAfterUpdate();
|
|
224
178
|
} else {
|
|
225
|
-
|
|
179
|
+
message.error(`${currentActive ? '禁用' : '启用'}功能模块失败: ${result.message}`);
|
|
226
180
|
}
|
|
227
181
|
} catch (error) {
|
|
228
|
-
console.error('
|
|
229
|
-
message.error(
|
|
182
|
+
console.error(`${currentActive ? '禁用' : '启用'}功能模块错误:`, error);
|
|
183
|
+
message.error(`操作失败: ${error.message}`);
|
|
230
184
|
}
|
|
231
185
|
};
|
|
232
186
|
|
|
233
|
-
const
|
|
234
|
-
|
|
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
|
|
238
|
-
|
|
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
|
-
|
|
245
|
-
const response = await fetch(
|
|
230
|
+
// 获取该功能的所有接口
|
|
231
|
+
const response = await fetch(`/cgi-bin/interfaces?featureId=${feature.id}`);
|
|
232
|
+
const result = await response.json();
|
|
246
233
|
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
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
|
-
|
|
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('
|
|
252
|
+
console.error('导出配置错误:', error);
|
|
264
253
|
message.error('导出失败: ' + error.message);
|
|
265
|
-
} finally {
|
|
266
|
-
setExportLoading(false);
|
|
267
254
|
}
|
|
268
255
|
};
|
|
269
256
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
} finally {
|
|
301
|
-
setImportLoading(false);
|
|
302
|
-
// 清除文件选择
|
|
303
|
-
event.target.value = '';
|
|
304
|
-
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
reader.readAsText(file);
|
|
305
350
|
};
|
|
306
|
-
|
|
307
|
-
|
|
351
|
+
|
|
352
|
+
input.click();
|
|
308
353
|
};
|
|
309
354
|
|
|
310
|
-
//
|
|
311
|
-
const
|
|
312
|
-
|
|
355
|
+
// 排序处理
|
|
356
|
+
const handleSortChange = (value) => {
|
|
357
|
+
saveListConfig({ sortBy: value, current: 1 });
|
|
313
358
|
};
|
|
314
359
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
fetchFeatures();
|
|
318
|
-
message.success('数据已刷新');
|
|
360
|
+
const handleSortOrderChange = (value) => {
|
|
361
|
+
saveListConfig({ sortOrder: value, current: 1 });
|
|
319
362
|
};
|
|
320
363
|
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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="
|
|
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
|
-
|
|
535
|
+
V2版本
|
|
384
536
|
</div>
|
|
385
537
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
<div className="
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
|
405
|
-
<
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
</
|
|
413
|
-
|
|
414
|
-
<
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
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
|
|
501
|
-
<
|
|
502
|
-
|
|
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
|
-
{
|
|
617
|
-
<div
|
|
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={
|
|
620
|
-
pageSize={pageSize}
|
|
621
|
-
total={
|
|
622
|
-
|
|
623
|
-
|
|
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
|
-
|
|
652
|
+
`第 ${range[0]}-${range[1]} 条,共 ${total} 个功能模块`
|
|
626
653
|
}
|
|
627
|
-
pageSizeOptions={['8', '12', '
|
|
628
|
-
|
|
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
|
-
</
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
name: '',
|
|
654
|
-
description: ''
|
|
655
|
-
}}
|
|
695
|
+
<Form.Item
|
|
696
|
+
name="name"
|
|
697
|
+
label="功能名称"
|
|
698
|
+
rules={[{ required: true, message: '请输入功能名称' }]}
|
|
656
699
|
>
|
|
657
|
-
<
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
};
|