UNPKG

whistle.mock-plugins

Version:

Whistle 插件,用于快速创建 API 模拟数据

1,519 lines (1,374 loc) 97.5 kB
/** * 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' }} />