mirror of
https://github.com/Zhuym07/Tsumugiboshi.git
synced 2025-05-20 02:17:27 +08:00
1
This commit is contained in:
commit
0d8a82fc33
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"CodeGPT.apiKey": "DeepSeek"
|
||||
}
|
103
config.json
Normal file
103
config.json
Normal file
@ -0,0 +1,103 @@
|
||||
{
|
||||
"title": "API调试工具",
|
||||
"theme": "light",
|
||||
"apis": [
|
||||
{
|
||||
"name": "二维码扫描",
|
||||
"type": "qr",
|
||||
"endpoint": "/qr",
|
||||
"method": "GET",
|
||||
"params": [
|
||||
{
|
||||
"key": "qrcode",
|
||||
"label": "二维码内容",
|
||||
"type": "qr",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responseFields": ["status", "timestamp", "info", "apiName", "date", "userId"]
|
||||
},
|
||||
{
|
||||
"name": "发送6倍券",
|
||||
"type": "action",
|
||||
"endpoint": "/ticket",
|
||||
"method": "GET",
|
||||
"params": [
|
||||
{
|
||||
"key": "userid",
|
||||
"label": "用户ID",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"source": "userId"
|
||||
}
|
||||
],
|
||||
"responseFields": ["status", "timestamp", "info", "apiName", "date", "userId"]
|
||||
},
|
||||
{
|
||||
"name": "保存99公里",
|
||||
"type": "action",
|
||||
"endpoint": "/mapstock",
|
||||
"method": "GET",
|
||||
"params": [
|
||||
{
|
||||
"key": "userid",
|
||||
"label": "用户ID",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"source": "userId"
|
||||
}
|
||||
],
|
||||
"responseFields": ["status", "timestamp", "info", "apiName", "date", "userId"]
|
||||
},
|
||||
{
|
||||
"name": "解锁DX曲目",
|
||||
"type": "action",
|
||||
"endpoint": "/unlock",
|
||||
"method": "GET",
|
||||
"params": [
|
||||
{
|
||||
"key": "userid",
|
||||
"label": "用户ID",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"source": "userId"
|
||||
}
|
||||
],
|
||||
"responseFields": ["status", "timestamp", "info", "apiName", "date", "userId"]
|
||||
},
|
||||
{
|
||||
"name": "更新音乐数据",
|
||||
"type": "form",
|
||||
"endpoint": "/music",
|
||||
"method": "POST",
|
||||
"params": [
|
||||
{
|
||||
"key": "userId",
|
||||
"label": "用户ID",
|
||||
"type": "text",
|
||||
"required": true,
|
||||
"source": "userId"
|
||||
},
|
||||
{
|
||||
"key": "musicId",
|
||||
"label": "音乐ID",
|
||||
"type": "number",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"key": "level",
|
||||
"label": "难度",
|
||||
"type": "number",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"key": "playCount",
|
||||
"label": "游玩次数",
|
||||
"type": "number",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responseFields": ["status", "timestamp", "info", "apiName", "date", "userId"]
|
||||
}
|
||||
]
|
||||
}
|
434
index.html
Normal file
434
index.html
Normal file
@ -0,0 +1,434 @@
|
||||
<!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>
|
431
index1.html
Normal file
431
index1.html
Normal file
@ -0,0 +1,431 @@
|
||||
<!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>
|
Loading…
x
Reference in New Issue
Block a user