whistle.mock-plugins
Version:
Whistle 插件,用于快速创建 API 模拟数据
1,519 lines (1,374 loc) • 97.5 kB
JavaScript
/**
* InterfaceManagementV2.js - 接口管理页面
*/
import React, { useState, useEffect, useRef } from 'react';
import { useParams, useHistory } from 'react-router-dom';
import AppLayout from '../components/AppLayout';
import {
Table, Button, Modal, Form, Input, Select, message, Switch,
Popconfirm, Alert, Space, Card, Badge, Tooltip, Row, Col,
Popover, Checkbox, Tag, Input as AntInput, Radio, Drawer, Dropdown, Menu, Collapse
} from 'antd';
import {
PlusOutlined, EditOutlined, DeleteOutlined,
FileTextOutlined, PlusCircleOutlined, SettingOutlined,
SearchOutlined, FilterOutlined, ReloadOutlined, DownOutlined, QuestionCircleOutlined, CopyOutlined
} from '@ant-design/icons';
import '../styles/interface-management.css';
import axios from 'axios';
// 导入拆分后的组件
import {
ResponseContentEditor,
PreviewModal,
contentTypes,
proxyTypes,
statusCodes,
httpMethods,
refreshCacheAfterUpdate,
formatResponseContent,
generateResponseId
} from '../components/interface-management';
import DataManagementModal from '../components/interface-management/DataManagementModal';
import { DataManagementDrawer } from '../components/interface-management';
const { Option } = Select;
const { Search } = AntInput;
// 列配置数据结构
const COLUMN_CONFIG = [
{ key: 'active', title: '状态', required: true },
{ key: 'name', title: '名称', required: true },
{ key: 'group', title: '分组', required: false },
{ key: 'urlPattern', title: 'URL匹配规则', required: false },
{ key: 'proxyType', title: '处理方式', required: false },
{ key: 'currentResponse', title: '当前响应', required: false },
{ key: 'responseDelay', title: '延迟(毫秒)', required: false },
{ key: 'httpStatus', title: '状态码', required: false },
{ key: 'contentType', title: '内容类型', required: false },
{ key: 'targetUrl', title: '目标URL', required: false },
{ key: 'customHeaders', title: '自定义头', required: false },
{ key: 'paramMatchers', title: '参数匹配', required: false },
{ key: 'httpMethod', title: '请求方法', required: false },
{ key: 'action', title: '操作', required: true }
];
const InterfaceManagement = () => {
const { featureId } = useParams();
const history = useHistory();
// 基础状态
const [features, setFeatures] = useState([]);
const [interfaces, setInterfaces] = useState([]);
const [loading, setLoading] = useState(false);
const [featuresLoading, setFeaturesLoading] = useState(false);
const [interfacesLoading, setInterfacesLoading] = useState(false);
const [selectedFeatureId, setSelectedFeatureId] = useState(null);
// 分组状态
const [groups, setGroups] = useState([]);
const [selectedGroup, setSelectedGroup] = useState(null);
const [groupActionLoading, setGroupActionLoading] = useState(false);
// 模态框状态
const [modalVisible, setModalVisible] = useState(false);
const [previewVisible, setPreviewVisible] = useState(false);
const [isDuplicateMode, setIsDuplicateMode] = useState(false);
const [dataModalVisible, setDataModalVisible] = useState(false);
const [dataModalRecord, setDataModalRecord] = useState(null);
// 列配置状态
const [columnConfigVisible, setColumnConfigVisible] = useState(false);
// 编辑状态
const [editingInterface, setEditingInterface] = useState(null);
const [previewContent, setPreviewContent] = useState(null);
const [currentResponseId, setCurrentResponseId] = useState(null);
// 搜索状态
const searchRef = useRef(null);
const [searchValue, setSearchValue] = useState('');
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const [drawerVisible, setDrawerVisible] = useState(false);
// 数据管理显示模式(modal / drawer)
const [displayMode, setDisplayMode] = useState('modal');
// 接口表单显示模式(modal / drawer)
const [interfaceFormDisplayMode, setInterfaceFormDisplayMode] = useState('modal');
// 接口入参匹配显示开关
const [interfaceParamMatcherEnabled, setInterfaceParamMatcherEnabled] = useState(true);
// 响应入参匹配显示开关
const [responseParamMatcherEnabled, setResponseParamMatcherEnabled] = useState(true);
// 表单实例
const [form] = Form.useForm();
// 表格配置状态(支持缓存)
const [tableConfig, setTableConfig] = useState(() => {
const cached = localStorage.getItem('interface-table-config');
const defaultVisibleColumns = COLUMN_CONFIG.map(col => col.key);
return cached ? JSON.parse(cached) : {
sortOrder: null,
sortField: null,
pageSize: 10,
current: 1,
visibleColumns: defaultVisibleColumns
};
});
// 保存表格配置到localStorage
const saveTableConfig = (config) => {
const newConfig = { ...tableConfig, ...config };
setTableConfig(newConfig);
localStorage.setItem('interface-table-config', JSON.stringify(newConfig));
};
useEffect(() => {
fetchFeatures();
}, []);
useEffect(() => {
if (selectedFeatureId) {
fetchInterfaces();
}
}, [selectedFeatureId]);
// 键盘快捷键监听
useEffect(() => {
const handleSave = () => {
if (modalVisible) {
handleSubmit();
}
};
const handleNew = () => {
handleAddInterface();
};
const handleFocusSearch = () => {
searchRef.current?.focus();
};
window.addEventListener('shortcut-save', handleSave);
window.addEventListener('shortcut-new-interface', handleNew);
window.addEventListener('shortcut-focus-search', handleFocusSearch);
return () => {
window.removeEventListener('shortcut-save', handleSave);
window.removeEventListener('shortcut-new-interface', handleNew);
window.removeEventListener('shortcut-focus-search', handleFocusSearch);
};
}, [modalVisible]);
// 监听数据管理显示模式变化
useEffect(() => {
const savedMode = localStorage.getItem('dataManagementDisplayMode');
if (savedMode) {
setDisplayMode(savedMode);
}
const handleStorageChange = () => {
const newMode = localStorage.getItem('dataManagementDisplayMode');
if (newMode) {
setDisplayMode(newMode);
}
};
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []);
// 监听接口表单显示模式变化
useEffect(() => {
const savedMode = localStorage.getItem('interfaceFormDisplayMode');
if (savedMode) {
setInterfaceFormDisplayMode(savedMode);
}
const handleStorageChange = () => {
const newMode = localStorage.getItem('interfaceFormDisplayMode');
if (newMode) {
setInterfaceFormDisplayMode(newMode);
}
};
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []);
// 监听接口入参匹配 / 响应入参匹配开关变化
useEffect(() => {
const readSwitches = () => {
const v1 = localStorage.getItem('interfaceParamMatcherEnabled');
setInterfaceParamMatcherEnabled(v1 === null ? true : v1 !== 'false');
const v2 = localStorage.getItem('responseParamMatcherEnabled');
setResponseParamMatcherEnabled(v2 === null ? true : v2 !== 'false');
};
readSwitches();
window.addEventListener('storage', readSwitches);
return () => window.removeEventListener('storage', readSwitches);
}, []);
// 获取当前选中的功能模块
const selectedFeature = features.find(f => f.id === selectedFeatureId);
const fetchFeatures = async () => {
try {
setFeaturesLoading(true);
const response = await axios.get('/cgi-bin/features');
if (response.data && response.data.code === 0 && Array.isArray(response.data.data)) {
const featureList = response.data.data;
setFeatures(featureList);
// 优先级:URL 参数 > localStorage 上次选择 > 第一个功能模块
const lastSelected = localStorage.getItem('selectedFeatureId');
const validLastSelected = lastSelected && featureList.find(f => f.id === lastSelected) ? lastSelected : null;
const initialFeatureId = featureId || validLastSelected || featureList[0]?.id;
if (initialFeatureId) {
setSelectedFeatureId(initialFeatureId);
}
} else {
setFeatures([]);
message.warning('获取功能模块数据格式不正确');
}
} catch (error) {
console.error('获取功能模块失败:', error);
message.error(error.response?.data?.message || '获取功能模块失败');
setFeatures([]);
} finally {
setFeaturesLoading(false);
}
};
const fetchInterfaces = async () => {
if (!selectedFeatureId) {
return;
}
try {
setInterfacesLoading(true);
const response = await axios.get(`/cgi-bin/interfaces?featureId=${selectedFeatureId}`);
console.log('获取接口列表响应:', response.data);
if (response.data && response.data.code === 0 && Array.isArray(response.data.data)) {
// 确保每个接口的响应数据格式正确
const processedInterfaces = response.data.data.map(item => {
console.log(`处理接口 ${item.name}:`, {
hasResponses: !!item.responses,
responsesLength: item.responses ? item.responses.length : 0,
activeResponseId: item.activeResponseId
});
// 确保 responses 是数组
if (!Array.isArray(item.responses)) {
item.responses = [];
}
// 确保每个响应都有必要的字段
item.responses = item.responses.map((resp, index) => ({
id: resp.id || `resp-${index}-${Date.now()}`,
name: resp.name || `响应 ${index + 1}`,
description: resp.description || '',
content: resp.content || '{}',
paramMatchers: Array.isArray(resp.paramMatchers) ? resp.paramMatchers : []
}));
return item;
});
setInterfaces(processedInterfaces);
console.log('处理后的接口列表:', processedInterfaces);
console.log('接口分组信息:', processedInterfaces.map(item => ({ id: item.id, name: item.name, group: item.group })));
// 提取并更新分组列表
updateGroups(processedInterfaces);
} else {
console.warn('接口数据格式不正确:', response.data);
setInterfaces([]);
message.warning('获取接口配置数据格式不正确');
}
} catch (error) {
console.error('获取接口配置失败:', error);
message.error(error.response?.data?.message || '获取接口配置失败');
setInterfaces([]);
} finally {
setInterfacesLoading(false);
}
};
// 提取并更新分组列表
const updateGroups = (interfaces) => {
// 从接口列表中提取所有分组
const groupSet = new Set();
interfaces.forEach(item => {
if (item.group) {
groupSet.add(item.group);
}
});
// 转换为数组并排序
const groupArray = Array.from(groupSet).sort();
setGroups(groupArray);
// 如果当前选中的分组不在列表中,重置选中的分组
if (selectedGroup && !groupArray.includes(selectedGroup)) {
setSelectedGroup(null);
}
};
// 根据分组和搜索关键词筛选接口
const getFilteredInterfaces = () => {
// 首先按功能模块筛选
let featureFiltered = interfaces.filter(item =>
!selectedFeatureId || item.featureId === selectedFeatureId
);
// 然后按分组筛选
if (selectedGroup) {
featureFiltered = featureFiltered.filter(item => item.group === selectedGroup);
}
// 最后按搜索关键词筛选
if (searchValue) {
const searchLower = searchValue.toLowerCase();
return featureFiltered.filter(item =>
(item.name && item.name.toLowerCase().includes(searchLower)) ||
(item.urlPattern && item.urlPattern.toLowerCase().includes(searchLower))
);
}
return featureFiltered;
};
const handleAddInterface = () => {
if (!selectedFeatureId) {
message.warning('请先选择一个功能模块');
return;
}
form.resetFields();
setIsDuplicateMode(false);
// 创建默认响应
const defaultResponseId = generateResponseId();
const defaultResponses = [{
id: defaultResponseId,
name: '默认响应',
description: '',
content: '{\n "code": 0,\n "message": "success",\n "data": {}\n}'
}];
console.log('创建新接口,初始化响应数据:', defaultResponses);
form.setFieldsValue({
proxyType: 'response',
statusCode: '200',
contentType: 'application/json; charset=utf-8',
responses: defaultResponses,
activeResponseId: defaultResponseId,
httpMethod: 'ALL',
headerItems: [], // 初始化为空数组
paramMatchers: [], // 初始化为空数组
group: undefined // 确保分组字段为undefined,而不是空字符串
});
setCurrentResponseId(defaultResponseId);
setEditingInterface(null);
setModalVisible(true);
};
const handleEditInterface = (record) => {
if (!record) {
message.warning('接口数据不完整,无法编辑');
return;
}
console.log('编辑接口:', record);
// 重置表单
form.resetFields();
setIsDuplicateMode(false);
// 处理响应数据
let responses = [];
let activeResponseId = '';
if (record.responses && Array.isArray(record.responses) && record.responses.length > 0) {
responses = record.responses;
activeResponseId = record.activeResponseId || responses[0].id;
} else if (record.responseContent) {
// 兼容旧数据格式,创建默认响应
const defaultResponseId = generateResponseId();
responses = [{
id: defaultResponseId,
name: '默认响应',
description: '',
content: record.responseContent
}];
activeResponseId = defaultResponseId;
}
// 处理自定义请求头
let headerItems = [];
if (record.headers && typeof record.headers === 'object') {
headerItems = Object.entries(record.headers).map(([headerName, headerValue]) => ({
headerName,
headerValue
}));
}
// 处理分组值,确保它是字符串而不是数组
let groupValue = record.group;
if (Array.isArray(groupValue)) {
groupValue = groupValue.length > 0 ? groupValue[0] : undefined;
}
// 设置表单值
form.setFieldsValue({
name: record.name,
group: groupValue || undefined, // 使用undefined而不是空字符串
pattern: record.urlPattern,
proxyType: record.proxyType || 'response',
statusCode: (record.httpStatus || record.statusCode || 200).toString(),
contentType: record.contentType || 'application/json; charset=utf-8',
responses,
activeResponseId,
httpMethod: record.httpMethod || record.method || 'ALL',
targetUrl: record.targetUrl || '',
headerItems,
paramMatchers: record.paramMatchers || [],
responseDelay: record.responseDelay ? record.responseDelay.toString() : '0'
});
console.log('编辑接口时设置的分组值:', groupValue);
setCurrentResponseId(activeResponseId);
setEditingInterface(record);
setModalVisible(true);
};
// 复制新增接口
const handleDuplicate = (record) => {
if (!selectedFeatureId) {
message.warning('请先选择一个功能模块');
return;
}
if (!record) {
message.warning('接口数据不完整,无法复制');
return;
}
form.resetFields();
// 深拷贝源数据
const source = JSON.parse(JSON.stringify(record));
// 处理响应数据
let responses = [];
let activeResponseId = '';
if (Array.isArray(source.responses) && source.responses.length > 0) {
// 为每条响应生成新的ID,并重映射activeResponseId
const idMap = new Map();
responses = source.responses.map((resp) => {
const newId = generateResponseId();
idMap.set(resp.id, newId);
return {
id: newId,
name: resp.name || '未命名响应',
description: resp.description || '',
content: resp.content || '{}',
paramMatchers: Array.isArray(resp.paramMatchers) ? resp.paramMatchers.map(m => ({ ...m })) : []
};
});
const mapped = idMap.get(source.activeResponseId) || responses[0]?.id;
activeResponseId = mapped || '';
} else if (source.responseContent) {
// 兼容旧数据格式
const defaultResponseId = generateResponseId();
responses = [{
id: defaultResponseId,
name: '默认响应',
description: '',
content: source.responseContent
}];
activeResponseId = defaultResponseId;
}
// 处理自定义请求头 -> 表单需要的数组结构
let headerItems = [];
if (source.headers && typeof source.headers === 'object') {
headerItems = Object.entries(source.headers).map(([headerName, headerValue]) => ({
headerName,
headerValue
}));
}
// 分组值保证为字符串
let groupValue = source.group;
if (Array.isArray(groupValue)) {
groupValue = groupValue.length > 0 ? groupValue[0] : undefined;
}
// 设定表单初值
form.setFieldsValue({
name: `${source.name || '未命名接口'} - 副本`,
group: groupValue || undefined,
pattern: source.urlPattern,
proxyType: source.proxyType || 'response',
statusCode: (source.httpStatus || source.statusCode || 200).toString(),
contentType: source.contentType || 'application/json; charset=utf-8',
responses,
activeResponseId,
httpMethod: source.httpMethod || source.method || 'ALL',
targetUrl: source.targetUrl || '',
headerItems,
paramMatchers: Array.isArray(source.paramMatchers) ? source.paramMatchers : [],
responseDelay: source.responseDelay ? String(source.responseDelay) : '0'
});
setCurrentResponseId(activeResponseId || null);
setEditingInterface(null); // 新增模式
setIsDuplicateMode(true);
setModalVisible(true);
};
const handleDeleteInterface = async (id) => {
try {
const response = await axios.delete(`/cgi-bin/interfaces?id=${id}`);
if (response.data && response.data.code === 0) {
message.success('接口删除成功');
fetchInterfaces();
// 刷新规则缓存
refreshCacheAfterUpdate();
} else {
throw new Error(response.data?.message || '接口删除失败');
}
} catch (error) {
console.error('接口删除失败:', error);
message.error(error.response?.data?.message || error.message || '接口删除失败');
}
};
const handleToggleActive = async (id, currentActive) => {
try {
const response = await axios.patch(`/cgi-bin/interfaces/${id}`, {
active: !currentActive
});
if (response.data && response.data.code === 0) {
message.success(`接口${!currentActive ? '启用' : '禁用'}成功`);
fetchInterfaces();
// 刷新规则缓存
refreshCacheAfterUpdate();
} else {
throw new Error(response.data?.message || `接口${!currentActive ? '启用' : '禁用'}失败`);
}
} catch (error) {
console.error(`接口${!currentActive ? '启用' : '禁用'}失败:`, error);
message.error(error.response?.data?.message || error.message || `接口${!currentActive ? '启用' : '禁用'}失败`);
}
};
const handleSubmit = async () => {
try {
// 表单验证 - 先验证所有字段,再获取所有字段值(包括折叠的)
await form.validateFields();
const values = form.getFieldsValue(true); // true表示获取所有字段,包括未渲染的
console.log('表单提交数据:', values);
// 处理自定义请求头
let headers = {};
if (values.headerItems && Array.isArray(values.headerItems)) {
values.headerItems.forEach(item => {
if (item && item.headerName) {
headers[item.headerName] = item.headerValue;
}
});
}
// 处理分组值,确保它是字符串而不是数组
let groupValue = values.group;
if (Array.isArray(groupValue)) {
groupValue = groupValue.length > 0 ? groupValue[0] : '';
}
// 处理 responses,清理每个响应中的空 paramMatchers 规则
const cleanedResponses = (values.responses || (editingInterface ? editingInterface.responses : [])).map(resp => ({
...resp,
paramMatchers: Array.isArray(resp.paramMatchers)
? resp.paramMatchers.filter(m => m && m.paramPath && m.paramValue)
: []
}));
// 构建接口数据 - 在编辑模式下,确保关键字段不丢失
const interfaceData = {
name: values.name || (editingInterface ? editingInterface.name : ''),
group: groupValue || (editingInterface ? editingInterface.group : '') || '', // 确保group不为undefined
urlPattern: values.pattern || (editingInterface ? editingInterface.urlPattern : ''),
proxyType: values.proxyType || (editingInterface ? editingInterface.proxyType : 'response'),
featureId: selectedFeatureId,
responses: cleanedResponses,
activeResponseId: values.activeResponseId || (editingInterface ? editingInterface.activeResponseId : null),
httpStatus: parseInt(values.statusCode, 10) || (editingInterface ? editingInterface.httpStatus : 200), // 转换为数字
contentType: values.contentType || (editingInterface ? editingInterface.contentType : 'application/json; charset=utf-8'),
responseDelay: parseInt(values.responseDelay, 10) || (editingInterface ? editingInterface.responseDelay : 0) || 0,
httpMethod: values.httpMethod || (editingInterface ? editingInterface.httpMethod : 'ALL'),
active: true
};
console.log('提交的分组值:', groupValue);
// 根据代理类型添加不同字段
if (values.proxyType === 'redirect' || values.proxyType === 'url_redirect') {
interfaceData.targetUrl = values.targetUrl;
interfaceData.headers = headers;
}
// 添加参数匹配规则(始终发送,空数组用于清空后端旧值)
const filteredParamMatchers = Array.isArray(values.paramMatchers)
? values.paramMatchers.filter(matcher => matcher && matcher.paramPath && matcher.paramValue)
: [];
interfaceData.paramMatchers = filteredParamMatchers;
console.log('提交接口数据:', interfaceData);
if (editingInterface) {
// 更新接口
const response = await axios.put(`/cgi-bin/interfaces?id=${editingInterface.id}`, interfaceData);
if (response.data && response.data.code === 0) {
message.success('接口更新成功');
setModalVisible(false);
fetchInterfaces();
// 刷新规则缓存
refreshCacheAfterUpdate();
} else {
throw new Error(response.data?.message || '接口更新失败');
}
} else {
// 创建接口
const response = await axios.post('/cgi-bin/interfaces', interfaceData);
if (response.data && response.data.code === 0) {
message.success('接口创建成功');
console.log('接口创建成功,返回数据:', response.data);
console.log('返回的接口数据中的分组信息:', response.data.data?.group);
setModalVisible(false);
fetchInterfaces();
// 刷新规则缓存
refreshCacheAfterUpdate();
} else {
throw new Error(response.data?.message || '接口创建失败');
}
}
} catch (error) {
console.error('表单提交失败:', error);
message.error(error.response?.data?.message || error.message || '操作失败');
}
};
const handleCancel = () => {
setModalVisible(false);
setIsDuplicateMode(false);
};
const handleSelectFeature = (featureId) => {
setSelectedFeatureId(featureId);
localStorage.setItem('selectedFeatureId', featureId);
// 切换功能模块时重置分页到第一页,但保留其他配置
saveTableConfig({ current: 1 });
};
// 预览响应内容
const handlePreview = () => {
// 获取当前选中的响应内容
const responses = form.getFieldValue('responses') || [];
const activeResponseId = form.getFieldValue('activeResponseId');
const activeResponse = responses.find(r => r.id === activeResponseId);
if (!activeResponse) {
message.error('未找到有效的响应内容');
return;
}
// 根据内容类型格式化响应内容
const contentType = form.getFieldValue('contentType') || '';
const formattedContent = formatResponseContent(activeResponse.content, contentType);
// 设置预览内容,包含响应名称
setPreviewContent({
title: `预览: ${activeResponse.name || '未命名响应'}`,
content: formattedContent,
description: '',
contentType
});
setPreviewVisible(true);
};
const handleResponseSelect = (responseId) => {
setCurrentResponseId(responseId);
};
// 在列表页面直接切换响应数据
const handleResponseSwitch = async (interfaceId, responseId) => {
try {
const response = await axios.patch(`/cgi-bin/interfaces?id=${interfaceId}`, {
activeResponseId: responseId
});
if (response.data.code === 0) {
// 更新本地状态
setInterfaces(interfaces.map(item =>
item.id === interfaceId
? { ...item, activeResponseId: responseId }
: item
));
const activeResponse = interfaces
.find(item => item.id === interfaceId)
?.responses
?.find(resp => resp.id === responseId);
message.success(`已切换到响应: ${activeResponse?.name || '未命名'}`);
// 刷新缓存以立即生效
await refreshCacheAfterUpdate();
}
} catch (error) {
console.error('切换响应失败:', error);
message.error('切换响应失败: ' + (error.response?.data?.message || error.message));
}
};
const filteredInterfaces = getFilteredInterfaces();
// 处理表格变化(排序、分页)
const handleTableChange = (pagination, filters, sorter) => {
console.log('表格变化:', { pagination, filters, sorter });
// 保存排序配置
const sortConfig = {
sortField: sorter.field || null,
sortOrder: sorter.order || null,
current: pagination.current,
pageSize: pagination.pageSize
};
saveTableConfig(sortConfig);
};
// 处理列配置变更
const handleColumnConfigChange = (checkedValues) => {
// 确保必须显示的列始终被选中
const requiredColumns = COLUMN_CONFIG.filter(col => col.required).map(col => col.key);
const finalColumns = [...new Set([...requiredColumns, ...checkedValues])];
saveTableConfig({ visibleColumns: finalColumns });
};
// 全选列配置
const handleSelectAllColumns = () => {
const allColumns = COLUMN_CONFIG.map(col => col.key);
saveTableConfig({ visibleColumns: allColumns });
};
// 重置列配置
const handleResetColumns = () => {
const defaultColumns = COLUMN_CONFIG.map(col => col.key);
saveTableConfig({ visibleColumns: defaultColumns });
};
// 切换列配置面板显示
const handleColumnConfigToggle = (visible) => {
setColumnConfigVisible(visible);
};
// 搜索处理
const handleSearch = (value) => {
setSearchValue(value);
};
// 清除搜索
const handleClearSearch = () => {
setSearchValue('');
};
// 表格行选择处理
const onSelectChange = (selectedKeys) => {
setSelectedRowKeys(selectedKeys);
};
// 刷新数据
const refreshData = () => {
fetchInterfaces();
message.success('数据已刷新');
};
// 批量操作接口状态
const handleBatchOperation = async (active) => {
if (!selectedRowKeys || selectedRowKeys.length === 0) {
message.warning('请先选择要操作的接口');
return;
}
try {
setLoading(true);
const operations = selectedRowKeys.map(id =>
axios.patch(`/cgi-bin/interfaces/${id}`, { active })
);
await Promise.all(operations);
message.success(`已${active ? '启用' : '禁用'} ${selectedRowKeys.length} 个接口`);
setSelectedRowKeys([]);
fetchInterfaces();
// 刷新规则缓存
refreshCacheAfterUpdate();
} catch (error) {
console.error('批量操作失败:', error);
message.error('批量操作失败: ' + (error.response?.data?.message || error.message));
} finally {
setLoading(false);
}
};
// 定义所有可用的列
const allColumns = [
{
title: '状态',
dataIndex: 'active',
key: 'active',
width: 80,
render: (active, record) => (
<Switch
checked={active !== false}
onChange={(checked) => handleToggleActive(record.id, active !== false)}
size="small"
/>
),
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
sorter: (a, b) => a.name.localeCompare(b.name),
render: (text) => <span className="interface-name">{text}</span>,
},
{
title: '分组',
dataIndex: 'group',
key: 'group',
width: 120,
sorter: (a, b) => {
const groupA = a.group || '';
const groupB = b.group || '';
return groupA.localeCompare(groupB);
},
render: (group) => {
if (!group) {
return <span style={{ color: '#999', fontStyle: 'italic' }}>未分组</span>;
}
return (
<Tag color="blue" style={{ cursor: 'pointer' }} onClick={() => setSelectedGroup(group)}>
{group}
</Tag>
);
},
},
{
title: 'URL匹配规则',
dataIndex: 'urlPattern',
key: 'urlPattern',
ellipsis: true,
render: (text) => <span className="url-pattern">{text}</span>,
},
{
title: '处理方式',
dataIndex: 'proxyType',
key: 'proxyType',
width: 120,
render: (text) => {
const proxyType = proxyTypes.find(item => item.value === text);
return (
<Badge
color={proxyType?.color || '#999'}
text={proxyType?.label || text}
/>
);
},
},
{
title: '当前响应',
dataIndex: 'activeResponseId',
key: 'currentResponse',
width: 200,
render: (activeResponseId, record) => {
// 如果没有响应数据,返回空
if (!record.responses || !Array.isArray(record.responses) || record.responses.length === 0) {
return <span className="no-response">无响应数据</span>;
}
return (
<Select
value={activeResponseId || record.responses[0]?.id}
style={{ width: '100%' }}
onChange={(value) => handleResponseSwitch(record.id, value)}
disabled={record.proxyType !== 'response'}
>
{record.responses.map(resp => {
return (
<Option key={resp.id} value={resp.id}>
{resp.name}
</Option>
);
})}
</Select>
);
},
},
{
title: '延迟(毫秒)',
dataIndex: 'responseDelay',
key: 'responseDelay',
width: 100,
sorter: (a, b) => (a.responseDelay || 0) - (b.responseDelay || 0),
render: (delay) => {
const delayValue = parseInt(delay, 10) || 0;
return (
<span className={delayValue > 0 ? 'delay-active' : 'delay-inactive'}>
{delayValue > 0 ? delayValue : '无延迟'}
</span>
);
},
},
{
title: '状态码',
dataIndex: 'httpStatus',
key: 'httpStatus',
width: 100,
render: (status) => {
const statusCode = status || 200;
let statusClass = 'status-success';
if (statusCode >= 400) {
statusClass = 'status-error';
} else if (statusCode >= 300) {
statusClass = 'status-warning';
}
return <span className={statusClass}>{statusCode}</span>;
},
},
{
title: '内容类型',
dataIndex: 'contentType',
key: 'contentType',
width: 120,
sorter: (a, b) => {
const aType = a.contentType || '';
const bType = b.contentType || '';
return aType.localeCompare(bType);
},
sortDirections: ['ascend', 'descend'],
render: (text, record) => {
if (record.proxyType !== 'response') {
return '-';
}
const found = contentTypes.find(item => item.value === text);
return found ? found.label : text;
}
},
{
title: '目标URL',
dataIndex: 'targetUrl',
key: 'targetUrl',
ellipsis: true,
sorter: (a, b) => {
const aUrl = a.targetUrl || '';
const bUrl = b.targetUrl || '';
return aUrl.localeCompare(bUrl);
},
sortDirections: ['ascend', 'descend'],
render: (text, record) => {
return (record.proxyType === 'redirect' || record.proxyType === 'url_redirect') ? text : '-';
}
},
{
title: '自定义头',
dataIndex: 'customHeaders',
key: 'customHeaders',
width: 100,
sorter: (a, b) => {
const aCount = Object.keys(a.customHeaders || {}).length;
const bCount = Object.keys(b.customHeaders || {}).length;
return aCount - bCount;
},
sortDirections: ['ascend', 'descend'],
render: (_, record) => {
if (record.proxyType !== 'redirect' && record.proxyType !== 'url_redirect') {
return '-';
}
const headers = record.customHeaders || {};
const count = Object.keys(headers).length;
if (count === 0) {
return '-';
}
// 检查是否包含随机值
const hasRandomValue = Object.values(headers).some(v => v && v.startsWith('@'));
return (
<Tooltip title={
<div>
{Object.entries(headers).map(([key, value]) => (
<div key={key}>
{key}: {value}
{value && value.startsWith('@') && (
<span style={{ color: '#52c41a' }}> (随机)</span>
)}
</div>
))}
</div>
}>
<span style={{ color: '#1677ff' }}>
{count}个
{hasRandomValue && <span style={{ marginLeft: 4 }}>🎲</span>}
</span>
</Tooltip>
);
}
},
{
title: '参数匹配',
dataIndex: 'paramMatchers',
key: 'paramMatchers',
width: 120,
render: (_, record) => {
if (record.proxyType !== 'response') {
return '-';
}
const showInterface = interfaceParamMatcherEnabled;
const showResponse = responseParamMatcherEnabled;
const interfaceMatchers = record.paramMatchers || [];
const interfaceCount = showInterface ? interfaceMatchers.filter(m => m.paramPath).length : 0;
// 统计所有响应级规则
const responsesWithMatchers = showResponse
? (record.responses || []).filter(resp =>
Array.isArray(resp.paramMatchers) && resp.paramMatchers.filter(m => m.paramPath).length > 0
)
: [];
const responseMatcherTotal = responsesWithMatchers.reduce(
(sum, resp) => sum + resp.paramMatchers.filter(m => m.paramPath).length, 0
);
if (interfaceCount === 0 && responseMatcherTotal === 0) {
return '-';
}
const tooltipContent = (
<div style={{ maxWidth: 280 }}>
{interfaceCount > 0 && (
<div style={{ marginBottom: responsesWithMatchers.length > 0 ? 8 : 0 }}>
<div style={{ fontWeight: 'bold', marginBottom: 4, color: '#faad14' }}>接口级条件({interfaceCount}条)</div>
{interfaceMatchers.filter(m => m.paramPath).map((m, i) => (
<div key={i} style={{ fontSize: '12px' }}>
{m.paramPath}: {m.paramValue}
<span style={{ color: '#52c41a', marginLeft: 4 }}>
({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
</span>
</div>
))}
</div>
)}
{responsesWithMatchers.length > 0 && (
<div>
<div style={{ fontWeight: 'bold', marginBottom: 4, color: '#1677ff' }}>响应级规则(共{responseMatcherTotal}条)</div>
{responsesWithMatchers.map((resp, ri) => (
<div key={ri} style={{ marginBottom: 4 }}>
<div style={{ fontSize: '12px', color: '#aaa', marginBottom: 2 }}>{resp.name}:</div>
{resp.paramMatchers.filter(m => m.paramPath).map((m, mi) => (
<div key={mi} style={{ fontSize: '12px', paddingLeft: 8 }}>
{m.paramPath}: {m.paramValue}
<span style={{ color: '#52c41a', marginLeft: 4 }}>
({m.matchType === 'exact' ? '精确' : m.matchType === 'contains' ? '包含' : '正则'})
</span>
</div>
))}
</div>
))}
</div>
)}
</div>
);
return (
<Tooltip title={tooltipContent}>
<Space size={4} direction="vertical" style={{ lineHeight: 1.4 }}>
{interfaceCount > 0 && (
<Tag color="orange" style={{ fontSize: '11px', padding: '0 4px' }}>接口:{interfaceCount}条</Tag>
)}
{responseMatcherTotal > 0 && (
<Tag color="blue" style={{ fontSize: '11px', padding: '0 4px' }}>响应:{responseMatcherTotal}条</Tag>
)}
</Space>
</Tooltip>
);
}
},
{
title: '请求方法',
dataIndex: 'httpMethod',
key: 'httpMethod',
width: 120,
sorter: (a, b) => {
const aMethod = a.httpMethod || '';
const bMethod = b.httpMethod || '';
return aMethod.localeCompare(bMethod);
},
sortDirections: ['ascend', 'descend'],
render: (text) => {
const found = httpMethods.find(item => item.value === text);
return found ? found.label : text;
}
},
{
title: '操作',
key: 'action',
width: 220,
render: (_, record) => (
<div className="action-buttons">
<Button
type="text"
icon={<EditOutlined />}
onClick={() => handleEditInterface(record)}
/>
{record.proxyType === 'response' && (
<Tooltip title="数据管理">
<Button
type="text"
icon={<FileTextOutlined />}
onClick={() => { setDataModalRecord(record); setDataModalVisible(true); }}
/>
</Tooltip>
)}
<Tooltip title="复制新增">
<Button
type="text"
icon={<CopyOutlined />}
onClick={() => handleDuplicate(record)}
/>
</Tooltip>
<Popconfirm
title="确定要删除此接口吗?"
onConfirm={() => handleDeleteInterface(record.id)}
okText="确定"
cancelText="取消"
>
<Button type="text" danger icon={<DeleteOutlined />} />
</Popconfirm>
</div>
),
},
];
// 根据配置过滤可见列
const visibleColumns = tableConfig.visibleColumns || COLUMN_CONFIG.map(col => col.key);
const columns = allColumns.filter(col => visibleColumns.includes(col.key));
// 分组筛选和操作区域
const handleBatchToggleActive = async (active) => {
try {
setGroupActionLoading(true);
const response = await axios.patch(`/cgi-bin/interfaces?active=${active}`, {
featureId: selectedFeatureId,
group: selectedGroup
});
if (response.data && response.data.code === 0) {
message.success(`分组 "${selectedGroup}" 中的接口已成功${active ? '启用' : '禁用'}`);
fetchInterfaces();
} else {
throw new Error(response.data?.message || '批量操作失败');
}
} catch (error) {
console.error('批量操作失败:', error);
message.error(error.response?.data?.message || error.message || '批量操作失败');
} finally {
setGroupActionLoading(false);
}
};
return (
<AppLayout>
<div className="interface-management-container">
{/* 警告信息置顶显示 */}
{!features.length && (
<Alert
message="未找到功能模块"
description="请先在模拟数据页面创建功能模块,然后再添加接口"
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>
)}
{selectedFeature?.active === false && (
<Alert
message="功能模块已禁用"
description="当前功能模块已被禁用,所有关联接口不会生效。您可以在模拟数据页面启用此功能模块。"
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>
)}
<Card className="interface-control-panel">
<div className="control-panel-header">
<div className="control-left">
<div className="feature-selector">
<span>功能模块:</span>
<Select
value={selectedFeatureId}
onChange={handleSelectFeature}
style={{ width: 200 }}
placeholder="选择功能模块"
loading={featuresLoading}
dropdownMatchSelectWidth={false}
>
{(features || []).map(feature => (
<Option key={feature.id} value={feature.id}>
{feature.name}
{feature.active === false &&
<span style={{ color: '#ff4d4f', marginLeft: 8 }}>(已禁用)</span>
}
</Option>
))}
</Select>
</div>
<Search
ref={searchRef}
placeholder="搜索接口名称或URL"
onSearch={handleSearch}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
style={{ width: 220, marginLeft: 12 }}
enterButton={<SearchOutlined />}
allowClear
/>
<Dropdown
overlay={
<Menu
selectedKeys={selectedGroup ? [selectedGroup] : ['ALL_GROUPS']}
onClick={({ key }) => {
if (key === 'ALL_GROUPS') {
setSelectedGroup(null);
} else {
setSelectedGroup(key === selectedGroup ? null : key);
}
}}
>
<Menu.Item key="ALL_GROUPS">全部分组</Menu.Item>
{groups.map(group => (
<Menu.Item key={group}>
{group} ({interfaces.filter(item => item.group === group).length})
</Menu.Item>
))}
</Menu>
}
trigger={['click']}
disabled={groups.length === 0}
>
<Button style={{ marginLeft: 12 }} icon={<FilterOutlined />}>
{selectedGroup ? `分组: ${selectedGroup}` : '分组筛选'}
<DownOutlined />
</Button>
</Dropdown>
</div>
<div className="control-right">
<Space>
<Tooltip title="刷新数据">
<Button
icon={<ReloadOutlined />}
onClick={refreshData}
loading={interfacesLoading}
/>
</Tooltip>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleAddInterface}
disabled={!selectedFeatureId || selectedFeature?.active === false}
>
添加接口
</Button>
</Space>
</div>
</div>
{/* 统计和批量操作行 */}
<div className="control-panel-stats">
<div className="stats-left">
<Space size="large">
{selectedGroup && (
<span className="stat-item">
<span className="stat-label">当前分组:</span>
<Tag color="blue">{selectedGroup}</Tag>
</span>
)}
<span className="stat-item">
{searchValue ? (
<Badge
count={`搜索"${searchValue}" - ${filteredInterfaces.length}个结果`}
style={{ backgroundColor: '#108ee9' }}
/>
) : (
<Badge
count={`共 ${filteredInterfaces.length} 个接口`}
style={{ backgroundColor: '#52c41a' }}
/>
)}
</span>
</Space>
</div>
<div className="stats-right">
{selectedRowKeys.length > 0 && (
<Space>
<span className="selected-count">
已选择 {selectedRowKeys.length} 项
</span>
<Popconfirm
title="确定要批量启用选中的接口吗?"
onConfirm={() => handleBatchOperation(true)}
okText="确定"
cancelText="取消"
>
<Button size="small" type="primary">批量启用</Button>
</Popconfirm>
<Popconfirm
title="确定要批量禁用选中的接口吗?"
onConfirm={() => handleBatchOperation(false)}
okText="确定"
cancelText="取消"
>
<Button size="small" danger>批量禁用</Button>
</Popconfirm>
</Space>
)}
{selectedGroup && (
<Space style={{ marginLeft: selectedRowKeys.length > 0 ? 16 : 0 }}>
<Popconfirm
title={`确定要启用分组 "${selectedGroup}" 中的所有接口吗?`}
onConfirm={() => handleBatchToggleActive(true)}
okText="确定"
cancelText="取消"
>
<Button
type="primary"
size="small"
loading={groupActionLoading}
>
分组启用
</Button>
</Popconfirm>
<Popconfirm
title={`确定要禁用分组 "${selectedGroup}" 中的所有接口吗?`}
onConfirm={() => handleBatchToggleActive(false)}
okText="确定"
cancelText="取消"
>
<Button
danger
size="small"
loading={groupActionLoading}
>
分组禁用
</Button>
</Popconfirm>
</Space>
)}
{/* 列设置按钮 */}
<Popover
title="自定义显示列"
trigger="click"
open={columnConfigVisible}
onOpenChange={handleColumnConfigToggle}
content={
<div style={{ width: 280 }}>
<div style={{ marginBottom: 12 }}>
<Space>
<Button size="small" onClick={handleSelectAllColumns}>
全选
</Button>
<Button size="small" onClick={handleResetColumns}>
重置
</Button>
</Space>
</div>
<Checkbox.Group
value={tableConfig.visibleColumns || COLUMN_CONFIG.map(col => col.key)}
onChange={handleColumnConfigChange}
style={{ width: '100%' }}
>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{COLUMN_CONFIG.map(col => (
<Checkbox
key={col.key}
value={col.key}
disabled={col.required}
style={{
width: '100%',
color: col.required ? '#999' : undefined
}}
>
{col.title}
{col.required && (
<span style={{ color: '#999', fontSize: '12px', marginLeft: 4 }}>
(必须)
</span>
)}
</Checkbox>
))}
</div>
</Checkbox.Group>
</div>
}
>
<Tooltip title="自定义表格显示列">
<Button
icon={<SettingOutlined />}
type="text"
size="small"
className="inline-column-settings-btn"
style={{ marginLeft: (selectedRowKeys.length > 0 || selectedGroup) ? 16 : 0 }}
>
列设置
</Button>
</Tooltip>
</Popover>
</div>
</div>
</Card>
<Card className="list-container-card" bordered={false} bodyStyle={{ padding: 0 }}>
<div className="interface-list-container">
<Table
columns={columns}
dataSource={filteredInterfaces}
rowKey="id"
loading={interfacesLoading}
onChange={handleTableChange}
rowSelection={{
selectedRowKeys,
onChange: onSelectChange,
}}
size="middle"
pagination={{
current: tableConfig.current,
pageSize: tableConfig.pageSize,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => `共 ${total} 个接口,显示第 ${range[0]}-${range[1]} 个`,
pageSizeOptions: ['10', '20', '50', '100'],
size: 'default'
}}
locale={{ emptyText: '暂无接口配置' }}
sortDirections={['ascend', 'descend']}
className="interface-table"
/>
</div>
</Card>
{/* 接口编辑/创建模态框/抽屉 */}
{interfaceFormDisplayMode === 'drawer' ? (
<Drawer
title={
<div style={{
fontSize: '18px',
fontWeight: 600,
color: '#262626',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<span>{editingInterface ? '编辑接口' : (isDuplicateMode ? '复制新增接口' : '添加接口')}</span>
<Badge
count="配置向导"
style={{
backgroundColor: '#f0f9ff',
color: '#1677ff',
fontSize: '11px',
fontWeight: 'normal'
}}
/>