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

434 lines
22 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
//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('');
// 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);
};
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} - {backend.value} {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="🪪登录" />
<Tab label="🎟发6倍票" />
<Tab label="🗺存入Stock" />
<Tab label="🔓解锁紫铺" />
<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 && "发送「6倍功能票」到账户。"}
{tabValue === 2 && "保存 99 公里 Stock 到账户。"}
{tabValue === 3 && "解锁所有 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="💯分数"
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">🧩难度</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>
<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel id="comboStatus-select-label">comboStatus</InputLabel>
<Select
labelId="comboStatus-select-label"
id="comboStatus-select"
value={musicData.music.comboStatus}
onChange={(e) => setMusicData({...musicData, music: {...musicData.music, comboStatus: e.target.value}})}
>
<MenuItem value={0}>0 (未达成)</MenuItem>
<MenuItem value={1}>1 (🟢已达成)</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<FormControl fullWidth>
<InputLabel id="syncStatus-select-label">syncStatus</InputLabel>
<Select
labelId="syncStatus-select-label"
id="syncStatus-select"
value={musicData.music.syncStatus}
onChange={(e) => setMusicData({...musicData, music: {...musicData.music, syncStatus: e.target.value}})}
>
<MenuItem value={0}>0 (未同步)</MenuItem>
<MenuItem value={1}>1 (🔵已同步)</MenuItem>
</Select>
</FormControl>
</Grid>
{Object.entries(musicData.music).map(([key, val]) => (
key !== 'level' && key !== 'achievement' && key !== 'comboStatus' && key !== 'syncStatus' && (
<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={async () => {
const dataToSend = JSON.stringify(musicData, null, 2);
const confirmationMessage = `即将发送的数据:\nUserID: ${musicData.userId}\n歌曲ID: ${musicData.music.musicId}\nachievement: ${musicData.music.achievement}\nlevel: ${musicData.music.level}\ncomboStatus: ${musicData.music.comboStatus}\nsyncStatus: ${musicData.music.syncStatus}\n\n原始请求:\n${dataToSend}`;
alert(confirmationMessage); // 显示即将发送的数据
const confirmation = window.confirm("确认提交数据?"); // 弹出确认对话框
if (confirmation) {
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>