UNPKG

whistle.mock-plugins

Version:

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

300 lines (278 loc) 12.3 kB
import React, { useState, useEffect } from 'react'; import AppLayout from '../components/AppLayout'; import { Card, Typography, Switch, Space, Modal, Button, message, Radio } from 'antd'; import { QuestionCircleOutlined } from '@ant-design/icons'; import axios from 'axios'; import '../styles/settings.css'; const { Text } = Typography; const syncSettingsToBackend = (patch) => { axios.put('/cgi-bin/settings', patch).catch(() => {}); }; const Settings = () => { const [displayMode, setDisplayMode] = useState('modal'); const [interfaceFormDisplayMode, setInterfaceFormDisplayMode] = useState('modal'); const [interfaceParamMatcherEnabled, setInterfaceParamMatcherEnabled] = useState(true); const [responseParamMatcherEnabled, setResponseParamMatcherEnabled] = useState(true); const [currentTheme, setCurrentTheme] = useState('light'); useEffect(() => { const savedMode = localStorage.getItem('dataManagementDisplayMode'); if (savedMode) setDisplayMode(savedMode); const savedInterfaceFormMode = localStorage.getItem('interfaceFormDisplayMode'); if (savedInterfaceFormMode) setInterfaceFormDisplayMode(savedInterfaceFormMode); // 优先从后端读取入参匹配开关,保持前后端一致 axios.get('/cgi-bin/settings').then(({ data }) => { const ipm = data.interfaceParamMatcherEnabled !== false; const rpm = data.responseParamMatcherEnabled !== false; setInterfaceParamMatcherEnabled(ipm); setResponseParamMatcherEnabled(rpm); localStorage.setItem('interfaceParamMatcherEnabled', String(ipm)); localStorage.setItem('responseParamMatcherEnabled', String(rpm)); window.dispatchEvent(new Event('storage')); }).catch(() => { const savedInterfaceParamMatcher = localStorage.getItem('interfaceParamMatcherEnabled'); if (savedInterfaceParamMatcher !== null) setInterfaceParamMatcherEnabled(savedInterfaceParamMatcher !== 'false'); const savedResponseParamMatcher = localStorage.getItem('responseParamMatcherEnabled'); if (savedResponseParamMatcher !== null) setResponseParamMatcherEnabled(savedResponseParamMatcher !== 'false'); }); // 读取主题设置(兼容旧版 cyberTheme) const savedTheme = localStorage.getItem('theme'); if (savedTheme) { setCurrentTheme(savedTheme); } else { const oldCyber = localStorage.getItem('cyberTheme'); const themeValue = oldCyber === 'false' ? 'light' : oldCyber === 'true' ? 'cyber' : 'light'; setCurrentTheme(themeValue); localStorage.setItem('theme', themeValue); } }, []); const handleDisplayModeChange = (checked) => { const newMode = checked ? 'drawer' : 'modal'; setDisplayMode(newMode); localStorage.setItem('dataManagementDisplayMode', newMode); window.dispatchEvent(new Event('storage')); }; const handleInterfaceFormDisplayModeChange = (checked) => { const newMode = checked ? 'drawer' : 'modal'; setInterfaceFormDisplayMode(newMode); localStorage.setItem('interfaceFormDisplayMode', newMode); window.dispatchEvent(new Event('storage')); }; const handleInterfaceParamMatcherChange = (checked) => { setInterfaceParamMatcherEnabled(checked); localStorage.setItem('interfaceParamMatcherEnabled', String(checked)); syncSettingsToBackend({ interfaceParamMatcherEnabled: checked }); window.dispatchEvent(new Event('storage')); }; const handleResponseParamMatcherChange = (checked) => { setResponseParamMatcherEnabled(checked); localStorage.setItem('responseParamMatcherEnabled', String(checked)); syncSettingsToBackend({ responseParamMatcherEnabled: checked }); window.dispatchEvent(new Event('storage')); }; const handleThemeChange = (e) => { const newTheme = e.target.value; setCurrentTheme(newTheme); localStorage.setItem('theme', newTheme); // 兼容旧版存储 localStorage.setItem('cyberTheme', String(newTheme === 'cyber')); window.dispatchEvent(new Event('storage')); const themeNames = { light: '经典亮色', cyber: '赛博朋克', earth: '大地色系' }; message.success(`${themeNames[newTheme]}主题已开启`); }; const showMatcherRuleModal = (type) => { if (type === 'interface') { Modal.info({ title: '接口入参匹配规则说明', width: 560, content: ( <div> <p>用于区分同一 URL 下的不同接口定义(接口级匹配)。</p> <p>执行顺序与优先级:</p> <ul> <li>先进行 URL 匹配,命中多个接口时,按接口匹配条件顺序查找第一条命中规则。</li> <li>若命中规则且配置了目标响应,则直接返回该响应(最高优先级)。</li> <li>若命中规则但未配置目标响应,则进入“响应入参匹配”阶段继续判断。</li> <li>若本开关关闭,则跳过接口级匹配,直接走“响应入参匹配”(若开启)。</li> </ul> <p>兜底逻辑:</p> <ul> <li>若最终没有任何入参规则命中,系统返回默认响应。</li> <li>支持嵌套参数路径,例如:data.user.id。</li> </ul> </div> ) }); return; } Modal.info({ title: '响应入参匹配规则说明', width: 560, content: ( <div> <p>用于在同一接口内,根据请求参数选择具体响应(响应级匹配)。</p> <p>执行顺序与优先级:</p> <ul> <li>仅在“接口入参匹配”未直接确定目标响应时才会执行。</li> <li>按响应配置顺序匹配,命中第一条规则即返回对应响应。</li> <li>支持精确匹配、包含匹配、正则匹配。</li> <li>若本开关关闭,则跳过响应级匹配,直接使用默认响应。</li> </ul> <p>兜底逻辑:</p> <ul> <li>开启但未命中任一规则时,使用默认响应。</li> <li>与接口级都关闭时,请求将直接返回默认响应。</li> </ul> </div> ) }); }; return ( <AppLayout> <div className="page-container"> <div className="page-title-bar"> <div> <h1 className="page-title">系统设置</h1> <div className="page-description"> 配置插件的全局设置项和界面选项 </div> </div> </div> <Card title="界面设置" className="settings-card"> <div className="settings-item"> <div className="settings-item-header"> <div> <div className="settings-item-title">数据管理交互</div> <div className="settings-item-description"> 选择数据管理的显示方式 </div> </div> <Space> <Text type="secondary"> {displayMode === 'drawer' ? '抽屉模式' : '弹窗模式'} </Text> <Switch checked={displayMode === 'drawer'} onChange={handleDisplayModeChange} /> </Space> </div> </div> <div className="settings-item"> <div className="settings-item-header"> <div> <div className="settings-item-title">接口表单交互</div> <div className="settings-item-description"> 选择添加/编辑接口的显示方式 </div> </div> <Space> <Text type="secondary"> {interfaceFormDisplayMode === 'drawer' ? '抽屉模式' : '弹窗模式'} </Text> <Switch checked={interfaceFormDisplayMode === 'drawer'} onChange={handleInterfaceFormDisplayModeChange} /> </Space> </div> </div> <div className="settings-item"> <div className="settings-item-header"> <div> <div className="settings-item-title"> 接口入参匹配 <Button type="text" size="small" className="settings-help-icon" icon={<QuestionCircleOutlined />} onClick={() => showMatcherRuleModal('interface')} /> </div> <div className="settings-item-description"> 是否在接口编辑表单中显示「接口匹配条件」配置区域 </div> </div> <Space> <Text type="secondary"> {interfaceParamMatcherEnabled ? '已开启' : '已关闭'} </Text> <Switch checked={interfaceParamMatcherEnabled} onChange={handleInterfaceParamMatcherChange} /> </Space> </div> </div> <div className="settings-item"> <div className="settings-item-header"> <div> <div className="settings-item-title"> 响应入参匹配 <Button type="text" size="small" className="settings-help-icon" icon={<QuestionCircleOutlined />} onClick={() => showMatcherRuleModal('response')} /> </div> <div className="settings-item-description"> 是否在响应内容编辑器中显示「入参匹配规则」配置区域 </div> </div> <Space> <Text type="secondary"> {responseParamMatcherEnabled ? '已开启' : '已关闭'} </Text> <Switch checked={responseParamMatcherEnabled} onChange={handleResponseParamMatcherChange} /> </Space> </div> </div> </Card> <Card title="主题设置" className="settings-card" style={{ marginTop: 24 }}> <div className="settings-item"> <div className="settings-item-header"> <div> <div className="settings-item-title">界面主题</div> <div className="settings-item-description"> 选择插件的界面主题风格 </div> </div> </div> <Radio.Group value={currentTheme} onChange={handleThemeChange} style={{ marginTop: 12 }} > <Space direction="vertical"> <Radio value="light"> <Space> <span style={{ display: 'inline-block', width: 16, height: 16, borderRadius: 4, background: '#1890ff', verticalAlign: 'middle', marginRight: 4 }} /> 经典亮色 — Ant Design 默认亮色风格 </Space> </Radio> <Radio value="cyber"> <Space> <span style={{ display: 'inline-block', width: 16, height: 16, borderRadius: 4, background: 'linear-gradient(135deg, #00f0ff, #00ff41)', verticalAlign: 'middle', marginRight: 4 }} /> 赛博朋克 — 深色霓虹科技风格 </Space> </Radio> <Radio value="earth"> <Space> <span style={{ display: 'inline-block', width: 16, height: 16, borderRadius: 4, background: 'linear-gradient(135deg, #b87333, #d4a017)', verticalAlign: 'middle', marginRight: 4 }} /> 大地色系 — 暖色调自然舒适风格 </Space> </Radio> </Space> </Radio.Group> </div> </Card> </div> </AppLayout> ); }; export default Settings;