mirror of
https://github.com/Zhuym07/Tsumugiboshi.git
synced 2025-05-20 01:17:27 +08:00
first update
This commit is contained in:
parent
0d8a82fc33
commit
dd468817b4
BIN
._index.html
Normal file
BIN
._index.html
Normal file
Binary file not shown.
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# 忽略UserDataDir文件夹
|
||||||
|
UserDataDir
|
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Chrome",
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"file": "${workspaceFolder}/index.html",
|
||||||
|
"runtimeArgs": [
|
||||||
|
"--disable-web-security",
|
||||||
|
"--user-data-dir=${workspaceFolder}/UserDataDir"
|
||||||
|
],
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
103
config.json
103
config.json
@ -1,103 +0,0 @@
|
|||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
161
config/label.json
Normal file
161
config/label.json
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
{
|
||||||
|
"forms": {
|
||||||
|
"login": {
|
||||||
|
"tab": {
|
||||||
|
"index": 0,
|
||||||
|
"label": "🪪登录",
|
||||||
|
"description": "上传登录二维码解码后的字符串来获取 userID"
|
||||||
|
},
|
||||||
|
"endpoint": "/qr",
|
||||||
|
"method": "GET",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"id": "qrcode",
|
||||||
|
"label": "二维码内容",
|
||||||
|
"type": "text",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"confirmBeforeSubmit": false
|
||||||
|
},
|
||||||
|
"ticket": {
|
||||||
|
"tab": {
|
||||||
|
"index": 1,
|
||||||
|
"label": "🎟️发6倍票",
|
||||||
|
"description": "发送「6倍功能票」到账户"
|
||||||
|
},
|
||||||
|
"endpoint": "/ticket",
|
||||||
|
"method": "GET",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"id": "userid",
|
||||||
|
"label": "🪪UserID",
|
||||||
|
"type": "text",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"confirmBeforeSubmit": true
|
||||||
|
},
|
||||||
|
"mapstock": {
|
||||||
|
"tab": {
|
||||||
|
"index": 2,
|
||||||
|
"label": "🗺️存入Stock",
|
||||||
|
"description": "保存 99km Stocks 到账户"
|
||||||
|
},
|
||||||
|
"endpoint": "/mapstock",
|
||||||
|
"method": "GET",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"id": "userid",
|
||||||
|
"label": "🪪UserID",
|
||||||
|
"type": "text",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"confirmBeforeSubmit": true
|
||||||
|
},
|
||||||
|
"unlock": {
|
||||||
|
"tab": {
|
||||||
|
"index": 3,
|
||||||
|
"label": "🔓解锁紫铺",
|
||||||
|
"description": "解锁所有 DX Master 谱面"
|
||||||
|
},
|
||||||
|
"endpoint": "/unlock",
|
||||||
|
"method": "GET",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"id": "userid",
|
||||||
|
"label": "🪪UserID",
|
||||||
|
"type": "text",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"confirmBeforeSubmit": true
|
||||||
|
},
|
||||||
|
"music": {
|
||||||
|
"tab": {
|
||||||
|
"index": 4,
|
||||||
|
"label": "✏️修改成绩",
|
||||||
|
"description": "⚠警告\n这将[覆写]账户中的成绩数据"
|
||||||
|
},
|
||||||
|
"endpoint": "/music",
|
||||||
|
"method": "POST",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"id": "userId",
|
||||||
|
"label": "🪪UserID",
|
||||||
|
"type": "number",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "music.achievement",
|
||||||
|
"label": "💯分数",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "1010000",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "music.level",
|
||||||
|
"label": "🧩难度",
|
||||||
|
"type": "select",
|
||||||
|
"options": [
|
||||||
|
{"value": 0, "label": "0 (🟩Bas)"},
|
||||||
|
{"value": 1, "label": "1 (🟨Adv)"},
|
||||||
|
{"value": 2, "label": "2 (🟥Exp)"},
|
||||||
|
{"value": 3, "label": "3 (🟪Mas)"},
|
||||||
|
{"value": 4, "label": "4 (⬜ReM)"}
|
||||||
|
],
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "music.comboStatus",
|
||||||
|
"label": "Full Combo 状态",
|
||||||
|
"type": "select",
|
||||||
|
"options": [
|
||||||
|
{"value": 0, "label": "0 (⚪未FC)"},
|
||||||
|
{"value": 1, "label": "1 (🟢已FC)"}
|
||||||
|
],
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "music.syncStatus",
|
||||||
|
"label": "Full Sync 状态",
|
||||||
|
"type": "select",
|
||||||
|
"options": [
|
||||||
|
{"value": 0, "label": "0 (⚪未Sync)"},
|
||||||
|
{"value": 1, "label": "1 (🔵已Sync)"}
|
||||||
|
],
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "music.musicId",
|
||||||
|
"label": "🎵musicID",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "11529",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "music.playCount",
|
||||||
|
"label": "🎰游玩次数",
|
||||||
|
"type": "number",
|
||||||
|
"placeholder": "1",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "music.deluxscoreMax",
|
||||||
|
"label": "🔢deluxe分数",
|
||||||
|
"type": "number",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "music.scoreRank",
|
||||||
|
"label": "scoreRank",
|
||||||
|
"type": "number",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"confirmBeforeSubmit": true,
|
||||||
|
"confirmTemplate": "即将发送的数据:\nUserID: {userId}\n歌曲ID: {music.musicId}\nachievement: {music.achievement}\n难度: {music.level}\nFC状态: {music.comboStatus}\nsyncStatus: {music.syncStatus}\nFS状态:{music.syncStatus}\n\n原始请求:\n{rawData}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
465
index.html
465
index.html
@ -16,14 +16,81 @@
|
|||||||
<script src="https://unpkg.com/@mui/material@5.14.0/umd/material-ui.production.min.js"></script>
|
<script src="https://unpkg.com/@mui/material@5.14.0/umd/material-ui.production.min.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body { margin: 20px; font-family: Arial, sans-serif; }
|
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; }
|
pre { background: #f5f5f5; padding: 10px; border-radius: 4px; }
|
||||||
.api-description { margin-bottom: 16px; color: #666; }
|
.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; }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body></body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<footer>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
© 2023 Tsumugiboshi. All rights reserved.
|
||||||
|
<br />
|
||||||
|
</Typography>
|
||||||
|
</footer>
|
||||||
|
|
||||||
<script type="text/babel">
|
<script type="text/babel">
|
||||||
// 从全局对象获取Material UI组件
|
// 从全局对象获取Material UI组件
|
||||||
@ -48,7 +115,10 @@
|
|||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
Alert
|
Alert,
|
||||||
|
Menu,
|
||||||
|
IconButton,
|
||||||
|
Link
|
||||||
} = MaterialUI;
|
} = MaterialUI;
|
||||||
|
|
||||||
// 创建主题
|
// 创建主题
|
||||||
@ -61,6 +131,87 @@
|
|||||||
{ label: "生产环境", value: "http://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() {
|
function App() {
|
||||||
const [tabValue, setTabValue] = React.useState(0);
|
const [tabValue, setTabValue] = React.useState(0);
|
||||||
const [qrInput, setQrInput] = React.useState('');
|
const [qrInput, setQrInput] = React.useState('');
|
||||||
@ -90,6 +241,7 @@
|
|||||||
const [newBackend, setNewBackend] = React.useState({ label: '', value: '' });
|
const [newBackend, setNewBackend] = React.useState({ label: '', value: '' });
|
||||||
const [snackbarOpen, setSnackbarOpen] = React.useState(false);
|
const [snackbarOpen, setSnackbarOpen] = React.useState(false);
|
||||||
const [snackbarMessage, setSnackbarMessage] = React.useState('');
|
const [snackbarMessage, setSnackbarMessage] = React.useState('');
|
||||||
|
const [menuAnchorEl, setMenuAnchorEl] = React.useState(null);
|
||||||
|
|
||||||
// Cookie操作函数
|
// Cookie操作函数
|
||||||
function setCookie(name, value, days) {
|
function setCookie(name, value, days) {
|
||||||
@ -169,9 +321,52 @@
|
|||||||
setSnackbarOpen(false);
|
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 (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<Container maxWidth="md" style={{ marginTop: '2rem', marginBottom: '2rem' }}>
|
<Container maxWidth="md" style={{ marginTop: '1rem', marginBottom: '2rem' }}>
|
||||||
{/* 后端地址选择器 */}
|
{/* 后端地址选择器 */}
|
||||||
<FormControl fullWidth style={{ marginBottom: '2rem' }}>
|
<FormControl fullWidth style={{ marginBottom: '2rem' }}>
|
||||||
<InputLabel>选择后端地址</InputLabel>
|
<InputLabel>选择后端地址</InputLabel>
|
||||||
@ -180,11 +375,40 @@
|
|||||||
onChange={handleApiBaseChange}
|
onChange={handleApiBaseChange}
|
||||||
label="选择后端地址"
|
label="选择后端地址"
|
||||||
>
|
>
|
||||||
{backends.map((backend) => (
|
{/* 默认后端 */}
|
||||||
|
{PRESET_BACKENDS.map((backend) => (
|
||||||
<MenuItem key={backend.value} value={backend.value}>
|
<MenuItem key={backend.value} value={backend.value}>
|
||||||
{backend.label} - {backend.value} {apiBase === backend.value}
|
{backend.label} - {backend.value}
|
||||||
</MenuItem>
|
</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>
|
</Select>
|
||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
@ -235,178 +459,67 @@
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Paper elevation={3} style={{ padding: '1rem', marginBottom: '1rem' }}>
|
<Paper elevation={3} style={{ padding: '1rem', marginBottom: '1rem' }}>
|
||||||
<Tabs
|
<div className="tabs-container">
|
||||||
value={tabValue}
|
<Box className="scrollable-tabs">
|
||||||
onChange={(e, v) => setTabValue(v)}
|
<Tabs
|
||||||
indicatorColor="primary"
|
value={tabValue}
|
||||||
textColor="primary"
|
onChange={(e, v) => setTabValue(v)}
|
||||||
>
|
indicatorColor="primary"
|
||||||
<Tab label="🪪登录" />
|
textColor="primary"
|
||||||
<Tab label="🎟️发6倍票" />
|
variant="scrollable"
|
||||||
<Tab label="🗺️存入Stock" />
|
scrollButtons="auto"
|
||||||
<Tab label="🔓解锁紫铺" />
|
allowScrollButtonsMobile
|
||||||
<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
|
{formConfigs && Object.values(formConfigs)
|
||||||
</Button>
|
.sort((a, b) => a.tab.index - b.tab.index)
|
||||||
</div>
|
.map(config => (
|
||||||
)}
|
<Tab
|
||||||
|
key={config.tab.index}
|
||||||
{/* 通用GET请求表单 */}
|
label={config.tab.label}
|
||||||
{[1, 2, 3].includes(tabValue) && (
|
style={{ minWidth: 'max-content' }}
|
||||||
<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>
|
))}
|
||||||
)
|
</Tabs>
|
||||||
))}
|
</Box>
|
||||||
<Grid item xs={12}>
|
<IconButton
|
||||||
<Button
|
className="menu-button"
|
||||||
variant="contained"
|
onClick={handleMenuClick}
|
||||||
color="primary"
|
size="small"
|
||||||
onClick={async () => {
|
>
|
||||||
const dataToSend = JSON.stringify(musicData, null, 2);
|
<span className="material-icons">menu</span>
|
||||||
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}`;
|
</IconButton>
|
||||||
alert(confirmationMessage); // 显示即将发送的数据
|
<Menu
|
||||||
const confirmation = window.confirm("确认提交数据?"); // 弹出确认对话框
|
anchorEl={menuAnchorEl}
|
||||||
if (confirmation) {
|
open={Boolean(menuAnchorEl)}
|
||||||
handleApiCall('/music', 'POST', musicData); // 如果用户确认,发送请求
|
onClose={handleMenuClose}
|
||||||
}
|
>
|
||||||
}}
|
{formConfigs && Object.values(formConfigs)
|
||||||
>
|
.sort((a, b) => a.tab.index - b.tab.index)
|
||||||
提交音乐数据
|
.map(config => (
|
||||||
</Button>
|
<MenuItem
|
||||||
</Grid>
|
key={config.tab.index}
|
||||||
</Grid>
|
onClick={() => handleMenuSelect(config.tab.index)}
|
||||||
|
selected={tabValue === config.tab.index}
|
||||||
|
>
|
||||||
|
{config.tab.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
</div>
|
</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>
|
</Paper>
|
||||||
|
|
||||||
{response && (
|
{response && (
|
||||||
|
431
index1.html
431
index1.html
@ -1,431 +0,0 @@
|
|||||||
<!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>
|
|
228
src/components/ApiTabs.js
Normal file
228
src/components/ApiTabs.js
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
Tab,
|
||||||
|
TextField,
|
||||||
|
Button,
|
||||||
|
Paper,
|
||||||
|
Typography,
|
||||||
|
Grid,
|
||||||
|
Box,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
InputLabel,
|
||||||
|
FormControl,
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Snackbar,
|
||||||
|
Alert,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
FormControlLabel
|
||||||
|
} from '@mui/material';
|
||||||
|
import { setCookie } from '../utils/cookies';
|
||||||
|
|
||||||
|
function ApiTabs({
|
||||||
|
tabValue,
|
||||||
|
setTabValue,
|
||||||
|
qrInput,
|
||||||
|
setQrInput,
|
||||||
|
userId,
|
||||||
|
setUserId,
|
||||||
|
response,
|
||||||
|
setResponse,
|
||||||
|
musicData,
|
||||||
|
setMusicData,
|
||||||
|
apiBase,
|
||||||
|
setApiBase,
|
||||||
|
backends,
|
||||||
|
setBackends,
|
||||||
|
isDialogOpen,
|
||||||
|
setIsDialogOpen,
|
||||||
|
newBackend,
|
||||||
|
setNewBackend,
|
||||||
|
snackbarOpen,
|
||||||
|
setSnackbarOpen,
|
||||||
|
snackbarMessage,
|
||||||
|
setSnackbarMessage
|
||||||
|
}) {
|
||||||
|
const handleTabChange = (event, newValue) => {
|
||||||
|
setTabValue(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUserIdChange = (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setUserId(value);
|
||||||
|
setCookie('userId', value);
|
||||||
|
setMusicData(prev => ({ ...prev, userId: parseInt(value) || 0 }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackendChange = (event) => {
|
||||||
|
const value = event.target.value;
|
||||||
|
setApiBase(value);
|
||||||
|
localStorage.setItem('apiBase', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddBackend = () => {
|
||||||
|
if (!newBackend.label || !newBackend.value) {
|
||||||
|
setSnackbarMessage('请填写完整的后端信息');
|
||||||
|
setSnackbarOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedBackends = [...backends, newBackend];
|
||||||
|
setBackends(updatedBackends);
|
||||||
|
localStorage.setItem('customBackends', JSON.stringify(
|
||||||
|
updatedBackends.slice(PRESET_BACKENDS.length)
|
||||||
|
));
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
setNewBackend({ label: '', value: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Paper elevation={3} sx={{ p: 3, mb: 4 }}>
|
||||||
|
<Typography variant="h4" gutterBottom>
|
||||||
|
API调试工具
|
||||||
|
</Typography>
|
||||||
|
<div className="tabs-container">
|
||||||
|
<div className="scrollable-tabs">
|
||||||
|
<Tabs
|
||||||
|
value={tabValue}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
variant="scrollable"
|
||||||
|
scrollButtons="auto"
|
||||||
|
>
|
||||||
|
<Tab label="二维码解析" />
|
||||||
|
<Tab label="音乐数据" />
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Box sx={{ mt: 3 }}>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label="用户ID"
|
||||||
|
value={userId}
|
||||||
|
onChange={handleUserIdChange}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} md={6}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel>后端环境</InputLabel>
|
||||||
|
<Select
|
||||||
|
value={apiBase}
|
||||||
|
onChange={handleBackendChange}
|
||||||
|
label="后端环境"
|
||||||
|
>
|
||||||
|
{backends.map((backend, index) => (
|
||||||
|
<MenuItem key={index} value={backend.value}>
|
||||||
|
{backend.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
{tabValue === 0 && (
|
||||||
|
<Box sx={{ mt: 3 }}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
multiline
|
||||||
|
rows={4}
|
||||||
|
label="二维码内容"
|
||||||
|
value={qrInput}
|
||||||
|
onChange={(e) => setQrInput(e.target.value)}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tabValue === 1 && (
|
||||||
|
<Box sx={{ mt: 3 }}>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{Object.entries(musicData.music).map(([key, value]) => (
|
||||||
|
<Grid item xs={12} sm={6} md={4} key={key}>
|
||||||
|
<TextField
|
||||||
|
fullWidth
|
||||||
|
label={key}
|
||||||
|
type="number"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newValue = parseInt(e.target.value) || 0;
|
||||||
|
setMusicData(prev => ({
|
||||||
|
...prev,
|
||||||
|
music: {
|
||||||
|
...prev.music,
|
||||||
|
[key]: newValue
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{response && (
|
||||||
|
<Box sx={{ mt: 3 }}>
|
||||||
|
<Typography variant="h6" gutterBottom>
|
||||||
|
响应结果
|
||||||
|
</Typography>
|
||||||
|
<pre>{JSON.stringify(response, null, 2)}</pre>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Dialog open={isDialogOpen} onClose={() => setIsDialogOpen(false)}>
|
||||||
|
<DialogTitle>添加自定义后端</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
label="名称"
|
||||||
|
fullWidth
|
||||||
|
value={newBackend.label}
|
||||||
|
onChange={(e) => setNewBackend(prev => ({ ...prev, label: e.target.value }))}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
margin="dense"
|
||||||
|
label="地址"
|
||||||
|
fullWidth
|
||||||
|
value={newBackend.value}
|
||||||
|
onChange={(e) => setNewBackend(prev => ({ ...prev, value: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setIsDialogOpen(false)}>取消</Button>
|
||||||
|
<Button onClick={handleAddBackend}>添加</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Snackbar
|
||||||
|
open={snackbarOpen}
|
||||||
|
autoHideDuration={6000}
|
||||||
|
onClose={() => setSnackbarOpen(false)}
|
||||||
|
>
|
||||||
|
<Alert
|
||||||
|
onClose={() => setSnackbarOpen(false)}
|
||||||
|
severity="error"
|
||||||
|
sx={{ width: '100%' }}
|
||||||
|
>
|
||||||
|
{snackbarMessage}
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApiTabs;
|
79
src/components/App.js
Normal file
79
src/components/App.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Container, Typography } from '@mui/material';
|
||||||
|
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
||||||
|
import ApiTabs from './ApiTabs';
|
||||||
|
import { PRESET_BACKENDS } from '../constants';
|
||||||
|
import { getCookie, setCookie } from '../utils/cookies';
|
||||||
|
|
||||||
|
const theme = createTheme();
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [tabValue, setTabValue] = useState(0);
|
||||||
|
const [qrInput, setQrInput] = useState('');
|
||||||
|
const [userId, setUserId] = useState(getCookie('userId') || '');
|
||||||
|
const [response, setResponse] = useState('');
|
||||||
|
const [musicData, setMusicData] = useState({
|
||||||
|
userId: parseInt(userId) || 0,
|
||||||
|
music: {
|
||||||
|
musicId: 0,
|
||||||
|
level: 0,
|
||||||
|
achievement: 0,
|
||||||
|
playCount: 0,
|
||||||
|
comboStatus: 0,
|
||||||
|
syncStatus: 0,
|
||||||
|
deluxscoreMax: 0,
|
||||||
|
scoreRank: 0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const [apiBase, setApiBase] = useState(
|
||||||
|
localStorage.getItem('apiBase') || PRESET_BACKENDS[0].value
|
||||||
|
);
|
||||||
|
const [backends, setBackends] = useState([
|
||||||
|
...PRESET_BACKENDS,
|
||||||
|
...JSON.parse(localStorage.getItem('customBackends') || '[]')
|
||||||
|
]);
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const [newBackend, setNewBackend] = useState({ label: '', value: '' });
|
||||||
|
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||||
|
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={theme}>
|
||||||
|
<div id="root">
|
||||||
|
<Container maxWidth="md" sx={{ mt: 4, mb: 4 }}>
|
||||||
|
<ApiTabs
|
||||||
|
tabValue={tabValue}
|
||||||
|
setTabValue={setTabValue}
|
||||||
|
qrInput={qrInput}
|
||||||
|
setQrInput={setQrInput}
|
||||||
|
userId={userId}
|
||||||
|
setUserId={setUserId}
|
||||||
|
response={response}
|
||||||
|
setResponse={setResponse}
|
||||||
|
musicData={musicData}
|
||||||
|
setMusicData={setMusicData}
|
||||||
|
apiBase={apiBase}
|
||||||
|
setApiBase={setApiBase}
|
||||||
|
backends={backends}
|
||||||
|
setBackends={setBackends}
|
||||||
|
isDialogOpen={isDialogOpen}
|
||||||
|
setIsDialogOpen={setIsDialogOpen}
|
||||||
|
newBackend={newBackend}
|
||||||
|
setNewBackend={setNewBackend}
|
||||||
|
snackbarOpen={snackbarOpen}
|
||||||
|
setSnackbarOpen={setSnackbarOpen}
|
||||||
|
snackbarMessage={snackbarMessage}
|
||||||
|
setSnackbarMessage={setSnackbarMessage}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<Typography variant="body2" color="textSecondary">
|
||||||
|
© 2023 Tsumugiboshi. All rights reserved.
|
||||||
|
</Typography>
|
||||||
|
</footer>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
5
src/constants/index.js
Normal file
5
src/constants/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const PRESET_BACKENDS = [
|
||||||
|
{ label: "开发环境", value: "http://dev-api.example.com" },
|
||||||
|
{ label: "测试环境", value: "http://test-api.example.com" },
|
||||||
|
{ label: "生产环境", value: "http://api.example.com" }
|
||||||
|
];
|
85
src/styles/main.css
Normal file
85
src/styles/main.css
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
21
src/utils/cookies.js
Normal file
21
src/utils/cookies.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
export function setCookie(name, value, days = 30) {
|
||||||
|
const d = new Date();
|
||||||
|
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||||
|
const expires = `expires=${d.toUTCString()}`;
|
||||||
|
document.cookie = `${name}=${value};${expires};path=/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCookie(name) {
|
||||||
|
const nameEQ = `${name}=`;
|
||||||
|
const ca = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < ca.length; i++) {
|
||||||
|
let c = ca[i];
|
||||||
|
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
|
||||||
|
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCookie(name) {
|
||||||
|
document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/`;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user