UNPKG

whistle.mock-plugins

Version:

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

925 lines (838 loc) 30.6 kB
/** * MockDataV2.js - 功能模块管理页面 */ import React, { useState, useEffect } from 'react'; import { useRef } from 'react'; import { useHistory } from 'react-router-dom'; import AppLayout from '../components/AppLayout'; import { Card, Row, Col, Button, Modal, Form, Input, message, Switch, Empty, Spin, Typography, Tooltip, Badge, Select, Pagination, Table, Space, Tag } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, ApiOutlined, ExportOutlined, InfoCircleOutlined, CalendarOutlined, SortAscendingOutlined, AppstoreOutlined, UnorderedListOutlined } from '@ant-design/icons'; import '../styles/mock-data.css'; import axios from 'axios'; const { Text, Title, Paragraph } = Typography; const { TextArea } = Input; // 刷新缓存服务 const flushCache = async () => { try { const response = await axios.get(`/_flush_cache`); return response.data; } catch (error) { console.error('刷新缓存失败:', error); throw error; } }; const MockData = () => { const history = useHistory(); const [mockFeatures, setMockFeatures] = useState([]); const [showModal, setShowModal] = useState(false); const [currentFeature, setCurrentFeature] = useState(null); const [loading, setLoading] = useState(true); const [form] = Form.useForm(); // 排序和分页配置(支持缓存) const [listConfig, setListConfig] = useState(() => { const cached = localStorage.getItem('feature-list-config'); return cached ? JSON.parse(cached) : { sortBy: 'name', // name, createdAt, interfaceCount, active sortOrder: 'ascend', // ascend, descend pageSize: 12, current: 1, viewMode: 'card' // card, list }; }); // 保存列表配置到localStorage const saveListConfig = (config) => { const newConfig = { ...listConfig, ...config }; setListConfig(newConfig); localStorage.setItem('feature-list-config', JSON.stringify(newConfig)); }; // 加载功能列表 useEffect(() => { fetchFeatures(); }, []); // 键盘快捷键监听 useEffect(() => { const handleSave = () => { if (showModal) { handleSubmit(); } }; const handleNew = () => { openModal(); }; window.addEventListener('shortcut-save', handleSave); window.addEventListener('shortcut-new-interface', handleNew); return () => { window.removeEventListener('shortcut-save', handleSave); window.removeEventListener('shortcut-new-interface', handleNew); }; }, [showModal]); // 获取所有功能模块 const fetchFeatures = async () => { try { setLoading(true); const response = await fetch('/cgi-bin/features'); const result = await response.json(); if (result.code === 0) { setMockFeatures(result.data || []); } else { console.error('获取功能模块失败:', result.message); message.error('获取功能模块失败: ' + result.message); } } catch (error) { console.error('获取功能模块错误:', error); message.error('获取功能模块失败, 请检查网络连接'); } finally { setLoading(false); } }; const openModal = (feature = null) => { if (feature) { setCurrentFeature(feature); form.setFieldsValue({ name: feature.name, description: feature.description, active: feature.active !== false }); } else { setCurrentFeature(null); form.resetFields(); form.setFieldsValue({ active: true }); } setShowModal(true); }; const closeModal = () => { setShowModal(false); }; // 更新成功后刷新缓存 const refreshCacheAfterUpdate = async () => { try { await flushCache(); // 不显示提示,避免干扰主操作的反馈 } catch (error) { // 记录错误但不影响用户体验 console.error('刷新缓存失败,可能需要等待缓存自动过期:', error); } }; const handleSubmit = async (values) => { try { const featureData = { ...values }; // 如果是编辑已有功能,添加ID if (currentFeature) { featureData.id = currentFeature.id; } setLoading(true); const response = await fetch('/cgi-bin/features', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(featureData) }); const result = await response.json(); if (result.code === 0) { // 刷新功能列表 message.success(currentFeature ? '功能模块更新成功' : '功能模块创建成功'); fetchFeatures(); closeModal(); // 刷新缓存 refreshCacheAfterUpdate(); } else { message.error('操作失败: ' + result.message); } } catch (error) { console.error('保存功能模块错误:', error); message.error('保存失败: ' + error.message); } finally { setLoading(false); } }; const handleToggleActive = async (id, currentActive) => { try { const response = await fetch(`/cgi-bin/features?id=${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ active: !currentActive }) }); const result = await response.json(); if (result.code === 0) { message.success(`${currentActive ? '禁用' : '启用'}功能模块成功`); // 更新本地状态 setMockFeatures(mockFeatures.map(feature => feature.id === id ? { ...feature, active: !currentActive } : feature )); // 刷新缓存 - 功能启用状态变化是最关键的需要刷新缓存的操作 refreshCacheAfterUpdate(); } else { message.error(`${currentActive ? '禁用' : '启用'}功能模块失败: ${result.message}`); } } catch (error) { console.error(`${currentActive ? '禁用' : '启用'}功能模块错误:`, error); message.error(`操作失败: ${error.message}`); } }; const deleteFeature = async (id) => { Modal.confirm({ title: '确定要删除此功能吗?', content: '这将删除所有相关的接口和模拟数据,此操作无法恢复。', okText: '删除', okType: 'danger', cancelText: '取消', onOk: async () => { try { setLoading(true); const response = await fetch(`/cgi-bin/features?id=${id}`, { method: 'DELETE' }); const result = await response.json(); if (result.code === 0) { message.success('功能模块已成功删除'); // 更新本地状态 setMockFeatures(mockFeatures.filter(f => f.id !== id)); // 刷新缓存 refreshCacheAfterUpdate(); } else { message.error('删除失败: ' + result.message); } } catch (error) { console.error('删除功能错误:', error); message.error('操作失败: ' + error.message); } finally { setLoading(false); } } }); }; const viewInterfaces = (feature) => { localStorage.setItem('selectedFeatureId', feature.id); history.push(`/interface/${feature.id}`); }; const exportFeatureConfig = async (feature) => { try { // 获取该功能的所有接口 const response = await fetch(`/cgi-bin/interfaces?featureId=${feature.id}`); const result = await response.json(); // 创建完整配置 const config = { ...feature, interfaces: result.code === 0 ? result.data : [] }; const dataStr = JSON.stringify(config, null, 2); const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); const exportFileDefaultName = `whistle-mock-feature-${feature.id}.json`; const linkElement = document.createElement('a'); linkElement.setAttribute('href', dataUri); linkElement.setAttribute('download', exportFileDefaultName); linkElement.click(); message.success('配置导出成功'); } catch (error) { console.error('导出配置错误:', error); message.error('导出失败: ' + error.message); } }; const importFeatureConfig = () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (event) => { try { const config = JSON.parse(event.target.result); // 验证导入的配置 if (!config.name) { message.error('无效的配置文件: 缺少功能名称'); return; } // 创建新功能 const featureData = { name: config.name, description: config.description || '', active: config.active !== false }; // 保存功能 const featureResponse = await fetch('/cgi-bin/features', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(featureData) }); const featureResult = await featureResponse.json(); if (featureResult.code === 0) { const newFeature = featureResult.data; // 导入接口配置 if (Array.isArray(config.interfaces) && config.interfaces.length > 0) { message.loading({ content: '正在导入接口配置...', key: 'importInfo' }); let successCount = 0; let errorCount = 0; for (const interfaceItem of config.interfaces) { try { // 创建接口,使用新功能ID const interfaceData = { ...interfaceItem, featureId: newFeature.id, id: undefined // 不使用原接口ID }; await fetch('/cgi-bin/interfaces', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(interfaceData) }); successCount++; } catch (error) { console.error('导入接口配置失败:', error); errorCount++; } } if (errorCount > 0) { message.info({ content: `导入完成,成功 ${successCount} 个接口,失败 ${errorCount} 个接口`, key: 'importInfo', duration: 3 }); } else { message.success({ content: `成功导入 ${successCount} 个接口`, key: 'importInfo', duration: 2 }); } } else { message.success('功能导入成功(无接口配置)'); } // 刷新功能列表 fetchFeatures(); } else { message.error('导入功能失败: ' + featureResult.message); } } catch (error) { console.error('导入配置错误:', error); message.error('导入失败: ' + error.message); } }; reader.readAsText(file); }; input.click(); }; // 排序处理 const handleSortChange = (value) => { saveListConfig({ sortBy: value, current: 1 }); }; const handleSortOrderChange = (value) => { saveListConfig({ sortOrder: value, current: 1 }); }; const handlePageSizeChange = (value) => { saveListConfig({ pageSize: value, current: 1 }); }; const handlePageChange = (page, pageSize) => { saveListConfig({ current: page, pageSize }); }; const handleViewModeChange = (mode) => { saveListConfig({ viewMode: mode, current: 1 }); }; // 获取排序后的功能列表 const getSortedFeatures = () => { const sorted = [...mockFeatures].sort((a, b) => { let aValue, bValue; switch (listConfig.sortBy) { case 'name': aValue = a.name || ''; bValue = b.name || ''; return listConfig.sortOrder === 'ascend' ? aValue.localeCompare(bValue, 'zh-CN') : bValue.localeCompare(aValue, 'zh-CN'); case 'createdAt': aValue = new Date(a.createdAt || 0).getTime(); bValue = new Date(b.createdAt || 0).getTime(); return listConfig.sortOrder === 'ascend' ? aValue - bValue : bValue - aValue; case 'interfaceCount': aValue = a.interfaceCount || 0; bValue = b.interfaceCount || 0; return listConfig.sortOrder === 'ascend' ? aValue - bValue : bValue - aValue; case 'active': aValue = a.active ? 1 : 0; bValue = b.active ? 1 : 0; return listConfig.sortOrder === 'ascend' ? aValue - bValue : bValue - aValue; default: return 0; } }); return sorted; }; // 获取分页后的功能列表 const getPaginatedFeatures = () => { const sortedFeatures = getSortedFeatures(); const startIndex = (listConfig.current - 1) * listConfig.pageSize; const endIndex = startIndex + listConfig.pageSize; return sortedFeatures.slice(startIndex, endIndex); }; // 渲染功能模块卡片 const renderFeatureCard = (feature) => { const formattedDate = feature.createdAt ? new Date(feature.createdAt).toLocaleDateString() : '未知日期'; // 处理卡片点击事件 const handleCardClick = (e) => { // 检查点击目标是否是开关或其父元素 const target = e.target; const switchElement = target.closest('.ant-switch') || target.closest('.feature-name .ant-switch'); // 如果点击的是开关区域,则不执行跳转 if (switchElement) { return; } // 检查是否点击了操作按钮区域 const actionElement = target.closest('.ant-card-actions') || target.closest('.ant-card-actions li'); if (actionElement) { return; } // 执行跳转到接口管理界面 viewInterfaces(feature); }; // 处理开关点击事件,阻止事件冒泡 const handleSwitchClick = (checked, e) => { e.stopPropagation(); // 阻止事件冒泡到卡片 handleToggleActive(feature.id, feature.active); }; return ( <Col xs={24} sm={12} md={8} lg={6} key={feature.id} style={{ marginBottom: 16 }}> <Badge.Ribbon text={feature.active ? '已启用' : '已禁用'} color={feature.active ? '#52c41a' : '#f5222d'} style={{ display: 'block' }} > <Card className={`feature-card ${!feature.active ? 'inactive-feature' : ''}`} onClick={handleCardClick} style={{ cursor: 'pointer' }} actions={[ <Tooltip title="管理接口"> <ApiOutlined key="interfaces" onClick={(e) => { e.stopPropagation(); viewInterfaces(feature); }} /> </Tooltip>, <Tooltip title="编辑功能"> <EditOutlined key="edit" onClick={(e) => { e.stopPropagation(); openModal(feature); }} /> </Tooltip>, <Tooltip title="删除功能"> <DeleteOutlined key="delete" onClick={(e) => { e.stopPropagation(); deleteFeature(feature.id); }} /> </Tooltip>, <Tooltip title="导出配置"> <ExportOutlined key="export" onClick={(e) => { e.stopPropagation(); exportFeatureConfig(feature); }} /> </Tooltip> ]} > <div className="feature-card-content"> <div className="feature-name"> <Title level={4} ellipsis={{ tooltip: feature.name }}> {feature.name} </Title> <Switch checked={feature.active} onChange={handleSwitchClick} size="small" onClick={() => {event.stopPropagation()}} /> </div> <Paragraph className="feature-description" ellipsis={{ rows: 2, expandable: false, tooltip: feature.description }}> {feature.description || '无描述'} </Paragraph> <div className="feature-stat"> <span> <InfoCircleOutlined /> {feature.interfaceCount || 0} 个接口 </span> <span className="feature-date"> <CalendarOutlined /> {formattedDate} </span> </div> </div> </Card> </Badge.Ribbon> </Col> ); }; // 渲染列表视图 const renderListView = () => { const columns = [ { title: '状态', dataIndex: 'active', key: 'active', width: 100, render: (active, record) => ( <Switch checked={active} onChange={(checked, e) => { e.stopPropagation(); handleToggleActive(record.id, active); }} checkedChildren="启用" unCheckedChildren="禁用" /> ), }, { title: '功能名称', dataIndex: 'name', key: 'name', ellipsis: { tooltip: true }, render: (name) => ( <Text strong style={{ fontSize: '14px' }}>{name}</Text> ), }, { title: '功能描述', dataIndex: 'description', key: 'description', ellipsis: { tooltip: true }, render: (description) => ( <Text type="secondary">{description || '无描述'}</Text> ), }, { title: '接口数量', dataIndex: 'interfaceCount', key: 'interfaceCount', width: 100, align: 'center', render: (count) => ( <Badge count={count || 0} showZero style={{ backgroundColor: '#1677ff' }} /> ), }, { title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 120, render: (createdAt) => ( <Text type="secondary"> {createdAt ? new Date(createdAt).toLocaleDateString() : '未知日期'} </Text> ), }, { title: '操作', key: 'action', width: 200, fixed: 'right', render: (_, record) => ( <Space size="small"> <Tooltip title="管理接口"> <Button type="link" size="small" icon={<ApiOutlined />} onClick={(e) => { e.stopPropagation(); viewInterfaces(record); }} > 接口 </Button> </Tooltip> <Tooltip title="编辑"> <Button type="link" size="small" icon={<EditOutlined />} onClick={(e) => { e.stopPropagation(); openModal(record); }} /> </Tooltip> <Tooltip title="导出"> <Button type="link" size="small" icon={<ExportOutlined />} onClick={(e) => { e.stopPropagation(); exportFeatureConfig(record); }} /> </Tooltip> <Tooltip title="删除"> <Button type="link" size="small" danger icon={<DeleteOutlined />} onClick={(e) => { e.stopPropagation(); deleteFeature(record.id); }} /> </Tooltip> </Space> ), }, ]; return ( <Table columns={columns} dataSource={getPaginatedFeatures()} rowKey="id" pagination={false} onRow={(record) => ({ onClick: () => viewInterfaces(record), style: { cursor: 'pointer' } })} rowClassName={(record) => !record.active ? 'inactive-row' : ''} /> ); }; return ( <AppLayout> <div className="page-container"> <div className="page-title-bar"> <div className="title-section"> <div className="title-with-badge"> <h1 className="page-title">功能模块管理</h1> </div> <div className="page-description"> 创建和管理功能模块,为每个功能配置独立的接口 </div> </div> <div className="page-actions"> <Button type="primary" icon={<PlusOutlined />} onClick={() => openModal()} > 新建功能 </Button> <Button type="primary" icon={<ExportOutlined />} onClick={importFeatureConfig} style={{ backgroundColor: '#52c41a', borderColor: '#52c41a' }} > 导入功能 </Button> </div> </div> {/* 控制面板 */} {mockFeatures.length > 0 && !loading && ( <Card className="control-panel-card" style={{ marginBottom: 24 }}> <Row gutter={[24, 16]} align="middle"> {/* 统计信息区域 */} <Col xs={24} sm={12} md={8} lg={6}> <div className="stats-section"> <div className="stats-item"> <span className="stats-number">{mockFeatures.length}</span> <span className="stats-label">功能模块</span> </div> <div className="stats-item"> <span className="stats-number"> {mockFeatures.filter(f => f.active).length} </span> <span className="stats-label">已启用</span> </div> <div className="stats-item"> <span className="stats-number"> {mockFeatures.reduce((sum, f) => sum + (f.interfaceCount || 0), 0)} </span> <span className="stats-label">接口总数</span> </div> </div> </Col> {/* 排序和视图控制区域 */} <Col xs={24} sm={12} md={8} lg={9}> <div className="sort-controls"> <div className="control-group"> <span className="control-label"> <SortAscendingOutlined /> 排序方式 </span> <Select value={listConfig.sortBy} onChange={handleSortChange} style={{ width: 120 }} size="middle" > <Select.Option value="name">名称</Select.Option> <Select.Option value="createdAt">创建时间</Select.Option> <Select.Option value="interfaceCount">接口数量</Select.Option> <Select.Option value="active">状态</Select.Option> </Select> <Select value={listConfig.sortOrder} onChange={handleSortOrderChange} style={{ width: 80 }} size="middle" > <Select.Option value="ascend">升序</Select.Option> <Select.Option value="descend">降序</Select.Option> </Select> </div> </div> </Col> {/* 视图切换区域 */} <Col xs={24} sm={12} md={4} lg={3}> <div className="view-mode-controls"> <Button.Group> <Tooltip title="卡片视图"> <Button type={listConfig.viewMode === 'card' ? 'primary' : 'default'} icon={<AppstoreOutlined />} onClick={() => handleViewModeChange('card')} /> </Tooltip> <Tooltip title="列表视图"> <Button type={listConfig.viewMode === 'list' ? 'primary' : 'default'} icon={<UnorderedListOutlined />} onClick={() => handleViewModeChange('list')} /> </Tooltip> </Button.Group> </div> </Col> {/* 分页控制区域 */} <Col xs={24} sm={24} md={12} lg={6}> <div className="pagination-controls"> <div className="control-group"> <span className="control-label">每页显示</span> <Select value={listConfig.pageSize} onChange={handlePageSizeChange} style={{ width: 80 }} size="middle" > <Select.Option value={8}>8</Select.Option> <Select.Option value={12}>12</Select.Option> <Select.Option value={16}>16</Select.Option> <Select.Option value={24}>24</Select.Option> </Select> <span className="page-info"> 第 {(listConfig.current - 1) * listConfig.pageSize + 1}-{Math.min(listConfig.current * listConfig.pageSize, mockFeatures.length)} 条 </span> </div> </div> </Col> </Row> </Card> )} <Card className="feature-list-card"> {loading ? ( <div className="loading-state"> <Spin size="large" /> <div className="loading-text">正在加载功能模块...</div> </div> ) : mockFeatures.length > 0 ? ( <> {listConfig.viewMode === 'card' ? ( <Row gutter={[20, 20]} className="feature-grid"> {getPaginatedFeatures().map(feature => renderFeatureCard(feature))} </Row> ) : ( renderListView() )} {/* 分页组件 */} {mockFeatures.length > listConfig.pageSize && ( <div className="pagination-wrapper"> <Pagination current={listConfig.current} pageSize={listConfig.pageSize} total={mockFeatures.length} onChange={handlePageChange} onShowSizeChange={handlePageChange} showSizeChanger={false} showQuickJumper={true} showTotal={(total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 个功能模块` } size="default" className="custom-pagination" /> </div> )} </> ) : ( <Empty className="custom-empty" image={ <div className="empty-image"> <ApiOutlined style={{ fontSize: 64, color: '#d9d9d9' }} /> </div> } description={ <div className="empty-description"> <div className="empty-title">暂无功能模块</div> <div className="empty-subtitle">创建您的第一个功能模块,开始管理接口</div> </div> } > <div className="empty-actions"> <Button type="primary" icon={<PlusOutlined />} onClick={() => openModal()}> 创建新功能 </Button> <Button icon={<ExportOutlined />} onClick={importFeatureConfig}> 导入功能 </Button> </div> </Empty> )} </Card> </div> <Modal title={currentFeature ? '编辑功能' : '新建功能'} open={showModal} onCancel={closeModal} footer={null} destroyOnClose width={500} > <Form form={form} layout="vertical" onFinish={handleSubmit} initialValues={{ name: '', description: '', active: true }} > <Form.Item name="name" label="功能名称" rules={[{ required: true, message: '请输入功能名称' }]} > <Input placeholder="请输入功能名称" /> </Form.Item> <Form.Item name="description" label="功能描述" > <TextArea placeholder="请输入功能描述(可选)" autoSize={{ minRows: 3, maxRows: 6 }} /> </Form.Item> <div className="form-actions"> <Button onClick={closeModal}> 取消 </Button> <Button type="primary" htmlType="submit" loading={loading}> 确定 </Button> </div> </Form> </Modal> </AppLayout> ); }; export default MockData;