From dd468817b49b8da2330e74c6596bf677fb8a3df9 Mon Sep 17 00:00:00 2001
From: Zhuym <zhuym7@gmail.com>
Date: Tue, 28 Jan 2025 15:22:18 +0800
Subject: [PATCH] first update

---
 ._index.html              | Bin 0 -> 4096 bytes
 ._src                     | Bin 0 -> 4096 bytes
 .gitignore                |   2 +
 .vscode/launch.json       |  16 ++
 config.json               | 103 ---------
 config/label.json         | 161 +++++++++++++
 index.html                | 465 +++++++++++++++++++++++---------------
 index1.html               | 431 -----------------------------------
 src/components/ApiTabs.js | 228 +++++++++++++++++++
 src/components/App.js     |  79 +++++++
 src/constants/index.js    |   5 +
 src/styles/main.css       |  85 +++++++
 src/utils/cookies.js      |  21 ++
 13 files changed, 886 insertions(+), 710 deletions(-)
 create mode 100644 ._index.html
 create mode 100644 ._src
 create mode 100644 .gitignore
 create mode 100644 .vscode/launch.json
 delete mode 100644 config.json
 create mode 100644 config/label.json
 delete mode 100644 index1.html
 create mode 100644 src/components/ApiTabs.js
 create mode 100644 src/components/App.js
 create mode 100644 src/constants/index.js
 create mode 100644 src/styles/main.css
 create mode 100644 src/utils/cookies.js

diff --git a/._index.html b/._index.html
new file mode 100644
index 0000000000000000000000000000000000000000..2043f67f7c01f6e223fe46a3b18d91a044ff11ca
GIT binary patch
literal 4096
zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103vf+zv$
zV3+~K+-O=D5#plB`MG+D1qC^&dId%KWvO|IdC92^j7$s_?Xk1?_j8A#X&vQ`hQMeD
zjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kgR_7DJdHbEE+<U%qsixrAei}Op1l2aAZ
x@{6(+GV@AO^GY)F^AdA%Div~4(@GSQauV~hfqh}9t|3jK`XBBU83wuk{{a}DCvX4&

literal 0
HcmV?d00001

diff --git a/._src b/._src
new file mode 100644
index 0000000000000000000000000000000000000000..2043f67f7c01f6e223fe46a3b18d91a044ff11ca
GIT binary patch
literal 4096
zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103vf+zv$
zV3+~K+-O=D5#plB`MG+D1qC^&dId%KWvO|IdC92^j7$s_?Xk1?_j8A#X&vQ`hQMeD
zjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kgR_7DJdHbEE+<U%qsixrAei}Op1l2aAZ
x@{6(+GV@AO^GY)F^AdA%Div~4(@GSQauV~hfqh}9t|3jK`XBBU83wuk{{a}DCvX4&

literal 0
HcmV?d00001

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..026930b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+# 忽略UserDataDir文件夹
+UserDataDir
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..e8549e7
--- /dev/null
+++ b/.vscode/launch.json
@@ -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}"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/config.json b/config.json
deleted file mode 100644
index bd7dfc0..0000000
--- a/config.json
+++ /dev/null
@@ -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"]
-      }
-    ]
-  }
\ No newline at end of file
diff --git a/config/label.json b/config/label.json
new file mode 100644
index 0000000..0bdcad9
--- /dev/null
+++ b/config/label.json
@@ -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}"
+    }
+  }
+}
diff --git a/index.html b/index.html
index 637e2eb..5ce5602 100644
--- a/index.html
+++ b/index.html
@@ -16,14 +16,81 @@
     <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; }
+        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-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; }
+        .scrollable-tabs {
+            max-width: calc(100% - 48px); /* 为菜单按钮留出空间 */
+            overflow-x: auto;
+            overflow-y: hidden;
+        }
+        .tabs-container {
+            display: flex;
+            align-items: center;
+            position: relative;
+            border-bottom: 1px solid rgba(0, 0, 0, 0.12);
+        }
+        .menu-button {
+            position: absolute !important;
+            right: 0;
+            top: 50%;
+            transform: translateY(-50%);
+        }
+        footer {
+            margin-top: auto;
+            padding: 16px;
+            background-color: #f5f5f5;
+            text-align: center;
+            font-size: 0.875rem;
+            color: #666;
+        }
+        @media (max-width: 600px) {
+            .MuiContainer-root {
+                padding: 12px !important;
+            }
+            .MuiPaper-root {
+                padding: 12px !important;
+            }
+            h4.MuiTypography-root {
+                font-size: 1.5rem;
+            }
+            .scrollable-tabs::-webkit-scrollbar {
+                display: none;
+            }
+            .api-description {
+                font-size: 0.875rem;
+                padding: 8px;
+            }
+        }
     </style>
 </head>
-<body>
+<body></body>
     <div id="root"></div>
+    <footer>
+        <Typography variant="body2" color="textSecondary">
+            © 2023 Tsumugiboshi. All rights reserved.
+            <br />
+        </Typography>
+    </footer>
 
     <script type="text/babel">
         // 从全局对象获取Material UI组件
@@ -48,7 +115,10 @@
             DialogContent,
             DialogActions,
             Snackbar,
-            Alert
+            Alert,
+            Menu,
+            IconButton,
+            Link
         } = MaterialUI;
 
         // 创建主题
@@ -61,6 +131,87 @@
             { label: "生产环境", value: "http://api.example.com" }
         ];
 
+        // 通用表单组件
+        const DynamicForm = ({ formConfig, onSubmit }) => {
+            const [formData, setFormData] = React.useState({});
+
+            const handleChange = (fieldId, value) => {
+                const keys = fieldId.split('.');
+                setFormData(prev => {
+                    let newData = {...prev};
+                    let current = newData;
+                    for (let i = 0; i < keys.length - 1; i++) {
+                        current[keys[i]] = current[keys[i]] || {};
+                        current = current[keys[i]];
+                    }
+                    current[keys[keys.length - 1]] = value;
+                    return newData;
+                });
+            };
+
+            const handleSubmit = async () => {
+                if (formConfig.confirmBeforeSubmit) {
+                    let confirmMessage = formConfig.confirmTemplate || "确认提交数据?";
+                    if (formConfig.confirmTemplate) {
+                        confirmMessage = confirmMessage.replace(/\{([^}]+)\}/g, (_, key) => {
+                            return key === 'rawData' ? JSON.stringify(formData, null, 2) : 
+                                   key.split('.').reduce((obj, k) => obj && obj[k], formData) || '';
+                        });
+                    }
+                    if (!confirm(confirmMessage)) return;
+                }
+                onSubmit(formData);
+            };
+
+            return (
+                <div style={{ padding: '1rem' }}>
+                    <Typography variant="body1" className="api-description">
+                        {formConfig.tab.description}
+                    </Typography>
+                    <Grid container spacing={2}>
+                        {formConfig.fields.map(field => (
+                            <Grid item xs={12} key={field.id}>
+                                {field.type === 'select' ? (
+                                    <FormControl fullWidth>
+                                        <InputLabel>{field.label}</InputLabel>
+                                        <Select
+                                            value={field.id.split('.').reduce((obj, key) => obj && obj[key], formData) ?? ''}
+                                            onChange={(e) => handleChange(field.id, e.target.value)}
+                                            label={field.label}
+                                        >
+                                            {field.options.map(opt => (
+                                                <MenuItem key={opt.value} value={opt.value}>
+                                                    {opt.label}
+                                                </MenuItem>
+                                            ))}
+                                        </Select>
+                                    </FormControl>
+                                ) : (
+                                    <TextField
+                                        fullWidth
+                                        label={field.label}
+                                        type={field.type}
+                                        value={field.id.split('.').reduce((obj, key) => obj && obj[key], formData) || ''}
+                                        onChange={(e) => handleChange(field.id, e.target.value)}
+                                        placeholder={field.placeholder}
+                                    />
+                                )}
+                            </Grid>
+                        ))}
+                        <Grid item xs={12}>
+                            <Button
+                                variant="contained"
+                                color="primary"
+                                onClick={handleSubmit}
+                            >
+                                提交
+                            </Button>
+                        </Grid>
+                    </Grid>
+                </div>
+            );
+        };
+
         function App() {
             const [tabValue, setTabValue] = React.useState(0);
             const [qrInput, setQrInput] = React.useState('');
@@ -90,6 +241,7 @@
             const [newBackend, setNewBackend] = React.useState({ label: '', value: '' });
             const [snackbarOpen, setSnackbarOpen] = React.useState(false);
             const [snackbarMessage, setSnackbarMessage] = React.useState('');
+            const [menuAnchorEl, setMenuAnchorEl] = React.useState(null);
 
             // Cookie操作函数
             function setCookie(name, value, days) {
@@ -169,9 +321,52 @@
                 setSnackbarOpen(false);
             };
 
+            const handleDeleteBackend = (backendValue) => {
+                const updatedBackends = backends.filter(b => b.value !== backendValue);
+                setBackends(updatedBackends);
+                
+                // 如果删除的是当前选中的后端,切换到第一个默认后端
+                if (apiBase === backendValue) {
+                    setApiBase(PRESET_BACKENDS[0].value);
+                    localStorage.setItem('apiBase', PRESET_BACKENDS[0].value);
+                }
+                
+                // 只保存自定义后端
+                localStorage.setItem('customBackends', 
+                    JSON.stringify(updatedBackends.filter(b => 
+                        !PRESET_BACKENDS.some(preset => preset.value === b.value)
+                    ))
+                );
+                
+                setSnackbarMessage('后端已删除');
+                setSnackbarOpen(true);
+            };
+
+            const handleMenuClick = (event) => {
+                setMenuAnchorEl(event.currentTarget);
+            };
+
+            const handleMenuClose = () => {
+                setMenuAnchorEl(null);
+            };
+
+            const handleMenuSelect = (index) => {
+                setTabValue(index);
+                handleMenuClose();
+            };
+
+            // 加载表单配置
+            const [formConfigs, setFormConfigs] = React.useState(null);
+
+            React.useEffect(() => {
+                fetch('config/label.json')
+                    .then(res => res.json())
+                    .then(config => setFormConfigs(config.forms));
+            }, []);
+
             return (
                 <ThemeProvider theme={theme}>
-                    <Container maxWidth="md" style={{ marginTop: '2rem', marginBottom: '2rem' }}>
+                    <Container maxWidth="md" style={{ marginTop: '1rem', marginBottom: '2rem' }}>
                         {/* 后端地址选择器 */}
                         <FormControl fullWidth style={{ marginBottom: '2rem' }}>
                             <InputLabel>选择后端地址</InputLabel>
@@ -180,11 +375,40 @@
                                 onChange={handleApiBaseChange}
                                 label="选择后端地址"
                             >
-                                {backends.map((backend) => (
+                                {/* 默认后端 */}
+                                {PRESET_BACKENDS.map((backend) => (
                                     <MenuItem key={backend.value} value={backend.value}>
-                                        {backend.label} - {backend.value} {apiBase === backend.value}
+                                        {backend.label} - {backend.value}
                                     </MenuItem>
                                 ))}
+                                
+                                {/* 分割线 */}
+                                {backends.some(b => !PRESET_BACKENDS.some(preset => preset.value === b.value)) && (
+                                    <MenuItem disabled>
+                                        <hr style={{ width: '100%', margin: '4px 0' }} />
+                                    </MenuItem>
+                                )}
+                                
+                                {/* 自定义后端 */}
+                                {backends
+                                    .filter(b => !PRESET_BACKENDS.some(preset => preset.value === b.value))
+                                    .map((backend) => (
+                                        <MenuItem key={backend.value} value={backend.value}>
+                                            <Box display="flex" justifyContent="space-between" width="100%">
+                                                <span>{backend.label} - {backend.value}</span>
+                                                <Button
+                                                    size="small"
+                                                    color="error"
+                                                    onClick={(e) => {
+                                                        e.stopPropagation();
+                                                        handleDeleteBackend(backend.value);
+                                                    }}
+                                                >
+                                                    删除
+                                                </Button>
+                                            </Box>
+                                        </MenuItem>
+                                    ))}
                             </Select>
                             <Button
                                 variant="outlined"
@@ -235,178 +459,67 @@
                         </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)}`)}
+                            <div className="tabs-container">
+                                <Box className="scrollable-tabs">
+                                    <Tabs 
+                                        value={tabValue} 
+                                        onChange={(e, v) => setTabValue(v)}
+                                        indicatorColor="primary"
+                                        textColor="primary"
+                                        variant="scrollable"
+                                        scrollButtons="auto"
+                                        allowScrollButtonsMobile
                                     >
-                                        获取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}
+                                        {formConfigs && Object.values(formConfigs)
+                                            .sort((a, b) => a.tab.index - b.tab.index)
+                                            .map(config => (
+                                                <Tab 
+                                                    key={config.tab.index} 
+                                                    label={config.tab.label}
+                                                    style={{ minWidth: 'max-content' }}
                                                 />
-                                            </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>
+                                            ))}
+                                    </Tabs>
+                                </Box>
+                                <IconButton 
+                                    className="menu-button"
+                                    onClick={handleMenuClick}
+                                    size="small"
+                                >
+                                    <span className="material-icons">menu</span>
+                                </IconButton>
+                                <Menu
+                                    anchorEl={menuAnchorEl}
+                                    open={Boolean(menuAnchorEl)}
+                                    onClose={handleMenuClose}
+                                >
+                                    {formConfigs && Object.values(formConfigs)
+                                        .sort((a, b) => a.tab.index - b.tab.index)
+                                        .map(config => (
+                                            <MenuItem 
+                                                key={config.tab.index}
+                                                onClick={() => handleMenuSelect(config.tab.index)}
+                                                selected={tabValue === config.tab.index}
+                                            >
+                                                {config.tab.label}
+                                            </MenuItem>
+                                        ))}
+                                </Menu>
                             </div>
-                        )}
+
+                            {formConfigs && Object.entries(formConfigs).map(([key, config]) => (
+                                tabValue === config.tab.index && (
+                                    <DynamicForm 
+                                        key={key}
+                                        formConfig={config}
+                                        onSubmit={(data) => handleApiCall(
+                                            config.endpoint,
+                                            config.method,
+                                            config.method === 'POST' ? data : null
+                                        )}
+                                    />
+                                )
+                            ))}
                         </Paper>
 
                         {response && (
diff --git a/index1.html b/index1.html
deleted file mode 100644
index 4e6dde1..0000000
--- a/index1.html
+++ /dev/null
@@ -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>
\ No newline at end of file
diff --git a/src/components/ApiTabs.js b/src/components/ApiTabs.js
new file mode 100644
index 0000000..061c580
--- /dev/null
+++ b/src/components/ApiTabs.js
@@ -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;
\ No newline at end of file
diff --git a/src/components/App.js b/src/components/App.js
new file mode 100644
index 0000000..2d8c45a
--- /dev/null
+++ b/src/components/App.js
@@ -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;
\ No newline at end of file
diff --git a/src/constants/index.js b/src/constants/index.js
new file mode 100644
index 0000000..c76a669
--- /dev/null
+++ b/src/constants/index.js
@@ -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" }
+];
\ No newline at end of file
diff --git a/src/styles/main.css b/src/styles/main.css
new file mode 100644
index 0000000..16f5522
--- /dev/null
+++ b/src/styles/main.css
@@ -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;
+    }
+}
\ No newline at end of file
diff --git a/src/utils/cookies.js b/src/utils/cookies.js
new file mode 100644
index 0000000..80b1a48
--- /dev/null
+++ b/src/utils/cookies.js
@@ -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=/`;
+}
\ No newline at end of file