Tsumugiboshi/index1.html
2025-01-28 13:31:02 +08:00

431 lines
20 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: 20px; font-family: Arial, sans-serif; }
pre { background: #f5f5f5; padding: 10px; border-radius: 4px; }
.api-description { margin-bottom: 16px; color: #666; }
.selected-backend { font-weight: bold; color: #1976d2; }
</style>
</head>
<body>
<div id="root"></div>
<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
} = 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" }
];
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,
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('');
// 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));
//记录日志
const log = {
timestamp: new Date().toLocaleString(),
apiName: data.apiName || endpoint,
status: data.status || 'Unknown',
response: data,
expanded: false
};
setLogs(prevLogs => [log, ...prevLogs]);
//处理userID
if (data.userId) {
setUserId(data.userId);
localStorage.setItem('userId', data.userId);
}
//显示成功toast
if (data.status === "200 OK") {
const date = new Date(data.timestamp * 1000).toLocaleString();
setSnackbarMessage(`成功: ${data.apiName} (${date})`);
setSnackbarOpen(true);
}
} 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 toggleLogExpanded = (index) => {
setLogs(prevLogs => {
const newLogs = [...prevLogs];
newLogs[index].expanded = !newLogs[index].expanded;
return newLogs;
});
};
0
return (
<ThemeProvider theme={theme}>
<Container maxWidth="md" style={{ marginTop: '2rem', marginBottom: '2rem' }}>
{/* 后端地址选择器 */}
<FormControl fullWidth style={{ marginBottom: '2rem' }}>
<InputLabel>选择后端地址</InputLabel>
<Select
value={apiBase}
onChange={handleApiBaseChange}
label="选择后端地址"
>
{backends.map((backend) => (
<MenuItem key={backend.value} value={backend.value}>
{backend.label} {apiBase === backend.value && '◉'}
</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' }}>
<Tabs
value={tabValue}
onChange={(e, v) => setTabValue(v)}
indicatorColor="primary"
textColor="primary"
>
<Tab label="二维码转UID" />
<Tab label="6倍票券" />
<Tab label="地图库存" />
<Tab label="解锁DX" />
<Tab label="音乐数据" />
</Tabs>
{/* 二维码转UID表单 */}
{tabValue === 0 && (
<div style={{ padding: '1rem' }}>
<Typography variant="body1" className="api-description">
通过上传用户登录二维码解码后的字符串来获取 userID
</Typography>
<TextField
fullWidth
label="二维码内容"
value={qrInput}
onChange={(e) => setQrInput(e.target.value)}
style={{ marginBottom: '1rem' }}
/>
<Button
variant="contained"
color="primary"
onClick={() => handleApiCall(`/qr?qrcode=${encodeURIComponent(qrInput)}`)}
>
获取UserID
</Button>
</div>
)}
{/* 通用GET请求表单 */}
{[1, 2, 3].includes(tabValue) && (
<div style={{ padding: '1rem' }}>
<Typography variant="body1" className="api-description">
{tabValue === 1 && "使用用户的 userID 发送「6倍チケット」到账户。"}
{tabValue === 2 && "使用用户的 userID 保存 99 公里地图库存到账户。"}
{tabValue === 3 && "使用用户的 userID 解锁所有 DX Master 谱面。"}
</Typography>
<TextField
label="UserID"
value={userId}
onChange={(e) => setUserId(e.target.value)}
style={{ marginBottom: '1rem' }}
fullWidth
/>
<Button
variant="contained"
color="primary"
onClick={() => {
const endpoints = ['/ticket', '/mapstock', '/unlock'];
handleApiCall(`${endpoints[tabValue-1]}?userid=${userId}`);
}}
>
发送请求
</Button>
</div>
)}
{/* 音乐数据POST表单 */}
{tabValue === 4 && (
<div style={{ padding: '1rem' }}>
<Typography variant="body1" className="api-description">
覆写账户中的音乐数据
</Typography>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
fullWidth
label="UserID"
type="number"
value={musicData.userId}
onChange={(e) => setMusicData({...musicData, userId: e.target.value})}
/>
</Grid>
<Grid item xs={12}>
<TextField
fullWidth
label="achievement"
type="number"
value={musicData.music.achievement}
onChange={(e) => setMusicData({...musicData, music: {...musicData.music, achievement: e.target.value}})}
placeholder="1010000"
InputProps={{
style: { color: 'grey' }
}}
/>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel id="level-select-label">level</InputLabel>
<Select
labelId="level-select-label"
id="level-select"
value={musicData.music.level}
onChange={(e) => setMusicData({...musicData, music: {...musicData.music, level: e.target.value}})}
>
<MenuItem value={0}>0 (BAS)</MenuItem>
<MenuItem value={1}>1 (ADV)</MenuItem>
<MenuItem value={2}>2 (EXP)</MenuItem>
<MenuItem value={3}>3 (MAS)</MenuItem>
<MenuItem value={4}>4 (ReM)</MenuItem>
</Select>
</FormControl>
</Grid>
{Object.entries(musicData.music).map(([key, val]) => (
key !== 'level' && key !== 'achievement' && (
<Grid item xs={6} sm={4} key={key}>
<TextField
fullWidth
label={key}
type="number"
name={key}
value={val}
onChange={handleMusicChange}
/>
</Grid>
)
))}
<Grid item xs={12}>
<Button
variant="contained"
color="primary"
onClick={() => handleApiCall('/music', 'POST', musicData)}
>
提交音乐数据
</Button>
</Grid>
</Grid>
</div>
)}
</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>