Tsumugiboshi/index.html
2025-01-28 15:22:18 +08:00

547 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html>
<head>
<title>API调试工具</title>
<!-- Material Icons -->
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<!-- React 依赖 -->
<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<!-- Babel -->
<script src="https://unpkg.com/@babel/standalone@7.21.5/babel.min.js"></script>
<!-- Material UI 组件库 -->
<script src="https://unpkg.com/@mui/material@5.14.0/umd/material-ui.production.min.js"></script>
<style>
body {
margin: 0;
font-family: Arial, sans-serif;
min-height: 100vh;
display: flex;
flex-direction: column;
}
#root {
flex: 1;
}
pre { background: #f5f5f5; padding: 10px; border-radius: 4px; }
.api-description {
margin: 8px 0 16px !important;
color: #666;
white-space: pre-line;
line-height: 1.5;
background-color: #f8f9fa;
padding: 12px;
border-radius: 4px;
border-left: 4px solid #1976d2;
}
.selected-backend { font-weight: bold; color: #1976d2; }
.scrollable-tabs {
max-width: calc(100% - 48px); /* 为菜单按钮留出空间 */
overflow-x: auto;
overflow-y: hidden;
}
.tabs-container {
display: flex;
align-items: center;
position: relative;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}
.menu-button {
position: absolute !important;
right: 0;
top: 50%;
transform: translateY(-50%);
}
footer {
margin-top: auto;
padding: 16px;
background-color: #f5f5f5;
text-align: center;
font-size: 0.875rem;
color: #666;
}
@media (max-width: 600px) {
.MuiContainer-root {
padding: 12px !important;
}
.MuiPaper-root {
padding: 12px !important;
}
h4.MuiTypography-root {
font-size: 1.5rem;
}
.scrollable-tabs::-webkit-scrollbar {
display: none;
}
.api-description {
font-size: 0.875rem;
padding: 8px;
}
}
</style>
</head>
<body></body>
<div id="root"></div>
<footer>
<Typography variant="body2" color="textSecondary">
© 2023 Tsumugiboshi. All rights reserved.
<br />
</Typography>
</footer>
<script type="text/babel">
// 从全局对象获取Material UI组件
const {
Container,
Tabs,
Tab,
TextField,
Button,
Paper,
Typography,
Grid,
Box,
createTheme,
ThemeProvider,
Select,
MenuItem,
InputLabel,
FormControl,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Snackbar,
Alert,
Menu,
IconButton,
Link
} = MaterialUI;
// 创建主题
const theme = createTheme();
// 预设的后端地址
const PRESET_BACKENDS = [
{ label: "开发环境", value: "http://dev-api.example.com" },
{ label: "测试环境", value: "http://test-api.example.com" },
{ label: "生产环境", value: "http://api.example.com" }
];
// 通用表单组件
const DynamicForm = ({ formConfig, onSubmit }) => {
const [formData, setFormData] = React.useState({});
const handleChange = (fieldId, value) => {
const keys = fieldId.split('.');
setFormData(prev => {
let newData = {...prev};
let current = newData;
for (let i = 0; i < keys.length - 1; i++) {
current[keys[i]] = current[keys[i]] || {};
current = current[keys[i]];
}
current[keys[keys.length - 1]] = value;
return newData;
});
};
const handleSubmit = async () => {
if (formConfig.confirmBeforeSubmit) {
let confirmMessage = formConfig.confirmTemplate || "确认提交数据?";
if (formConfig.confirmTemplate) {
confirmMessage = confirmMessage.replace(/\{([^}]+)\}/g, (_, key) => {
return key === 'rawData' ? JSON.stringify(formData, null, 2) :
key.split('.').reduce((obj, k) => obj && obj[k], formData) || '';
});
}
if (!confirm(confirmMessage)) return;
}
onSubmit(formData);
};
return (
<div style={{ padding: '1rem' }}>
<Typography variant="body1" className="api-description">
{formConfig.tab.description}
</Typography>
<Grid container spacing={2}>
{formConfig.fields.map(field => (
<Grid item xs={12} key={field.id}>
{field.type === 'select' ? (
<FormControl fullWidth>
<InputLabel>{field.label}</InputLabel>
<Select
value={field.id.split('.').reduce((obj, key) => obj && obj[key], formData) ?? ''}
onChange={(e) => handleChange(field.id, e.target.value)}
label={field.label}
>
{field.options.map(opt => (
<MenuItem key={opt.value} value={opt.value}>
{opt.label}
</MenuItem>
))}
</Select>
</FormControl>
) : (
<TextField
fullWidth
label={field.label}
type={field.type}
value={field.id.split('.').reduce((obj, key) => obj && obj[key], formData) || ''}
onChange={(e) => handleChange(field.id, e.target.value)}
placeholder={field.placeholder}
/>
)}
</Grid>
))}
<Grid item xs={12}>
<Button
variant="contained"
color="primary"
onClick={handleSubmit}
>
提交
</Button>
</Grid>
</Grid>
</div>
);
};
function App() {
const [tabValue, setTabValue] = React.useState(0);
const [qrInput, setQrInput] = React.useState('');
const [userId, setUserId] = React.useState(getCookie('userId') || '');
const [response, setResponse] = React.useState('');
const [musicData, setMusicData] = React.useState({
userId: parseInt(userId) || 0,
music: {
musicId: 0,
level: 0,
playCount: 0,
//achievement: 0,
comboStatus: 0,
syncStatus: 0,
deluxscoreMax: 0,
scoreRank: 0
}
});
const [apiBase, setApiBase] = React.useState(
localStorage.getItem('apiBase') || PRESET_BACKENDS[0].value
);
const [backends, setBackends] = React.useState([
...PRESET_BACKENDS,
...JSON.parse(localStorage.getItem('customBackends') || '[]')
]);
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
const [newBackend, setNewBackend] = React.useState({ label: '', value: '' });
const [snackbarOpen, setSnackbarOpen] = React.useState(false);
const [snackbarMessage, setSnackbarMessage] = React.useState('');
const [menuAnchorEl, setMenuAnchorEl] = React.useState(null);
// Cookie操作函数
function setCookie(name, value, days) {
const d = new Date();
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = `${name}=${value};expires=${d.toUTCString()};path=/`;
}
function getCookie(name) {
return document.cookie.split('; ')
.find(row => row.startsWith(name + '='))
?.split('=')[1];
}
React.useEffect(() => {
if (userId) setCookie('userId', userId, 7);
}, [userId]);
const handleApiCall = async (endpoint, method = 'GET', body = null) => {
try {
const options = {
method,
headers: { 'Content-Type': 'application/json' }
};
if (body) options.body = JSON.stringify(body);
const res = await fetch(`${apiBase}${endpoint}`, options);
const data = await res.json();
setResponse(JSON.stringify(data, null, 2));
} catch (error) {
setResponse(`Error: ${error.message}`);
}
};
const handleMusicChange = (e) => {
const { name, value } = e.target;
setMusicData(prev => ({
...prev,
music: {
...prev.music,
[name]: parseInt(value) || 0
}
}));
};
const handleApiBaseChange = (event) => {
const value = event.target.value;
setApiBase(value);
localStorage.setItem('apiBase', value);
};
const handleAddBackend = () => {
setIsDialogOpen(true);
};
const handleDialogClose = () => {
setIsDialogOpen(false);
setNewBackend({ label: '', value: '' });
};
const handleSaveBackend = () => {
if (!newBackend.label || !newBackend.value) {
setSnackbarMessage('后端名称和地址不能为空');
setSnackbarOpen(true);
return;
}
const updatedBackends = [...backends, newBackend];
setBackends(updatedBackends);
localStorage.setItem('customBackends', JSON.stringify(updatedBackends.filter(b => !PRESET_BACKENDS.includes(b))));
setIsDialogOpen(false);
setSnackbarMessage('自定义后端已保存');
setSnackbarOpen(true);
};
const handleSnackbarClose = () => {
setSnackbarOpen(false);
};
const handleDeleteBackend = (backendValue) => {
const updatedBackends = backends.filter(b => b.value !== backendValue);
setBackends(updatedBackends);
// 如果删除的是当前选中的后端,切换到第一个默认后端
if (apiBase === backendValue) {
setApiBase(PRESET_BACKENDS[0].value);
localStorage.setItem('apiBase', PRESET_BACKENDS[0].value);
}
// 只保存自定义后端
localStorage.setItem('customBackends',
JSON.stringify(updatedBackends.filter(b =>
!PRESET_BACKENDS.some(preset => preset.value === b.value)
))
);
setSnackbarMessage('后端已删除');
setSnackbarOpen(true);
};
const handleMenuClick = (event) => {
setMenuAnchorEl(event.currentTarget);
};
const handleMenuClose = () => {
setMenuAnchorEl(null);
};
const handleMenuSelect = (index) => {
setTabValue(index);
handleMenuClose();
};
// 加载表单配置
const [formConfigs, setFormConfigs] = React.useState(null);
React.useEffect(() => {
fetch('config/label.json')
.then(res => res.json())
.then(config => setFormConfigs(config.forms));
}, []);
return (
<ThemeProvider theme={theme}>
<Container maxWidth="md" style={{ marginTop: '1rem', marginBottom: '2rem' }}>
{/* 后端地址选择器 */}
<FormControl fullWidth style={{ marginBottom: '2rem' }}>
<InputLabel>选择后端地址</InputLabel>
<Select
value={apiBase}
onChange={handleApiBaseChange}
label="选择后端地址"
>
{/* 默认后端 */}
{PRESET_BACKENDS.map((backend) => (
<MenuItem key={backend.value} value={backend.value}>
{backend.label} - {backend.value}
</MenuItem>
))}
{/* 分割线 */}
{backends.some(b => !PRESET_BACKENDS.some(preset => preset.value === b.value)) && (
<MenuItem disabled>
<hr style={{ width: '100%', margin: '4px 0' }} />
</MenuItem>
)}
{/* 自定义后端 */}
{backends
.filter(b => !PRESET_BACKENDS.some(preset => preset.value === b.value))
.map((backend) => (
<MenuItem key={backend.value} value={backend.value}>
<Box display="flex" justifyContent="space-between" width="100%">
<span>{backend.label} - {backend.value}</span>
<Button
size="small"
color="error"
onClick={(e) => {
e.stopPropagation();
handleDeleteBackend(backend.value);
}}
>
删除
</Button>
</Box>
</MenuItem>
))}
</Select>
<Button
variant="outlined"
color="primary"
style={{ marginTop: '1rem' }}
onClick={handleAddBackend}
>
添加自定义后端
</Button>
</FormControl>
{/* 添加自定义后端对话框 */}
<Dialog open={isDialogOpen} onClose={handleDialogClose}>
<DialogTitle>添加自定义后端</DialogTitle>
<DialogContent>
<TextField
fullWidth
label="后端名称"
value={newBackend.label}
onChange={(e) => setNewBackend({ ...newBackend, label: e.target.value })}
style={{ marginBottom: '1rem' }}
/>
<TextField
fullWidth
label="后端地址"
value={newBackend.value}
onChange={(e) => setNewBackend({ ...newBackend, value: e.target.value })}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleDialogClose}>取消</Button>
<Button onClick={handleSaveBackend} color="primary">保存</Button>
</DialogActions>
</Dialog>
{/* Snackbar 提示 */}
<Snackbar
open={snackbarOpen}
autoHideDuration={3000}
onClose={handleSnackbarClose}
>
<Alert onClose={handleSnackbarClose} severity="success">
{snackbarMessage}
</Alert>
</Snackbar>
<Typography variant="h4" gutterBottom>
API调试工具
</Typography>
<Paper elevation={3} style={{ padding: '1rem', marginBottom: '1rem' }}>
<div className="tabs-container">
<Box className="scrollable-tabs">
<Tabs
value={tabValue}
onChange={(e, v) => setTabValue(v)}
indicatorColor="primary"
textColor="primary"
variant="scrollable"
scrollButtons="auto"
allowScrollButtonsMobile
>
{formConfigs && Object.values(formConfigs)
.sort((a, b) => a.tab.index - b.tab.index)
.map(config => (
<Tab
key={config.tab.index}
label={config.tab.label}
style={{ minWidth: 'max-content' }}
/>
))}
</Tabs>
</Box>
<IconButton
className="menu-button"
onClick={handleMenuClick}
size="small"
>
<span className="material-icons">menu</span>
</IconButton>
<Menu
anchorEl={menuAnchorEl}
open={Boolean(menuAnchorEl)}
onClose={handleMenuClose}
>
{formConfigs && Object.values(formConfigs)
.sort((a, b) => a.tab.index - b.tab.index)
.map(config => (
<MenuItem
key={config.tab.index}
onClick={() => handleMenuSelect(config.tab.index)}
selected={tabValue === config.tab.index}
>
{config.tab.label}
</MenuItem>
))}
</Menu>
</div>
{formConfigs && Object.entries(formConfigs).map(([key, config]) => (
tabValue === config.tab.index && (
<DynamicForm
key={key}
formConfig={config}
onSubmit={(data) => handleApiCall(
config.endpoint,
config.method,
config.method === 'POST' ? data : null
)}
/>
)
))}
</Paper>
{response && (
<Paper elevation={3} style={{ padding: '1rem' }}>
<Typography variant="h6" gutterBottom>响应结果</Typography>
<pre style={{
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
backgroundColor: '#f5f5f5',
padding: '1rem',
borderRadius: '4px'
}}>
{response}
</pre>
</Paper>
)}
</Container>
</ThemeProvider>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
</html>